본문 바로가기
JAVA

async/sync & blocking/ non-blocking (동기/비동기 & 블로킹/논블로킹)

by 아이티.파머 2022. 11. 28.
반응형

동기화, 비동기화 & 블로킹/논블로킹을 이야기하다보면 아직도 많이 혼란스럽다. 최근에는 reactive 프로그래밍이라고 하여 프론트에서는 동기/비동기화 관련 글들이 많고 서버에서도 블로킹/논블로킹관련 reactive(리액티브) 글들이 많다. UI에서는 대표적으로 Vue.js 나 React.Js 를 꼽을수있고, 서버쪽에서는. RxJava(RetiveX), Spring Reactor(https://projectreactor.io/) 를 들을수있다.

간단하게 설명하면 Reactive X는 넷플릭스에서 공개한 오픈소스라이브러리 이고, Reactor는 Spring 오픈소팀이 만든 프레임워크 이다.

참고) Advanced Reactive Java

그럼 우리가 흔히 알고있는 async 및 sync 에 대하 먼저 알아보자. 간단하게 생각하면 동기비동기나, 블로킹논블로킹은 모두 비슷한것 처럼 느껴진다. 왜냐하면 Job을 처리함에 있어 A→ B로 호출할때 응답을 기다리지 않고 A의 다른 일을 수행하기 때문이다. 하지만 이두가지 동기비동기와, 블로킹논블로킹은 관점의 차이가 분명히 존재한다.

보통 웹에서 우리가 사용하는 기술인 Ajax,axios 는 XMLHttpRequest를 확장하여 만든 대표적인 동기 비동기 기술이다. 웹화면에 진입했을때 기본 프레임은 먼저 보여주고 데이터는 Async 기술을 이용하여 가져옴으로 사용자에게 빠르게 보여줄수 있는 데이터 부터 화면에 보여준다. 그렇다면 backand 기술에는 동기 비동기 기술이 없냐고 물어볼수있다. 답변은 당연히 있다. Future,FutureTask, ComplateableFeture 를 이용하여 동기, 비동기 기술을 구현 할 수 있다. 이처럼 front 에만 async/sync 있고, backand 에만 blocking/non-blocking 기술이 있는것이 아니다.

async/sync :

sync : 작업을 동시에 수향하거나, 동시에 끝나거나, 끝나는 동시에 시작함을 의미한다,

async : 시작,종료가 일치하지 않으며, 끝나는 동시에 시작을 하지 않음을 의미한다.

요청을 하고 결과값을 계속 기다리다가 결과가 왔을때 바로 실행하느냐, 요청을 하고 결과값이 올때 까지 언제 끝나는지 계속 확인을 하느냐의 차이

blocking/non-blocking

blocking : 자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 다른 작업이 끝날때까지 기다렸다가 다시 자신의 작업을 시작하는것

non-blocking: 다른 주체의 작업과 관련없이 자신의 작업을 수행하는것

→ A가 B를 호출하고 나서도 A가 다른 일을 할 수있는지에 대한 관점. 즉 로직의 흐름이 멈추느나 멈추지 않고 진행되느냐의 관점이다. (제어권, 작업의흐름 )

그림으로 살펴보면 다음과 같다.

Async / Sync

Async / sync는 작업의 결과를 직접 받아서 사용할것인지 아닌지에 대한 관점이 다를뿐이다.

Sync(동기화)

  • Sync 는 작업의 결과를 기다렸다 직접받아서 사용한다.
  • 커피를 주문하고 A는 B 가 커피를 만들어 줄때까지 아무것도 할 수 없다. B 가 커피를 만들어 준뒤에야 다른 작업이 가능하다.

 

Async(비동기화)

  • Async는 작업의 결과를 직접 받지않고 다른 작업을 할 수 있다. 호출한 A 함수는 B 함수가 어떤 작업을하는지는 신경쓰지 않는다(거의 즉시응답). 호출받은 함수 B가 작업의 결과를 반환해야 한다면 callback 기능을 사용하여 호출한 A 함수와는 관계없이 데이터를 반환한다.
  • A 는 getCoffeeAsync 함수를 호출하고 즉시응답받아 다른일을 수행한다. B에게 커피를 달라고하고 B가 커피를 만들동안 다른일을 하는것이다. 응답을 받을때 A는 B에게 커피가 만들어 졌는지 지속적으로 물어보고 B의 커피만들기가 끝났을때 B는 callback 기능을 통해 만들어진 커피를 A에게 전달한다.

blocking / non-blocking

제어권을 넘겨주고 다른일을 수행할수있는 흐름을 만들어 주는것인지, 제어권을 반환하지 않고 순서대로 하나씩 수행되게 하는것인지에 따라 blocking과 non-blocking 으로 나눌수있다.

Blocking

제어권을 가지고 순서대로 실행한다. A → B

  • getCoffeeAsync를 호출했지만 결국 리턴받을때는 get() 을 사용함으로 B의 결과(커피를 얻기위해)를 기다리기위해 blocking 상태가 된다.
  • blocking은 A가 B를 호출하고난뒤에 get()을 호출하고 결과를 받을때 까지 아무일도 할 수 없다. 이는 앞서 이야기한 sync와 비슷하다. sync와 blocking 모두 결과값을 주지 않으면 아무작업도 할 수 없기 때문이다.
  • 기능은 같더라도 blocking 과 sync 를 구분할수 있는것은 다음과 같다.
    • blocking 작업의 제어권을 가지고 반환하지 않고 순서대로 A,B가 수행되는것이고
    • sync 작업의 결과물을 B로부터 받아 작업완료여부를 A가 확하는지에 대한것이다.

(이렇게 적어두어도 저말이 이말같고 이말이 저말같다)

Blocking 예제코드

/**
 * Thread를 사용함으로 Non-blocking 같지만
 *  futureTask.get() 을 사용함으로 중간에 blocking이 된다.
 * @throws Exception
 */
public void getCoffeeBlocking() throws Exception{
    System.out.println("1. 커피주문 Nonblocking - blocking");
    CafeService service = new CafeService();
    System.out.println("2. Nonblocking - blocking");

    FutureTask<CoffeeEntity> futureTask = new FutureTask<>(()-> service.getCoffee("라때",2000L));
    Thread thread = new Thread(futureTask);
    thread.start();

    while (!futureTask.isDone()) {
        System.out.printf("3. other job %d \n", index++);
        Thread.sleep(500);

    }
    System.out.println("4. blocking : " +futureTask.get());
    System.out.println("5. other job");
    System.out.println("6. end");

}

public class CafeService {
    public CoffeeEntity getCoffee(String name, long price) throws InterruptedException {
        return this.makeCoffee(name,price);
    }

    private CoffeeEntity makeCoffee(String name, long price) throws InterruptedException{
        // 커피 만들때 항상 2초가 걸린다.
        Thread.sleep(2000);
        return new CoffeeEntity(name,price);
    }
}

Non-blocking

블로킹은 제어권을 바로 반환하지 않고 A,B 순서대로 진행하지만 논블로킹은 제어권을 반환하여 A가 B를 호출하고도 다른 작업을 할 수있도록 한다.

  • A가 B를 실행하고 B의 응답결과를 Callback 으로 받으며 A는 다른 업무를 할 수있다.
  • A가 B에게 커피를 요청하고 B가 커피를 만들동안 A는 휴대전화로 유트브를 보거나 전화를 하는 다른 업무를 수행할수있다. 이후 B의 커피만들기가 완성되었을때 callback으로 A에게 커피를 전달한다.

Non-blocking 예제코드

/**
 * Thread 를 사용하고 Non-blocking 하게 동작한다.
 * 이유는 get() 을할때 callback 으로 실행되기 때문이다.
 * @throws Exception
 */
public void getCoffeeNonblocking() throws Exception{
    System.out.println("1. 커피주문 Nonblocking");
    CafeService service = new CafeService();
    FutureTask<CoffeeEntity> coffeeEntityFutureTask = new FutureTask<>(() -> service.getCoffee("라때",3000L))
    {
        @Override
        public void done() {
            try {
                System.out.println("3. callback data : " + this.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
    };
    System.out.println("2. Nonblocking");
    executorService.execute(coffeeEntityFutureTask);
    executorService.shutdown();

    while (!coffeeEntityFutureTask.isDone()) {
        System.out.printf("4. other job %d \n", index ++);
        Thread.sleep(500);
    }

    System.out.println("5. END");
}

public class CafeService {
    public CoffeeEntity getCoffee(String name, long price) throws InterruptedException {
        return this.makeCoffee(name,price);
    }

    private CoffeeEntity makeCoffee(String name, long price) throws InterruptedException{
        // 커피 만들때 항상 2초가 걸린다.
        Thread.sleep(2000);
        return new CoffeeEntity(name,price);
    }
}

전체 예제코드

package thread.coffee;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**
 *
 * @author skan
 * @since 2022/11/17
 */
public class UserClient {

    private int index = 1;
    ExecutorService executorService = Executors.newCachedThreadPool();

    public static void main(String[] args) throws Exception {
        UserClient userClient = new UserClient();
        //System.out.println("-----------------------------------------");
        //userClient.getCoffee();
        //System.out.println("-----------------------------------------");
        //userClient.getCoffeeBlocking();
        //System.out.println("-----------------------------------------");
        userClient.getCoffeeNonblocking();
    }

    public void getCoffee() throws Exception{
        CafeService service = new CafeService();
        System.out.println("1. 커피주문 Blocking");
        System.out.println("2. Blocking");
        CoffeeEntity coffeeEntity = service.getCoffee("아메", 1000L);
        System.out.println("3. blocking :" + coffeeEntity);
        System.out.println("4. other job");
        System.out.println("5. END");
    }

    /**
     * Thread를 사용함으로 Non-blocking 같지만
     *  futureTask.get() 을 사용함으로 중간에 blocking이 된다.
     * @throws Exception
     */
    public void getCoffeeBlocking() throws Exception{
        System.out.println("1. 커피주문 Nonblocking - blocking");
        CafeService service = new CafeService();
        System.out.println("2. Nonblocking - blocking");

        FutureTask<CoffeeEntity> futureTask = new FutureTask<>(()-> service.getCoffee("라때",2000L));
        Thread thread = new Thread(futureTask);
        thread.start();

        while (!futureTask.isDone()) {
            System.out.printf("3. other job %d \n", index++);
            Thread.sleep(500);

        }
        System.out.println("4. blocking : " +futureTask.get());
        System.out.println("5. other job");
        System.out.println("6. end");

    }

    /**
     * Thread 를 사용하고 Non-blocking 하게 동작한다.
     * 이유는 get() 을할때 callback 으로 실행되기 때문이다.
     * @throws Exception
     */
    public void getCoffeeNonblocking() throws Exception{
        System.out.println("1. 커피주문 Nonblocking");
        CafeService service = new CafeService();
        FutureTask<CoffeeEntity> coffeeEntityFutureTask = new FutureTask<>(() -> service.getCoffee("라때",3000L))
        {
            @Override
            public void done() {
                try {
                    System.out.println("3. callback data : " + this.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        System.out.println("2. Nonblocking");
        executorService.execute(coffeeEntityFutureTask);
        executorService.shutdown();

        while (!coffeeEntityFutureTask.isDone()) {
            System.out.printf("4. other job %d \n", index ++);
            Thread.sleep(500);
        }

        System.out.println("5. END");
    }
}

동기/비동기 그리고 블로킹/논블로킹 기술모두 A가 B를 호출하도난뒤 A 가 다른일을 할 수 있는지 없는지에 따라 나뉘어진다.

그렇다면 async = non-blocking, sync = blocking 일까? 라고 생각할수있다. 언듯 보기에 큰개념상에서 보면 비슷한 내용이지만 보는 관점에 따라 다르게 표현한다는 말이 맞을것 같다.

동기/비동기의 경우엔 A가 B를 호출하고난뒤에 B에대한 작업완료여부를 A가 관여하는 것이고 (끝날때 까지 지속적으로 확인함), 블로킹 논블로킹은 제어권으 개념으로 A가 B를 호출했을대 제어권이 A에게있는냐 B에가 있느냐 그리고 제어권을 A에게 다시 넘겼는냐 넘기지 않았느냐로 볼수있다.

즉 두가지 모두 비슷한 개념으로 사용되면서도 어떻게 바라보냐에 따른 시각차(관점)에 따라 다르게 표현 할 수있다. Sync/Async 는 함수의 결과 값 즉 함수의 종료여부에 대해 호출자가 관여 하느냐 관여하지 않느냐의 관점으로 보고 Blocking/Non-blocking은 프로세스에 대한 제어권을바로 넘겨주느냐 혹은 결과가 끝난뒤에 넘겨주느냐의 관점으로 생각해볼수있다.

참고

RxJava vs Reactor 뭐가 좋나요 | Hyoj blog

이제 Async/Sync 그리고 Non-blocking/Blocking 의 조합을 생각해볼수있다.

기본적으로 동기-비동기, 블라킹-논블로킹을 검색하면 다음과 같은 표와 그림을 마주하게 된다. 처음엔 이게 무슨말이지 싶겠지만 위에 내용을 읽고 내려오면 왜 이런 조합이 나오는지 알 수있다.

 

 

실제 예를 들어 조금더 쉽게 다가가보자. 커피를 주문하는 것을 예로 조합을 상상해 보면 다음과 같다.

Synchronized/Blocking

나 : 아메리카노 하나요 (기다린다)
사장님 : 넵 주문받았습니다.
나 : 아무것도 안하고 커피가 올때까지 서있는다.
사장님 : 커피나왔습니다.
나: 네 감사합니다.

Synchronized / non-blocking

나 : 아메리카노 하나 주세요 (기다린다)
사장님 : 네 주문 받았습니다.
나 : 어디가지 못하고 기다린다.
사장님 : 아메리카노 만들고 다른사람이 주문하는것도 주문받고 주방에도가고 직원들한테 교육도 시킨다.
나 : 기다린다
사장님 : 여기 아메리카노 나왔습니다.
나 : 감사합니다.

Asynchronized / Blocking

나 : 아메리카노 하나 주세요
사장님 : 넵 주문받았습니다. (진동벨을 건내준다.)
나 : 자리에 갔다가, 휴지도 같다놓고 이것저것 하다가 사장님께 물어본다. “다됐나요?”
사장님 : 아니요.
나: 다됐나요?
사장님 : 아니요.
나 다됐나요?
사장님 : 진동벨을 울리며 커피가 다됐음을 알려준다.
나 : 네 감사합니다.

Asynchronized / Non-blocking

나 : 아메리카노 하나 주세요
사장님 : 네 주문 받았습니다. (진동벨을 건내준다.)
나 : 이리저리 돌아다니다가 자리에 앉아 전화도하고, 인터넷서핑도한다.
사장님 : 다른 사람주문도 받고 커피도 만들고 , 직원들 교육도한다.
나 : 잉여시간을 즐긴다.
사장님 : 진동벨을 울리며 커피가 나왔음을 알린다.
나 : 커피가지로 간다.

반응형