본문 바로가기
Language/자바&코틀린

[I/O 이해하기-1] 동기와 비동기 VS Blocking과 Non Blocking

by 코딩공장공장장 2024. 5. 5.

 

동기와 비동기 요청 작업의 완료여부를 체크하고 작업을 순차적으로 처리하느냐의 차이로 구분할 수 있고,

Blocking과 NonBlocking 제어권을 누가 갖는지에 따른 작업 차단(block) 유무로 구분할 수 있다.

 

동기와 비동기는 순서에 관한 것이고, Blocking과 NonBlocking은 작업 차단 유무에 관한 것이다.

동기와 비동기

 

 

동기는 요청 작업의 완료여부를 체크하고 작업을 순차적으로 진행한다. 

특정 함수를 호출하면 완료 응답을 받고 다음 작업을 진행한다.

통신 환경이라면 사용자의 요청에 응답을 제공한 이후 다음 요청 작업을 처리할 수 있다.

 

비동기는 요청 작업의 완료여부를 체크하지 않고 작업이 순차적으로 진행되지 않을 수 있다. 

특정 함수를 호출했을 때 완료 응답을 고려하지 않고 다음 작업을 진행한다.

통신 환경이라면 사용자의 요청에 응답을 바로 제공하지 않더라도 다음 요청 작업을 처리할 수 있다.

 

[참고] ‘완료 여부 체크’라는 것은 응답을 받는다거나 직접 피호출자의 상태(속성)를 확인하는 처리를 말한다.

Blocking과 NonBlocking

 

Blocking과 NonBlocking의 구분은 제어권을 누가 갖는지에 따른 작업 차단 유무이다.

Blocking은 호출 함수(caller)가 피호출 함수(callee)에게 제어권을 넘겨준다.

제어권이 없는 호출함수는 피호출함수가 작업을 완료하고 제어권을 돌려줄 때까지 차단되어 다른 작업을 진행하지 못한다.

Non Blocking은 피호출함수가 제어권을 받은 즉시 호출함수에게 제어권을 돌려준다. 

호출함수는 자신의 작업을 곧바로 진행할 수 있다.

 

Blocking 방식에서는 작업이 차단되기에 동시에 하나의 작업만 이뤄지고,

NonBlocking 방식에서는 작업이 차단되지 않기에 동시에 여러 작업을 진행할 수 있다.

 

동기/비동기, Blocking/NonBlocking의 구분

동기/비동기와 Blocking/NonBlocking은 따로 사용되는 개념이라기 보다 조합되어 사용된다.

대게 동기는 Blocking 방식과 함께, 비동기는 NonBlocking 방식과 함께 사용되기에

사용성을 본다면 동기=Blocking, 비동기=NonBlocking으로 이해해도 되지만

동기-NonBlocking의 사용성도 꽤 있기에 이론적으로 명확하게 구분하는 것은 중요하다.

 

위에서 정의한 것처럼 동기/비동기는 요청 작업의 완료 여부를 체크하여 순차적으로 진행하는지,

Blocking/NonBlocking은 제어권에 따른 차단여부로 구분된다.

 

개념을 혼동하게 만드는 부분이 동기와 비동기를 동시에 여러 작업이 가능한지로 구분하려는것에 있다고 생각한다.

“동시에 여러 작업 처리 가능 여부는 동기/비동기가 아닌 Blocking/NonBlocking이 결정한다.”

동시 작업 가능 여부는 호출자를 완전히 차단(Block)하는지 여부가 결정한다.

작업 순서에 의해 결정되는 것이 아니다.

동기의 경우 동시에 여러 작업이 수행될 수 있다.

비동기의 경우에도 동시에 하나의 작업만 처리 가능한 경우가 있지만 사용성이 거의 없다.

 

동기/비동기와 Blocking/NonBlocking을 조합하여 사용되는 경우의 사례를 보면 그 이유를 알 수 있을 것이다.

 

동기-Blocking

요청 작업의 완료 여부를 체크하여 순차적인 처리를 진행하고, 호출함수는 차단(block)되어 다른 작업을 할 수 없다.

 

Stream을 통한 I/O 작업이 대표적인 동기-블로킹 방식의 예이다.

public class SyncBlockingWithStream {

   public static void main(String[] args) throws IOException {

       // 서버 소켓 생성

       ServerSocket serverSocket = getServerSocket();

       while (true) {

           try {

               System.out.println("1. [main] : accept 메서드에 제어권 전달");

               // 클라이언트 요청 대기 및 승인

               Socket clientSocket = serverSocket.accept();

               System.out.println("2. [main] : accept가 클라이언트 요청 승인하고 제어권 반환");

 

               // 동기 블로킹 함수 호출

               System.out.println("3. [main] : read 메서드에 제어권 전달");

               String clientData = readClientData(clientSocket);

               System.out.println("5. [main] : 클라이언트 요청 데이터 읽기 종료로 제어권 반환");

               System.out.println("6. [main] : 클라이언트의 요청 데이터 = " + clientData);

 

               System.out.println("7. [main] : 제어권 되찾음");

           } catch (IOException e) {

               throw new RuntimeException(e);

           }

       }

   }

 

   // 요청 데이터 읽기

   private static String readClientData(Socket clientSocket) {

       try {

           InputStream inputStream = clientSocket.getInputStream();

           StringBuilder clientData = new StringBuilder();

           int byteRead;

           while ((byteRead = inputStream.read()) != -1) {

               if (clientData.isEmpty()) System.out.println("4. [stream] : 클라이언트 요청 데이터 읽기 시작");

               clientData.append((char) byteRead);

               if ((char) byteRead == '\n') break;

           }

           return clientData.toString();

       } catch (IOException e) {

           System.out.println("Error reading client data: " + e.getMessage());

           return null;

       }

   }

 

   // 서버 소켓 생성

   private static ServerSocket getServerSocket() throws IOException {

       ServerSocket serverSocket = new ServerSocket();

       SocketAddress address = new InetSocketAddress("localhost", 8000);

       serverSocket.bind(address);

       return serverSocket;

   }

}

위의 예제 코드는 대표적인 소켓 프로그래밍의 구현 방식이다.

서버 소켓을 생성하고 while문으로 무한 루프를 돌며 클라이언트의 요청이 들어왔는지 확인하고 수락한다.

 

위 코드를 실행시키면 아래와 같이 출력된다.

 

[출력]

1. [main] : accept 메서드에 제어권 전달

 

accept 메서드는 블로킹 메서드로 클라이언트의 요청이 들어올때까지 main 스레드는 차단(block)되어 있다.

따라서 그 이후 코드들을 accept가 제어권을 반환할 떄까지 실행되지 않는다.

프로그래밍 서버 콘솔창에 아무것도 출력되지 않음을 통해 확인할 수 있다. 

 

이제 터미널을 열어 아래와 같이 입력하여 프로그래밍 서버에 접속해보자.

nc localhost 8000

 프로그래밍 서버 콘솔 창에 아래와 같이 출력 될 것이다.

 

[출력]

2. [main] : accept가 클라이언트 요청 승인하고 제어권 반환

3. [main] : read 메서드에 제어권 전달

 

클라이언트가 로컬 서버에 접속을 했기 때문에 accept 메서드가 main에 완료 응답을하고 이시점에 제어권도 반환한다.

제어권을 반환 받았기에 2번 3번 로직이 실행될 수 있는 것이다.

 

그 다음 실행되는 InputStream의 read 메서드 또한 blocking 메서드다.

서버로 전송된 데이터를 읽어들이는데 도착하지 않으면 도착할때까지 대기(block)하고 있는다.

 

이제 터미널에 client data라고 입력하고 엔터 키를 눌러보자.

프로그래밍 서버에 데이터를 전송하는 것이다.

 

[출력]

4. [stream] : 클라이언트 요청 데이터 읽기 시작

5. [main] : 클라이언트 요청 데이터 읽기 종료로 제어권 반환

6. [main] : 클라이언트의 요청 데이터 = client data

7. [main] : 제어권 되찾음

1. [main] : accept 메서드에 제어권 전달

 

read 메서드가 입력된 데이터를 읽어들이면 입력된 데이터와 함께 제어권을 main 스레드에 반환한다.

main은 자신의 실행로직을 수행하고 다시 루프를 돌아 다시 accept 메서드에서 block 될 것이다.

 

출력 결과를 보면 번호 순서대로 출력되었다. 순서가 지켜진 동기적 처리가 됬음을 알 수 있다.

accept 메서드와 read 메서드가 실행되는 동안 터미널에 그 어떤 문자열도 출력되지 않는 것을 통해 blocking으로 동작한 것도 확인하였다.

 

피호출 함수의 작업 완료를 체크하여 호출 함수의 작업이 순차적으로 처리됬고(동기), 피호출함수가 실행되는 동안 호출함수는 작업이 차단(block)되어 호출함수와 피호출함수는 동시에 작업이 진행되지 않았다.

 

동기-Non Blocking

요청 작업의 완료여부를 체크하여 순서가 지켜지고 호출함수는 block되지 않고 자신의 작업을 수행한다.

 

간단한 예시를 보자.

아래 코드는 호출함수가 존재하는 메인 스레드에서 피호출함수를 포함한 별도의 스레드를 생성하고 실행시킨다.

이하 main 스레드를 caller(호출자)라고 칭하고 main함수가 생성한 스레드를 callee(피호출자)라고 칭하겠다.

caller는 callee의 작업 진척률을 계속 확인하며 콘솔에 작업 진척률을 나타내고 있다.

public class SyncWithNonBlocking {

   public static void main(String[] args) {

       

       // callee(피호출자) 스레드 생성

       CalleeThread th = new CalleeThread();

       Thread callee = new Thread(th);

 

       // callee 호출

       System.out.println("1. [caller] : callee 호출");

       callee.start();

 

       // callee 작업 완료여부 체크

       int downloadStatus = 0;

       while (callee.isAlive()) {

           if (th.workStatus % 33 == 0 && downloadStatus != th.workStatus) {

               downloadStatus = th.workStatus;

               System.out.println("[caller] : callee의 작업 " + th.workStatus + "% 처리됨을 직접 확인함");

           }

       }

 

       // caller 작업 완료

       System.out.println("4. [caller] : 작업 완료");

   }

}

 

public class CalleeThread implements Runnable {

   // 작업 진척률

   public int workStatus = 0;

 

   @Override

   public void run() {

       System.out.println("2. [callee] : 작업 시작");

       // workStatus가 100일 떄까지 작업 진행

       while (workStatus != 100) {

           try {

               Thread.sleep(200);

           } catch (InterruptedException e) {

               throw new RuntimeException(e);

           }

           workStatus++;

           if (workStatus == 50) System.out.println("[callee] : 작업 50% 처리함");

       }

       System.out.println("3. [callee] : 작업 완료");

   }

}

[출력결과]

1. [caller] : callee 호출

2. [callee] : 작업 시작

[caller] : callee의 작업 33% 처리됨을 직접 확인함

[callee] : 작업 50% 처리함

[caller] : callee의 작업 66% 처리됨을 직접 확인함

[caller] : callee의 작업 99% 처리됨을 직접 확인함

3. [callee] : 작업 완료

4. [caller] : 작업 완료

 

메서드의 실행흐름에 따르면 1,2,3,4 순서이고 실제 출력결과를 보니 1,2,3,4 순서대로 실행되었다.

2번과 3번 사이의 출력결과를 보면 caller와 callee가 동시에 작업을 처리하고 있는것을 알 수 있다.

피호출함수가 실행되는 동안 호출자인 main 스레드가 차단되지 않았음을 의미한다.(NonBlock)

호출자인 caller는 while (callee.isAlive()) 코드를 통해 callee의 작업 완료를 직접 체크하여 완료가 되면 자신의 작업인 System.out.println("4. [caller] : 작업 완료");을 실행한다.(동기)

 

피호출 함수의 작업 완료를 직접  체크한 후 호출 함수가 자신의 작업을 완료함으로써 순차적으로 처리되고(동기), 피호출함수가 실행되는 동안 호출함수는 작업이 차단되지 않아(NonBlock)되어 호출함수와 피호출함수는 동시에 작업이 진행되었다.

 

위와 같은 방식의 대표적인 사례가 게임에 접속할 때 나타나는 로딩바나 인터넷을 통해 파일을 다운로드하는 경우이다.

게임에 접속할때 맵을 가져오라는 함수를 호출하고 메인 스레드에서는 맵의 상태를 지속적으로 파악하여 로딩바를 나타내고
완료되면 유저를 접속시키는 것이다.

인터넷을 통해 파일을 다운로드할때 사용자는 브라우저에서 다른 작업을 할 수 있다.

브라우저가 다운로드 완료여부를 지속적으로 체크하고 완료되면 완료 알림을 주는 것이다.

 

I/O 작업에서 동기-NonBlocking의 사례

 

코드를 다 읽지 않고 주석만 봐도 괜찮다.

 

public class SyncNonBlockingWithChannel {

   public static void main(String[] args) {

       try {

           ServerSocketChannel serverSocketChannel = getServerSocketChannel();

           // 피호출자 스레드 List

           List<DataReaderThread> calleeList = new ArrayList<>();

 

           while (true) {

               // 클라이언트 연결 요청 수락(NonBlocking)

               SocketChannel clientChannel = serverSocketChannel.accept();

               if (clientChannel != null) {

                   System.out.println("클라이언트 연결됨");

 

                   // 피호출자 스레드 생성

                   DataReaderThread dataReaderThread = new DataReaderThread(clientChannel);

                   Thread thread = new Thread(dataReaderThread);

                   // 피호출 함수 실행

                   thread.start();

                   // List에 피호출자 스레드 담기

                   calleeList.add(dataReaderThread);

               }

 

               // List에 담긴 첫번째 피호출자 작업 완료 체크

               if (!calleeList.isEmpty() && calleeList.get(0).isDone) {

                   // 데이터를 읽음

                   System.out.println("수신한 데이터 : " + calleeList.get(0).getClientData());

                   // 완료된 피호출자 List에서 제거

                   calleeList.remove(0);

               }

 

               // 메인 스레드 작업

               if (LocalDateTime.now().getNano() == 0) System.out.println("메인 스레드의 동작은 멈추지 않음");

           }

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

 

   private static ServerSocketChannel getServerSocketChannel() throws IOException {

       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

       serverSocketChannel.bind(new InetSocketAddress("localhost", 8000));

       serverSocketChannel.configureBlocking(false); // Non-blocking 모드 설정

       return serverSocketChannel;

   }

}

 

 

public class DataReaderThread implements Runnable {

   private final SocketChannel clientChannel;

   public boolean isDone = false;

   private String clientData;

 

   public DataReaderThread(SocketChannel clientChannel) {

       this.clientChannel = clientChannel;

   }

 

   @Override

   public void run() {

       ByteBuffer buffer = ByteBuffer.allocate(4);

 

       StringBuilder sb = new StringBuilder();

       try {

           while (clientChannel.read(buffer) != -1) {

               buffer.flip(); // 읽기 모드

               while (buffer.hasRemaining()) {

                   byte dataByte = buffer.get();

                   char character = (char) dataByte;

                   sb.append(character);

                   if (character == '\n') {

                       break;

                   }

               }

               buffer.clear();

 

               if (sb.toString().contains("\n")) break;

           }

           isDone = true;

           clientData = sb.toString();

       } catch (IOException e) {

       }

   }

 

   public String getClientData() {

       return clientData;

   }

}

main에서는 클라이언트의 요청을 승인하고 데이터를 읽는 메서드를 포함한 스레드를 생성하여 실행시키는 코드이다.

 

테스트를 위해 터미널을 두개 열어야 한다.

 

첫번째로 연 터미널에 nc localhost 8000 명령어를 실행하고 두번째로 연 터미널에 nc localhost 8000를 실행하자.

 

이제 요청 데이터를 입력하는데 입력 순서는 접속한 순서의 반대로 입력해보자.

두번째로 연결한 터미널에 먼저 입력하고 그 다음 첫번째로 연결한 터미널에 아무 데이터를 입력하자.

 

프로그래밍 서버를 실행시킨 콘솔창을 보면 첫번째로 연결한 클라이언트의 데이터가 먼저 출력될 것이다.

데이터 입력은 두번째로 했음에도 먼저 출력되었다.

데이터 입력 순서와 상관없이 요청 순서에 따라 데이터를 출력한 것이다.

// List에 담긴 첫번째 피호출자 작업 완료 체크

if (!calleeList.isEmpty() && calleeList.get(0).isDone) {

     // 데이터를 읽음

     System.out.println("수신한 데이터 : " + calleeList.get(0).getClientData());

     calleeList.remove(0);

}

피호출자 스레드 리스트의 첫번째 작업(첫번째 연결 요청)을 가져와 isDone 플래그를 통해 작업 완료 여부를 확인한다.

완료된 작업이라면 피호출자가 읽어들인 데이터를 출력하고 해당 작업을 리스트에서 제거한다.

따라서 다음 루프에서는 두번째 작업이 리스트의 첫번째 작업이 되기에 순차적으로 처리가 될 것이다.

동기 방식인 요청 작업의 작업 완료를 체크하여 순차적인 처리가 진행되는 것이다.

 

위 코드를 실행하면 main의 System.out.println("메인 스레드의 동작은 멈추지 않음");의 출력값이 콘솔창에 주기적으로 나타날 것이다.

main 스레드가 차단되지 않고 Non block 방식으로 동작되는 것을 알 수 있다.

또한, 호출자의 작업과 피호출자의 작업이 동시에 진행됨을 알 수 있다.

 

[참고] 호출자가 피호출자의 작업 완료 상태를 지속적으로 체크하는 방식을 polling이라고 한다.

isDone이라는 플래그를 통해 완료 여부를 체크하는 것, 이전 예제에서 while (callee.isAlive()) 코드를 통해 작업 완료를 체크하는 부분 모두 polling방식의 예이다.

 

비동기-Blocking

요청 작업의 완료 여부를 체크하지 않고 순차적으로 작업이 진행되지 않아도 되며, 호출함수는 차단되어 다른 작업을 할 수 없다.

 

사실 이 케이스는 실제로 사용되는 경우는 거의 없고 일반적으로 안티패턴으로 치부된다.

호출함수가 피호출함수의 작업 완료여부를 체크하지 않더라도 block되어 피호출 함수 작업 완료 시점에 제어권을 돌려 받기에 순차적으로 처리하는 동기방식처럼 처리가 된다.

 

비동기-Blocking이 안티패턴인 이유

  1. 복잡도 상승
    비동기의 경우 일반적으로 콜백, 이벤트와 같은 구현 패턴을 갖는다.
    Blocking 되어 동기적으로 처리된다면 콜백, 프로미스, 이벤트와 같은 구현 패턴은 복잡도만 상승시킬 뿐이다.
  2. 자원 비효율
    비동기 프로그래밍 구현을 위해 멀티 쓰레드 환경으로 제공하는데 Block이 된다면 싱글스레드를 사용하는것과 다르지 않다. 
    스레드를 생성하고 해제하는데 불필요한 자원 할당과 CPU의 컨테스트 스위칭, 대기시간과 같은 비효율을 늘릴 뿐이다.

=> 일반적으로 비동기-Blocking이 사용됬다면 잘못 구현했을 가능성이 높다.

 

비동기-NonBlocking

요청 작업의 완료 여부를 체크하지 않아 순차적으로 작업이 진행되지 않아도 되며, 호출함수가 차단되지 않아 자신의 작업을 할 수 있다.

 channel과 selector를 통한 I/O 처리에서 비동기-NonBlocking 예제를 보자.

 main 함수의 주석만 읽어도 이해하는데 무리는 없다.

public class AsyncNonBlocking {

   public static void main(String[] args) {

       try {

           // 서버 소켓 채널 생성

           ServerSocketChannel serverSocketChannel = getServerSocketChannel();

 

           // Selector 생성 및 channel에 등록

           Selector selector = Selector.open();

           serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

 

           while (true) {

               // 이벤트 수신(non blocking)

               selector.selectNow();

 

               // 이벤트 존재여부 체크

               Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

               while (iterator.hasNext()) {

                   SelectionKey key = iterator.next();

                   iterator.remove();

 

                   // 클라이언트의 연결 요청이 들어왔다는 이벤트 발생

                   if (key.isAcceptable()) acceptClient(key, selector);

                   // 클라이언트의 요청 데이터가 도착했다는 이벤트 발생

                   else if (key.isReadable()) readClientData(key);

 

                   // 메인 스레드 쉬지 않고 작업함

                   System.out.println("메인 스레드의 동작은 멈추지 않음");

               }

 

               

           }

       } catch (IOException e) {

       }

   }

 

   private static ServerSocketChannel getServerSocketChannel() throws IOException {

       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

       serverSocketChannel.bind(new InetSocketAddress("localhost", 8000));

       serverSocketChannel.configureBlocking(false); // Non-blocking 모드 설정

       return serverSocketChannel;

   }

 

   private static void acceptClient(SelectionKey key, Selector selector) throws IOException {

       ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();

       SocketChannel clientChannel = serverChannel.accept();

       clientChannel.configureBlocking(false); // Non-blocking 모드 설정

       clientChannel.register(selector, SelectionKey.OP_READ); // 읽기 이벤트 등록

       System.out.println("클라이언트 연결됨");

   }

 

   private static void readClientData(SelectionKey key) throws IOException {

       SocketChannel clientChannel = (SocketChannel) key.channel();

       ByteBuffer buffer = ByteBuffer.allocate(1024);

       int bytesRead = clientChannel.read(buffer);

       if (bytesRead == -1) {

           clientChannel.close();

           System.out.println("클라이언트 연결 종료됨");

           return;

       }

       buffer.flip();

       byte[] data = new byte[buffer.limit()];

       buffer.get(data);

       System.out.println("수신한 데이터: " + new String(data));

   }

}

위 코드에서 main 스레드의 역할은 클라언트 요청을 승인하고 클라이언트의 요청 데이터를 읽어들이는 이벤트를 수신하는 것이다.

실질적인 요청 승인은 accept, 데이터 읽기는 read를 호출함으로써 이뤄지는데 NonBlocking으로 설정하였다.

 

비동기-NonBlocking의 동작방식은 read메서드에서 더욱 잘 나타나므로

호출자인 main 스레드와 피호출 함수인 read 메서드에 집중하여 설명하겠다.

 

main 스레드는 무한 루프를 돌며 이벤트를 수신한다.

클라이언트의 요청 데이터가 도착하면 main은 읽기 가능 이벤트를 수신 받고 channel의 read메서드를 호출한다.

 

read 메서드는 NonBlocking으로 설정하여 데이터가 올때까지 대기(block)하고 있지 않는다.

데이터가 도착했다는 이벤트를 받고 read를 호출하기 때문에 대기하고 있을 필요가 없다.

 

터미널을 열어 프로그밍 서버에 연결하고 데이터를 전달해보자.

nc localhost 8000 < [파일 경로]

 프로그래밍 콘솔창에 아래와 같이 출력될 것이다.

 

콘솔창에 read를 통해 읽어온 데이터와 main에서 println을 통해 출력하는 문자열 대기없이 출력되는 것을 볼 수 있을 것이다.

 

비동기로 동작하는지 알아보기 위해 이번에 터미널을 두개 열고

한쪽 터미널에 아래와 같이 입력하여 프로그래밍 서버에 연결하고 데이터를 입력하자.(아직 엔터 금지)

nc localhost 8000
random data

 다른 터미널에는 아래와 같이 입력하자.(아직 엔터 금지)

nc localhost 8000 < [파일 경로]

그리고 파일 전송 명령어를 포함한 터미널에 먼저 엔터키를 눌러 실행하고

최대한 빠르게 데이터만 입력한 터미널에도 엔터키를 눌러보자.

 

프로그래밍 서버 콘솔 창에 아래와 같이 읽어온 파일 데이터 중간에 random data가 출력됨을 확인할 수 있다.

 

이는 먼저 요청온 용량이 큰 파일을 읽어들이는 작업의 완료 유무에 상관 없이

상대적으로 빨리 읽기 가능한 문자열만 포함된 요청 작업이 먼저 완료된 것이다.

비동기적으로 작업이 실행됨을 알 수 있다.

 

비동기 방식은 작업 완료여부를 체크하여 순차적으로 처리하지 않아도 되니 이벤트를 전달받아 이벤트가 있을 때에만 작업을 진행할 수 있다.

 

I/O 작업에서 비동기-NonBlocking으로 구현하면 큰 효율을 얻을 수 있다.

웹서버가 클라이언트의 요청을 읽을 때, 클라이언트의 요청 순서대로 응답을 제공하지 않아도 되기에 요청 용량이 적은 데이터를 빠르게 읽어내어 요청 처리 작업을 진행할 수 있다.

또한 Block되는 부분이 없기에 특정 클라이언트의 요청을 읽어내는 작업이 다른 클라이언트에 영향을 미치지 않는다.

(ex. 특정 클라이언트의 PC에서 데이터 전달 속도가 느려져도 다른 클라이언트 읽는데 영향 안미침)

또한 데이터를 read하는데 대기하고 있는 시간이 없기에 CPU가 아무 작업하지 않고 대기하는 시간을 줄요 컴퓨팅 자원을 효율적으로 사용할 수 있다.

 

 

++++ 부가 개념 ++++

스레드 환경에 따른 동기와 비동기

동기와 비동기는 스레드와 많이 혼동되기도 하니 스레드 환경에 따른 동기와 비동기를 살펴보자.

 

Synchronous (with single-thread)

thread-A |-----------A-----------||-----------B-----------||-------C-------|

 

Synchronous (with multi-thread)

thread-A |-----------A-----------|
thread-B                               |-----------B-----------|
thread-C                                                             |-------C-------|

 => 응답 순서, 최종 응답 시점 같음



Asynchronous (with single-thread)

thread-A |-----------A-----------|
                                      |-----------B-----------|
                                          |-------C-------|

 

Asynchronous (with multi-thread)

thread-A |-----------A-----------|
thread-B                         |-----------B-----------|
thread-C                             |-------C-------|

 

=> 응답 순서, 최종 응답 시점 같음

 

동기와 비동기 각각을 보면 모두 스레드 환경에 따른 응답 순서와 응답 시점에 차이는 없다.

 

스레드는 공간의 개념이다. 순서와 시점에는 영향을 미치지 않는다.

 

아마 비동기 작업을 별도의 스레드에 할당하여 처리하는 경우가 많아 혼동되는 것 같다.

 

대게 main 작업의 부가적인 작업을 비동기로 진행하는 경우가 많다.

 

예를 들어 사용자의 주문내역을 저장하고 사용자 휴대폰에 알림메시지를 전송하는 작업이 있다고 하자.

DB에 주문 내역이 저장되면 알림메시지를 발송하고 클라이언트 화면에 주문완료 여부도 제공 해야한다.

이때 비동기로 진행하면 두 작업이 동시에 진행되기에 동기 처리보다 빠르게 사용자에게 주문 완료 여부를 알릴 수 있다.

스레드까지 별도로 할당하게 되면 알림 메시지 작업 중 예외가 발생하더라도 주문 완료 여부를 클라이언트에 제공하는 작업에 영향을 미치지 않을 수 있다.

 

보통 이렇게 비동기 작업시 별도의 스레드로 할당하는 경우가 많다. 그래서 개념이 많이 혼동되지 않나 싶다.

 

동기, 비동기와 스레드는 완전히 다른 개념이다.

동기와 비동기는 순서, 시점의 개념이고 스레드는 공간이다. 두 개념의 교집합은 없다.

 

반응형