동기(Sync)/비동기(Async) +블로킹(Blocking)/논블로킹(Non-Blocking)

김진호·2023년 7월 17일
0

Study

목록 보기
1/4

0. 동기/비동기 & 블로킹/논블로킹

비동기 처리에 관한 이야기에 앞서, 동기에 관한 이야기를 해보려고 한다. 왜냐하면 비(非)동기동기가 아니다라는 의미이기 때문에 동기가 무엇인지부터 정확히 하고 싶었다.
또한, 이들과 혼동되어 쓰이는 블로킹/논블로킹은 어떤 의미인지 추가적으로 알아보려고 한다.


1. 동기(Sync) vs. 비동기(Async)

동기(Sync)와 비동기(Async)는 작업의 처리 방식을 나타내는 개념이다.
결론부터 말하면, 이 둘의 주요한 차이점은 작업 순서 처리 차이이다.


1) 동기 (Synchronous) 란?

검색을 해보니 CS에서의 '동기'란 동시에 발생하는 것이라는 의미라고 한다.
필자는 대충 '겹치지 않고 순차적으로 작업을 진행하는 것 정도'로 이해했던 터라, "동시"라는 단어에서 논리적인 모순이 발생한다고 생각했다.

영어 사전을 참고하여 보자

synchronous

adjective

happening or done at the same time or speed:

출처 : https://dictionary.cambridge.org/

"Synchronous"는 동시에 발생하는 두 개 이상의 사건이나 프로세스가 동일한 시간에 진행되는 것을 의미하는 형용사이다.
번역 과정에서 편의를 위해 명사로 번역한 것으로 보이지만, 중요한 것은 "동일한 시간에 여러 개의 사건이 진행" 된다는 것이다.
여기서 오해하면 안되는 것이 이는 "병렬적인 작업방식을 말하는 것이 아니다."

"어떠한 일정 시점에 현재 작업의 Response과 다음 작업의 Request이 함께 진행되는 것"이라고 이해하는 것이 편할 것이다.
이는 작업이 순차적으로 처리되며, 이전 작업이 완료될 때까지 다음 작업이 시작되지 않음을 나타낸다.

CS에서의 Concurrency(동시성)Parallelism(병렬성)의 개념을 떠올리면 이해하기 쉽다.
운영체제에서 싱글 코어에서 멀티 쓰레드(Multi thread)를 동작 시키는 방식을 "동시성"이라고 하고,
멀티 코어에서 멀티 쓰레드(Multi thread)를 동작시키는 방식을 "병렬성"이라고 한다.

  • 즉, 동시에 진행된다는 말은 "순차적으로 여러 작업이 진행된다." 와 같이 생각하면 된다.

일반적으로 동기는 특정 작업이 완료될 때까지 다른 작업을 대기시키는 동작을 나타낸다.
이는 순차적인 프로그램 흐름과 함께 작동하며, 결과적으로 코드의 실행 순서가 예측 가능하다.

어떤 블로그를 참고하니, 이런식으로 '동기'를 설명하고 있다.

동기 작업은 작업의 완료를 기다리는 동안 호출자가 블로킹 되며, 다른 작업을 수행할 수 없게 된다.

하지만, '동기'와 '블로킹'은 엄연히 다른 개념이다.

일반적으로 블로킹을 하여 동기 작업을 진행하는 경우가 다수이지만, 동기 작업과 블로킹이 항상 함께 진행되어야만 하는 것은 아니다.

정리하자면,

  • 동기 방식은 작업이 어떠한 순서를 가지고 진행된다는 것 정도로 이해하면 좋을 것 같다.

2) 비동기 (Asynchronous) 란?

비동기 방식은 반대로 요청을 보냈을 때 응답 상태와 상관없이 다음 동작을 수행 할 수 있다.

즉, 요청된 작업이 완료될 때까지 기다리지 않고, 다른 작업을 수행할 수 있다.
비동기 작업은 작업이 완료되면 결과를 반환하거나, 콜백 함수를 호출한다.

정리하자면, 호출자는 비동기 작업을 시작한 후에 다른 작업을 수행할 수 있으며, 작업 완료 여부를 확인하거나 결과를 받기 위해 대기하지 않는다.

이해를 위해 상황으로 간단한 예시를 들어보겠다.


비동기적인 파일 다운로드를 수행하는 경우를 생각해보자.

동기적인 방식으로 파일을 다운로드한다면, 파일이 다운로드될 때까지 아무런 작업을 수행할 수 없고 파일이 완전히 다운로드된 후에야 다음 작업을 진행할 수 있다.
매우 비효율적인 작업 방식이다.

그러나 비동기적인 방식으로 파일을 다운로드한다면, 파일 다운로드 작업을 시작한 후에 다른 작업을 수행할 수 있다.
파일이 다운로드되면 특정 콜백 함수를 호출하여 다운로드된 파일을 처리하거나 결과를 반환받을 수 있다.
이렇게 하면 파일 다운로드 작업이 진행되는 동안 다른 작업을 수행할 수 있으며, 파일 다운로드가 완료되면 그 때 결과를 처리할 수 있다.



2. 블로킹(Blocking) vs. 논블로킹(Non-Blocking)

둘 간의 주요한 차이점은 제어권을 어떻게 처리하느냐의 여부이다.
블로킹 방식은 작업이 완료될 때까지 대기하고 제어권을 반환하지 않으며,
논블로킹 방식은 작업을 시작한 후에 제어권을 반환하여 다른 작업을 수행할 수 있다.

그렇다면 기준이 되는 "제어권"의 의미는 무엇일까?

  • 제어권
    : 제어권(Control flow)은 프로그램의 실행 흐름을 제어하는 권한 또는 컨트롤을 의미한다.
    제어권을 가진 함수는 자신의 코드를 끝까지 실행한 후, 자신을 호출한 함수에게 return 한다.

1) 블로킹(Blocking) 이란?

둘 간의 구분 방법은 호출된 함수(callee)가 호출한 함수(caller)에게 제어권을 넘겨 주는지 여부로 구분된다고 하였다.
제어권이 넘어가버리면 해당 스레드는 블로킹되게 된다.

즉, 위의 사진에서 A함수는 B함수에게 제어권을 넘겨줌과 동시에 함수 실행을 일시 정지하게 된다. = Blocking

2) 논블로킹(Non-Blocking) 이란?

논블로킹은 A함수가 B함수를 호출해도 제어권은 그대로 자신이 가지고 있는다.
호출된 B 함수는 실행되지만, 제어권은 A 함수가 그대로 가지고 있는다.
호출자(caller)는 작업이 완료되었는지 여부를 확인하거나 결과를 받기 위해 대기하지 않고 다른 작업을 수행할 수 있다.



3. 두 개념의 조합

두 가지 개념을 조합하면 4가지 형태의 조합이 나온다.

1) Sync + Blocking (동기 + 블로킹)
2) Sync + Non-Blocking (동기 + 논블로킹)
3) Async + Non-Blocking (비동기 + 논블로킹)
4) Async + Blocking (비동기 + 블로킹)

보통 1, 3번의 형식이 묶여서 흔히 사용된다.

1) Sync + Blocking (동기 + 블로킹)

함수 A는 함수 B의 리턴값을 필요로 한다.
이로 인해 작업의 흐름이 순차적으로 진행되는 것이 보장된다. (Sync)
제어권을 함수 B에게 넘겨준 후 함수 A는 대기한다.
함수 B가 실행을 완료하여 리턴값과 제어권을 돌려줄 때까지 함수 A는 기다리는 것이다. (Blocking)
즉, 두 가지 이상의 작업이 동시에 진행될 수 없다.

직관적으로 이해하기 쉽게 "코드가 순차적으로 진행된다."라고 이해하면 될 것 같다.

작업이 완료될 때까지 기다려야 하는 경우에 사용된다.
예를 들면, 파일을 읽은 후 처리하는 코드를 생각해보자.
파일을 처리하는 과정은 필연적으로 파일을 모두 읽은 후에 작업이 가능하기 때문이다.
또한 일반적으로 작업이 간단하거나 작업량이 적은 경우에 사용된다.
작업량이 많거나 오래 걸리는 작업을 이러한 방식으로 처리하면 매우 비효율적이기 때문이다.


2) Sync + Non-Blocking (동기 + 논블로킹)

A 함수가 B 함수를 호출할 때, A 함수는 B 함수에게 제어권을 주지 않고 자신의 코드를 계속 실행한다. (Non-Blocking)
즉, 다른 작업이 진행되는 동안에도 자신의 작업을 처리할 수 있다.
A 함수는 B 함수의 리턴값이 필요하기 때문에, 중간중간 B 함수에게 함수 실행을 완료했는지 물어본다.
이로써 작업을 순차대로 수행할 수 있는 것이다. (Sync)

그런데 어쩌면, 동기 + 논블로킹 방식이 직관적으로 이해가 잘 되지 않을 수 있다.
다른 작업을 막지 않고 어떻게 순차적인 작업 수행이 가능하다는 것인가?

답은 중간 중간 B 함수에게 함수 실행을 완료했는지 물어보는 방식을 활용하는 것이다.

다음 java 코드를 보자.

// Runnable 인터페이스를 구현하는 클래스 정의
class Task implements Runnable {
    @Override
    public void run() {
        // 비동기로 실행할 작업
        System.out.println("스레드 실행!");
        for(int i=0 ; i<5 ; i++)
        System.out.println(i+"번째 수행 중");
    }
}

public class Main {
    public static void main(String[] args) {
        // Thread 객체 생성
        Thread thread = new Thread(new Task());

        // 스레드 실행
        thread.start();

        // Non-Blocking이므로 다른 작업 계속 가능하다는 의미
        System.out.println("메인 스레드가 실행중입니다.");

        // Sync를 위해 스레드의 작업 완료 여부 확인 <계속 확인하는 과정이 필요하다>
        while (thread.isAlive()) {
            System.out.println("나는 다른 작업할게. 끝나면 알려줘.");
        }
        
        System.out.println("스레드 종료!");
        
        System.out.println("다음 작업 수행 가능합니다.");
    }
}

결과

메인 스레드가 실행중입니다.
스레드 실행!
나는 다른 작업할게. 끝나면 알려줘.
1번째 수행 중
나는 다른 작업할게. 끝나면 알려줘.
나는 다른 작업할게. 끝나면 알려줘.
2번째 수행 중
나는 다른 작업할게. 끝나면 알려줘.
나는 다른 작업할게. 끝나면 알려줘.
3번째 수행 중
4번째 수행 중
나는 다른 작업할게. 끝나면 알려줘.
5번째 수행 중
나는 다른 작업할게. 끝나면 알려줘.
스레드 종료!
다음 작업 수행 가능합니다.

작업을 병렬적으로 처리하도록 지시했지만,
메인 코드의 while문을 수행함으로써 요청한 작업의 완료 여부를 계속 확인하고 결과적으로 결국 동기적으로 작업을 순서대로 수행된다고 볼 수 있는 것이다.

사실 이러한 방식의 구현은 흔하지는 않다.
하지만, 활용되는 예를 들자면 로딩화면을 구성할 때 사용할 수 있다.
필요한 정보를 load 하는 중에 사용자에게 ProgressBar (load된 퍼센트)를 보여줘야 하는 경우가 있다.
load 하는 동작 완료할 때 까지, 이에 맞춰서 ProgressBar를 화면에 띄우는 작업을 할 수 있다.

🤔 <추가적인 이야기>
언어 간에도 구현 방식의 차이가 있을 수 있다.
언어 자체의 특성과 제공되는 라이브러리 또는 기술에 따라 구현 방법이 다를 수 있다는 것이다.
JavaScript는 단일 스레드 기반으로 동작하며, 콜백 함수, 프라미스, async/await 등의 메커니즘을 사용하여 동기 + 논블로킹 구현할 수 있다.
기본적으로 Promise.then 핸들러 방식은 비동기+논블로킹 방식이라 볼 수 있다.
하지만 async/await 키워드를 사용하여 순서를 지정해 줄 수 있다.
주의 해야할 점이 async 함수 자체가 메인 콜 스택(call stack)이 모두 실행되어 비워져야 수행하기 때문에 async/await은 내부적으로는 여전히 비동기 논블로킹 방식으로 동작한다는 점을 유의하자.


3) Async + Non-Blocking (비동기 + 논블로킹)

비동기 + 논블로킹 방식은 비교적 이해하기 쉽다.
제어권을 넘기지 않고 다른 작업이 진행되는 동안에도 자신의 작업을 처리할 수 있고, (Non-Blocking),
다른 작업의 결과를 바로 처리하지 않아 작업 순서가 지켜지지 않는 방식이다. (Async)
B 함수를 호출할 때 콜백 함수를 함께 줌으로써, B 함수는 자신의 작업이 끝나면 A 함수에게 준 콜백 함수를 실행시켜 자신의 작업이 끝났음으로 알려준다.

이전의 동기 + 논블로킹 방식은 A 함수(caller)가 B 함수(callee)의 실행이 종료되었는지 계속 예의 주시 해야했다.
비동기 + 논블로킹은 B 함수의 종료 여부에는 일단 관심이 없다.
B 함수가 자신의 실행을 완료했다면, A 함수의 어깨를 톡톡 쳐서 알려주면 되는 것이다.

다른 작업의 결과가 자신의 작업에 영향을 주지 않는 경우에 활용할 수 있다.
또한, 대용량의 데이터를 처리하는 서비스에서 흔히 사용된다.
가장 큰 특징은 호출 함수에 콜백 함수를 넣었다는 점이다.


4) Async + Blocking (비동기 + 블로킹)

이러한 방식은 실무에서 마주하기 흔치 않다.
아마 개발하는 과정에서 실제로 다룰 일은 거의 없을 것이다.

개념만 간단히 설명하자면,
다른 작업이 진행되는 동안 자신의 작업을 멈추고 기다린다. (Blocking)
다른 작업의 결과를 바로 처리하지 않고, 콜백함수를 보낸 후 기다림으로써 순서대로 작업을 수행되지 않을 수 있다. (Async)

따라서, A 함수는 자신과 관련 없는 B 함수의 작업이 끝날 때까지 기다려야 한다.
매우 비효율적인 로직이라고 볼 수 있다.

첫 번째 그림을 보면 이해하기 편하다.
1 작업 중에 2 작업을 해야지 Async의 이점을 살릴 수 있는 것인데,
이것을 다시 Blocking 방식을 사용함여 대기시킴으로써 이점을 제거해버린 것이다.....

+ 이러한 로직을 사용할 일은 거의 없지만, 반 강제적으로 활용(?)되는 예시는 있다.
node JS는 async와 non-blocking 로직으로 동작된다.
이러한 node JS에서 blocking 방식으로 작동되는 mysql DB에 접근하는 경우이다.
+추가적으로 이러한 동작방식은 개발자에게 혼동을 주기 때문에 실무에서는 Node.js 서버룰 프로그래밍할때 아예 async/await로 동기 처리를 해주는 편이다.

profile
멋쟁이

0개의 댓글