ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 네트워크 프로그램, 블록과 논블록
    프로그래밍/자바 2020. 2. 22. 00:22

      멀티스레드 프로그래밍에서 메서드나 객체에 대해 동기화를 할 경우 다른 누군가 그 메서드를 참조하고 있다면 “해당 메서드는 블록 된다”, 라는 표현을 사용한다. ‘가로막다’의 의미로 사용되는 것인데, 동시성 제어(변수의 무결성, 기타 등등)를 위하여 필요한 것이다.

     

      네트워크 프로그래밍으로 넘어와 이야기를 해 보겠다. 네트워크 프로그래밍에선 입출력 작업이 필요하다. 입출력 작업엔 블록 방식과 논블록 방식이 존재한다. IO와 NIO이다.

      자바에서 사용하는 블록방식의 입력(INPUT) 부분은 다음과 같다.

    while( (read = is.read(packet)) != -1){
        System.out.println("receive message, from server : " + new String(packet, 0, read));
    }

    java api를 참조하면 다음과 같은 설명이 있다.

     

    Reads the next byte of data from the input stream. The value byte is returned as an int in the range 0 to 255. If no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks until input data is available, the end of the stream is detected, or an exception is thrown. A subclass must provide an implementation of this method.

     

      InputStream.read 메소드는 읽을 데이터가 있던가, 스트림의 끝을 발견하던가, 예외가 전파되기까지 블록 된다고 한다. 여기서 블록이 된다는 건 해당 스레드가 다음으로 진행되지 않고 InputStream.read 메서드의 반환 값을 기다리고 있다는 뜻이다.

     

      네트워크프로그래밍에서 A노드와 B노드 사이에 동기 작업이 이루어져야 할 경우 쉽게 구현할 수 있는 방법이 바로 블록 IO를 이용하는 것이다. A노드는 B노드에게 요청을 보내고 결괏값을 기다린다. B노드가 응답을 하지 않았을 경우 A노드는 InputStream.read 메서드에서 블록이 걸려있을 것이다. 응답이 오면 블록이 해제되니 이후 처리할 로직을 넣으면 된다.

     

      여기서 오해할 수 있는 부분이 ‘블록IO로 비동기 처리를 하지 못하느냐’ 일 것이다. 결론부터 말하자면 블록 IO로도 비동기 처리를 할 수 있다. 그러나 설계가 조금 더 복잡해질 것이다. 송수신 부분과 처리 로직을 분리해야 할 것이고, 이 둘의 연결고리(인터페이스)를 어떻게 해야 할지 고민해봐야 할 것이다.

      블록IO는 단점이 있다. 자원의 낭비이다. 블록 IO의 해당 스레드는 언제나 블록 상태이다. A노드(클라이언트)와 B노드(서버) 1:1 상황에서 필요 스레드 수는 하나일 것이다. 하나 C노드(클라이언트)가 생기고 B노드(서버)에 요청하는 순간 B노드 수신 부분을 멀티스레드로 구현하지 않았다면 C노드(클라이언트)는 자신과 연관도 없는 A노드(클라이언트)의 영향을 받을 것이다. 그럼 여러 노드의 요청을 받아야 하는 B노드는 요청 개수 혹은 그 이상으로 스레드를 생성해야 한다. 스레드 생성 및 콘텍스트 스위칭 비용을 생각한다면 배보다 배꼽이 커질 수 있다. 블록 IO로 구현된 노드는 결국 연결 요청 수의 제한을 받게 될 것이다. 송수신 부분과 처리 로직에 대한 설계를 하지 않았다면 블록 IO로는 대용량 서비스가 힘들다.

    블록 IO의 자원 문제를 해결하고자 하는 것이 논 블록 IO이다. 어떤 점이 다른지 다음 소스코드를 먼저 보겠다.

     

    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while(true){
        selector.select();
        iter = selector.selectedKeys().iterator();
     
        while(iter.hasNext()){
            key = iter.next();
            iter.remove();
            if(!key.isValid()){
                continue;
            }
                     
            if(key.isAcceptable()){
                operation.accept(key, selector, block);
            }else if(key.isReadable()){
                operation.read(key);
            }else if(key.isWritable()){
                operation.write(key);
            }
        }
    }

    Selector에 대한 java api를 참조하면 다음과 같은 설명이 있다.

     

    Selects a set of keys whose corresponding channels are ready for I/O operations.
    This method performs a blocking selection operation. It returns only after at least one channel is selected, this selector's wakeup method is invoked, or the current thread is interrupted, whichever comes first.


      논블록IO라고 했는데 Selector.select() 메서드에서 블록이 된다고 한다. Selector.select(long timeout) 메서드를 사용하면 지정된 시간까지만 블록이 될 수 있다. 하지만 이 내용은 중요하지 않다. java api의 내용을 보면 Selector.select() 메서드는 준비된 IO가 존재할 경우 블록이 해제된다고 한다.

     

      예제 소스 50번째 줄에서 블록이 걸려있다가 준비된 IO가 발생할 경우 블록이 풀리고 selectedKeys() 메소드를 통해 선택된 키값들(Set)을 반환해 온다. 52번 줄에선 이 키값들을 돌려 가면서 해당 키값이 acceptable, readable, writable 중 어떤 것인지 판단하여 해당 채널을 불러와 그에 맞는 읽기나 쓰기 작업을 한다.
    이는 셀렉터를 담당하는 스레드가 하나만 있다면 io기반(채널별로 스레드를 증가)으로 구현했던 것과는 달리 자원 소모가 크게 늘어나지 않는다.

     

      네트워크 프로그래밍에서의 io와 nio에 대해 설명을 간략하게 하였다. nio가 io보다 장점이 많아 보이긴 하지만 더 좋다고는 말할 수 없을 것 같다. 각각의 특징을 인지하고 요구사항을 만족시킬 수 있는 좋은 설계가 필요할 것이다.

    댓글 0

Designed by Tistory.