Reactor 패턴

Reactor Pattern

Reactor Pattern 을 적용하기 전, 이전에 살펴본 nio 채팅 코드는 아래와 같았습니다.

// ...
 
  if(key.isAcceptable()){ // ACCEPT 이벤트일 경우
    // (1)
    // accept 를 통해 ClientSocket 획득
    var clientSocket = ((ServerSocketChannel) key.channel()).accept();
    // clientSocket 을 non-blocking 으로 설정
    clientSocket.configureBlocking(false);
    // clientSocket 을 selector 에 등록
    clientSocket.register(selector, SelectionKey.OP_READ);
  }
  if(key.isReadable()){ // READ 이벤트 일 때
    // clientSocket 을 얻어옴
    var clientSocket = (SocketChannel) key.channel();
 
    var requestBuffer = ByteBuffer.allocate(1024);
    // (2)
    clientSocket.read(requestBuffer); // clientSocket 으로부터 데이터 Read
    requestBuffer.flip();
 
    var received = new String(requestBuffer.array()).trim();
    log.info("client received = {}", received);
 
    var send = "답장보냅니다.";
    var responseBuffer = ByteBuffer.wrap(send.getBytes());
    clientSocket.write(responseBuffer);
    responseBuffer.clear();
    clientSocket.close();
  }
 
// ...

(1), (2) 로 표시한 부분에서는 accept, read 를 함수 호출로 처리하고 있습니다. 이렇게 함수를 그대로 작성해서 하는 것 보다는 조금은 확장성이 가능하도록 하는 코드를 작성해야 할 것 같습니다. 이번 문서에서는 이런 Plain 한 통신 로직을 Reactor Pattern 으로 바꾸면 어떻게 되는지를 확인해봅니다. Reactor 패턴은 이벤트를 감지하고 분류하는 Reactor 와 이벤트를 처리하는 Handler 로 이뤄져 있습니다.

Reactor Pattern 이란?

Reactor 패턴은 동시에 들어오는 요청들을 처리하기 위해 만들어진 이벤트 핸들링 패턴입니다. Reactor 패턴에는 Reactor, Dispatcher, Handler 라고 하는 대표적인 3 요소가 있습니다.

Reactor

  • 별도의 스레드에서 실행됩니다.
  • 여러 요청의 Write, Accept, Read 이벤트를 한 곳에 등록 후 관찰하는 역할을 수행합니다.
  • Connection 수립 후 TCP Handler 를 SelectionKey 에 대한 attachment 로 등록합니다.
  • Selector로 select() 를 통해 이벤트가 발생했음을 감지했을 때 이벤트들 각각을 처리하는 것은 Dispatcher 가 처리하게끔 유도합니다.

Dispatcher

  • Reactor 로부터 전달받은 SelectionKey 내에 등록된 attachment 를 읽어들이는데, 이때 보통 attachment 는 EventHandler 입니다.
  • Dispatcher 는 Handler가 Acceptor 인지 HttpEventHandler 인지는 모르고, 단순히 SelectionKey 에 attachment 로 등록된 EventHandler 타입의 handle() 을 호출합니다.

Handler

  • Handler 의 종류는 아래와 같이 구분했습니다.
    • Socket Accept 를 위한 용도의 Handler인 Acceptor
    • TCP 요청을 읽은 후 응답(Response)하기 위한 TcpEventHandler


Reactor

  • 이번 예제에서는 Selector 를 Reactor 라는 이름으로 사용합니다.

Dispatch

  • 이번예제에서는 Dispatch 는 객체로 선언하지 않고 Reactor 내에 dispatch() 라는 메서드로 구현해두었습니다.

이번 예제에서 구현하려는 Acceptor, TcpEventHandler 의 기능을 요약해보면 아래와 같습니다.

Acceptor

  • Acceptor 는 Application 전역적으로 1개의 객체만 존재합니다.

  • Acceptor 가 handle() 을 수행할 때 ServerSocketChannel 이 accept() 를 성공적으로 수립했을 때 생성된 클라이언트의 SocketChannel 을 획득합니다.

  • 이후에 TcpEventhHandler 객체를 생성하고, TcpEventHandler 객체 생성시에, 객체 내에 클라이언트의 SocketChannel 을 전달해주어 클라이언트 소켓을 바인딩해줍니다.

  • 신규 접속 생성시마다 해당하는 커넥션 하나당 하나의 HttpEventHandler 객체를 새로 생성합니다.

TcpEventHandler

  • TcpEventHandler 는 전달받은 클라이언트 SocketChannel 을 이용해서 아래의 작업들을 수행합니다.
  • 객체 생성 시에 전달받은 SocketChannel 을 Selector 에 등록해주고, 자기자신을 attachment 로 등록해줍니다.
  • 따라서 Accept 이벤트가 아닌 Readable 이벤트일 경우 TcpEventHandler 를 거치게 됩니다.
  • 요청으로 전달된 request 를 읽어들입니다.
  • 요청 처리, 응답(send - clientSocket.write()) 을 수행합니다

Acceptor, TcpEventHandler 는 Dispatch 입장에서는 EventHandler 라는 추상타입의 handle() 메서드를 통해 처리합니다. Dispatcher 입장에서는 Acceptor, TcpEventHandler 타입의 구체적인 동작을 알 필요 없이 EventHandler 의 handle() 메서드만을 호출하는 역할만을 수행합니다.


예제는 별도의 실습 문서 들에서 따로 정리하며 예제는 아래의 두 종류의 예제를 예제로 수행합니다.

  • Reactor 패턴 기반의 소켓 프로그램
  • Reactor 패턴 기반의 Http 서버 프로그램