[Java] TCP 넌블로킹 채널, 비동기 채널

kiteB·2022년 5월 18일
0

Java2

목록 보기
36/36
post-thumbnail

[ TCP 넌블로킹 채널 ]

ServerSocketChannel, SocketChannel블로킹(blocking) 방식도 지원하지만 넌블로킹(non-blocking) 방식도 지원한다.

넌블로킹 방식의 특징

블로킹 방식은 언제 클라이언트가 연결 요청을 할지 모르기 때문에 accept()에서 블로킹된다. 그리고 언제 클라이언트가 데이터를 보낼지 모르므로 read() 메소드는 항상 데이터를 받을 준비를 하기 위해 블로킹된다. 그렇기 때문에 ServerSocketChannel과 연결된 SocketChannel당 하나의 스레드가 할당되어야 한다. 따라서 연결된 클라이언트가 많을수록 스레드의 수가 증가하고 서버에 심각한 성능 문제를 유발할 수도 있다. 이 문제를 해결하기 위해 지금까지는 스레드풀(ExecutorService)을 사용했었다.

자바는 블로킹 방식의 또 다른 해결책으로 넌블로킹 방식을 지원하고 있다. 넌블로킹 방식은 connect(), accept(), read(), write() 메소드에서 블로킹이 없다. 클라이언트의 연결 요청이 없으면 accept()는 즉시 null을 리턴한다. 그리고 클라이언트가 데이터를 보내지 않으면 read()는 즉시 0을 리턴하고, 매개값으로 전달한 ByteBuffer에는 어떤 데이터도 저장되지 않는다.

넌블로킹 방식에서 다음 코드는 클라이언트가 연결 요청을 하지 않으면 무한 루프를 계속 돈다.

while(true) {
	SocketChannel socketChannel = serverSocketChannel.accept();
    ...
}

accpet() 메소드가 블로킹되지 않고 바로 리턴되기 때문에 클라이언트가 연결 요청을 보내기 전까지 while 블록 내의 코드가 쉴새없이 실행되어 CPU가 과도하게 소비되는 문제점이 발생한다. 그래서 넌블로킹은 이벤트 리스너 역할을 하는 셀렉터(Selector)를 사용한다. 넌블로킹 채널에 Selector를 등록해 놓으면 클라이언트의 연결 요청이 들어오거나 데이터가 도착할 경우, 채널은 Selector에 통보한다. Selector통보한 채널들을 선택해서 작업 스레드가 accept() 또는 read() 메소드를 실행해서 즉시 작업을 처리하도록 한다.

Selector멀티 채널의 작업을 싱글 스레드에서 처리할 수 있도록 해주는 멀티플렉서(multiplexor) 역할을 한다.

  1. 채널은 Selector에 자신을 등록할 때 작업 유형을 키(SelectionKey)로 생성하고, Selector관심키셋(interest-set)에 저장시킨다.
  2. 클라이언트가 처리 요청을 하면
  3. Selector는 관심키셋에 등록된 키 중에서 작업 처리 준비가 된 키를 선택된 키셋(selected-set)에 별도로 저장한다.
  4. 그리고 작업 스레드가 선택된 키셋에 있는 키를 하나씩 꺼내어 키와 연관된 채널 작업을 처리하게 된다. 작업 스레드가 선택된 키셋에 있는 모든 키를 처리하게 되면 선택된 키셋은 비워지고, Selector는 다시 관심키셋에서 작업 처리 준비가 된 키들을 선택해서 선택된 키셋을 채운다.

넌블로킹에서 작업 스레드를 꼭 하나만 사용할 필요는 없다. 채널 작업 처리 시(④) 스레드풀을 사용할 수 있다. 작업 스레드가 블로킹되지 않기 때문에 적은 수의 스레드로 많은 양의 작업을 고속으로 처리할 수 있어 블로킹 방식보다는 서버의 성능이 향상될 수 있다.


셀렉터 생성과 등록

  • Selector는 정적 메소드인 open() 메소드를 호출하여 생성한다. open() 메소드는 IOException이 발생할 수 있기 때문에 예외 처리가 필요하다.
try {
	Selector selector = Selector.open();
} catch (IOException e) { ... }
  • Selector에 등록할 수 있는 채널은 SelectableChannel의 하위 채널만 가능한데, TCP 통신에 사용되는 ServerSocketChannel, SocketChannel과 UDP 통신에 사용되는 DatagramChannel은 모두 SelectableChannel의 하위 클래스이므로 Selector에 등록할 수 있다.
    • 이때 넌블로킹으로 설정되는 것만 가능하다.

  • ServerSocketChannel을 넌블로킹으로 설정하는 코드
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
  • SocketChannel을 넌블로킹으로 설정하는 코드
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
  • 각 채널은 register() 메소드를 이용해서 Selector에 등록하는데, 첫 번째 매개값은 Selector이고 두 번째 매개값은 채널의 작업 유형이다.
SelectionKey selectionKey = serverSocketChannel.register(Selector sel, int ops);
SelectionKey selectionKey = socketChannel.register(Selector sel, int ops);
  • 다음은 두 번째 매개값으로 사용할 수 있는 작업 유형별 SelectionKey의 상수들이다.
SelectionKey의 상수설명
OP_ACCEPTServerSocketChannel의 연결 수락 작업
OP_CONNECTSocketChannel의 서버 연결 작업
OP_READSocketChannel의 데이터 읽기 작업
OP_WRITESocketChannel의 데이터 쓰기 작업
  • register()는 채널과 작업 유형 정보를 담고 있는 SelectionKey를 생성하고 Selector의 관심키셋에 저장한 후 해당 SelectionKey를 리턴한다.

  • ServerSocketChannelSelector에 자신의 작업 유형을 등록하는 코드
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  • ServerSocketChannel은 클라이언트 연결 수락 작업을 하므로 작업 유형은 OP_ACCEPT로 지정한다. 다음은 SocketChannelSelector에 자신의 작업 유형을 등록하는 코드
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_CONNECT);
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_READ);
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_WRITE);
  • SocketChannel의 작업은 세 가지인데, 서버 연결 요청 작업은 OP_CONNECT, 읽기 작업은 OP_READ, 쓰기 작업은 OP_WRITE로 지정한다.
  • 동일한 SocketChannel로 두 가지 이상의 작업 유형을 등록할 수 없다.
    • 즉, **register()를 두 번 이상 호출할 수 없다.
    • 등록은 한 번만 하되, 작업 유형이 변경되면 이미 생성된 SelectionKey를 수정해야 한다.
  • register()가 리턴한 SelectionKey는 작업 유형 변경, 첨부 객체 저장, 채널 등록 취소 등을 할 때 사용된다.
  • 채널이 Selector를 등록하면 채널의 keyFor() 메소드로 SelectionKey를 언제든지 얻을 수 있기 때문에 SelectionKey를 별도로 관리할 필요는 없다.
SelectionKey key = sockeyChannel.keyFor(selector);

[ TCP 비동기 채널 ]

NIO는 TCP 블로킹, 넌블로킹 채널 이외에 TCP 비동기 채널로, AsynchronousServerSocketChannelAsynchronousSocketChannel을 제공한다. 각각 ServerSocketChannelSocketChannel에 대응된다.

비동기 채널의 특징

TCP 비동기 채널은 연결 요청(connect()), 연결 수락(accept()), 읽기(read()), 쓰기(write())를 호출하면 즉시 리턴된다. 이것은 넌블로킹 방식과 동일하다.

  • 차이점은 이 메소드들을 호출하면 스레드풀에게 작업 처리를 요청하고 이 메소드들은 즉시 리턴된다. 실질적인 작업 처리는 스레드풀의 작업 스레드가 담당한다.
  • 작업 스레드가 작업을 완료하게 되면 콜백(callback) 메소드가 자동 호출되기 때문에 작업 완료 후 실행해야 할 코드가 있다면 콜백 메소드에서 작성하면 된다.
  • 애플리케이션에서 read() 메소드를 호출하면 즉시 리턴되지만, 실질적으로 내부에서는 스레드풀의 작업 스레드가 read() 메소드를 실행한다.
  • 작업 스레드가 read() 메소드를 모두 실행하고 나면 콜백 메소드인 completed() 메소드가 자동 호출된다. 이때 completed() 메소드를 실행하는 스레드는 스레드풀의 작업 스레드이다.

비동기 채널 그룹

비동기 채널 그룹(AynchronousChannelGroup)은 같은 스레드풀을 공유하는 비동기 채널들의 묶음이라고 볼 수 있다. 하나의 스레드풀을 사용한다면 모든 비동기 채널은 같은 채널 그룹에 속해야 한다.

  • 비동기 채널을 생성할 때 채널 그룹을 지정하지 않으면 기본 비동기 채널 그룹이 생성된다.
  • 기본 비동기 채널 그룹은 내부적으로 다음과 같이 스레드풀을 생성한다.
new ThreadPoolExecutor(
	0, Integer.MAX_VALUE,
    Long.MAX_VALUE, TimeUnit.MILLISECONDS,
    new SynchronousQueue<Runnable>(),
    threadFactory);
  • 이론적으로 Integer.MAX_VALUE개만큼 스레드가 증가할 수 있도록 되어 있다.
  • 하지만 스레드풀은 대부분 최대 스레드 수를 지정해서 사용하므로 다음과 같이 AsynchronousChannelGroup을 직접 생성하고 사용하는 것이 일반적이다.
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withFixedThreadPool(
	최대스레드수,
    Executors.defaultThreadFactory()
);
  • 다음은 CPU 코어의 수만큼 스레드를 관리하는 스레드풀을 생성하고 이것을 이용하는 비동기 채널 그룹을 생성한다.
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withFixedThreadPool(
	Runtime.getRuntime().availableProcessors(),
    Executors.defaultThreadFactory()
);
  • 이렇게 생성된 비동기 채널 그룹은 비동기 채널을 생성할 때 매개값으로 사용된다.
  • 비동기 채널 그룹을 더 이상 사용하지 않고 종료할 경우에는 shutdown()shutdownNow() 메소드를 호출할 수 있다.
channelGroup.shutdown();
channelGroup.shutdownNow();
  • shutdown()은 비동기 채널 그룹을 종료하겠다는 의사만 전달할 뿐 즉시 비동기 채널 그룹을 종료하지 않는다. 비동기 채널 그룹에 포함된 모든 비동기 채널이 닫히면 비로소 비동기 채널 그룹이 종료된다.
  • shutdown() 메소드를 호출한 이후에 새로운 비동기 채널을 비동기 채널 그룹에 포함시키면 ShutdownChannelGroupException이 발생한다.
  • shutdownNow()는 강제적으로 비동기 채널 그룹에 호함된 모든 비동기 채널을 닫아버리고 비동기 채널 그룹을 종료한다.
    • 단, 완료 콜백을 실행하고 있는 스레드는 종료되거나, 인터럽트되지 않는다.

비동기 서버소켓 채널

AsynchronousServerSocketChannel은 두 가지 정적 메소드인 open()을 호출해서 얻을 수 있다.

  • 기본 비동기 채널 그룹에 포함되는 AsynchronousServerSocketChannel매개값 없는 open() 메소드를 호출하여 얻을 수 있다.
AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
  • 별도로 비동기 채널 그룹을 생성하고 여기에 포함되는 AsynchronousServerSocketChannel을 얻고 싶다면 다음과 같이 비동기 채널 그룹을 매개값으로 갖는 open() 메소드를 호출하면 된다.
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withFixedPool(
	Runtime.getRuntime().availableProcessors(),
    Executors.defaultThreadFactory()
);
AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open(channelGroup);
  • AsynchronousServerSocketChannel을 생성하고 나서는 포트 바인딩을 위해 다음과 같이 bind() 메소드를 호출해야 한다.
asynchronousServerSocketChannel.bind(new InetSocketAddress(5001));
  • AsynchronousServerSocketChannel더 이상 사용하지 않을 경우에는 close() 메소드를 호출해서 서버가 사용한 포트를 언바인딩해준다.
asynchronousServerSocketChannel.close();
  • AsynchronousServerSocketChannel은 연결 수락 작업을 스레드풀을 이용해서 비동기로 처리한다.
  • accept() 메소드를 호출하는 코드는 다음과 같다.
accept(A attachment, CompletionHandler<AsynchronousSocketChannel, A> handler);
  • 첫 번째 매개값은 콜백 메소드의 매개값으로 제공할 첨부 객체인데, 연결 수락 작업에는 별도의 첨부 객체가 필요하지 않기 때문에 null을 지정한다.
  • 두 번째 매개값은 콜백 메소드를 가지고 있는 CompletionHandler<AsynchronousSocketChannel, A> 구현 객체이다.
    • A는 첨부 객체 타입인데, 연결 수락 작업에는 별도의 첨부 객체가 필요하지 않기 때문에 Void로 지정한다.

  • 다음은 accept() 메소드를 호출하는 기본 뼈대이다.
asynchronousServerSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
	@Override
    public void completed(AsynchronousSocketChannel asynchronousSocketChannel, Void attachment) {
    	//연결 수락 후 실행할 코드
        asynchronousServerSocketChannel.accept(null, this);
    }
    @Override
    public void failed(Throwable exc, Void attachment) {
    	//연결 수락 실패 시 실행할 코드
    }
});

completed() 메소드는 연결 수락이 완료되었을 때 스레드풀의 스레드가 호출한다.

  • 첫 번째 매개값은 연결 수락 후 리턴된 AsynchronousSocketChannel이 대입되고,
  • 두 번째 매개값은 첨부 객체인데 accept()의 첫 번째 매개값이 null이므로 null이 대입된다.

만약 스레드풀의 스레드가 연결 수락에 문제가 생겨 예외를 발생시키면 failed()가 호출된다.

  • failed()의 첫 번째 매개값은 예외 객체이고
  • 두 번재 매개값은 첨부 객체인데 accept()의 첫 번재 매개값이 null이므로 null이 대입된다.

accept()를 반복해서 호출하는 무한 루프가 없는 대신 completed() 메소드 끝에 accept()를 재호출해서 반복적으로 클라이언트의 연결 수락 작업을 수행한다.


[ 참고자료 ]

이것이 자바다 책

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글