Multi Flex Server

소켓은 네트워크 상에서 서버와 클라이언트가 특정 포트를 통해 양방향 통신이 가능하도록 만들어주는 추상화된 리소스다. 하나의 소켓은 하나의 연결을 담당하기 때문에 다중 접속을 지원하기 위해선 연결 요청이 올 때 소켓을 새로운 프로세스 또는 스레드에 할당하여 데이터 송수신을 가능하게 하였다. 이렇게 소켓을 프로세스에 할당하는 방식을 멀티 프로세스 기반 서버, 스레드에 할당하는 방식을 멀티 스레드 기반 서버라고 한다. 하지만 이 두 방식은 컨텍스트 스위칭Context Switching의 오버헤드가 존재한다.

multiflex

컨텍스트 스위칭은 각 컨텍스트에서 처리되는 복잡도가 적을 수록 비율적으로 더 많은 오버헤드가 발생한다. 이 오버헤드를 없애기 위해선 하나의 프로세스 혹은 스레드에서 여러 개의 소켓을 핸들링할 수 있어야 한다. 이를 멀티 플렉싱 기반 서버라고 하며, 리눅스 커널에선 이를 지원하는 select, poll 그리고 epoll 과 같은 시스템 콜을 제공하고, 자바에서도 java.nio 패키지를 통해 하나의 스레드에서 여러 채널을 모니터링할 수 있는 API가 1.4부터 제공하고 있다.

멀티 플렉싱

멀티 플렉싱은 논블록킹Non-blocking I/O를 활용한 모델로 기존 블록킹Blocking I/O와의 동작의 차이점을 갖는다. 기존 블록킹 I/O의 경우 데이터가 준비 될 때까지 블록된 상태로 대기하며 파일을 다 읽은 뒤에 읽은 데이터 그램과 컨트롤를 함께 반환한다.

multiflex (1)

이에 반해 논블록킹의 경우 요청과 함께 컨트롤을 반환하며, 파일을 읽고 데이터 그램을 생성할 수 있을 때 이벤트를 보내기 때문에, 파일을 읽을 동안 반환된 컨트롤을 통해 다른 작업을 수행할 수 있다.

multiflex (2)

Java NIO

Java NIO에선 이런 논블록처리를 지원한다. 이를 이해하기 위해선 새롭게 추가된 몇가지에 대한 이해가 필요하다.

Channel

기존 블록팅 I/O에선 스트림Stream을 사용하여 파일을 읽고 쓰는 것이 가능했지만, NIO에선 채널Channel을 사용한다. 채널은 스트림과 유사하지만 몇가지 차이점이 있다.

  • 채널을 통해서는 읽고 쓸 수 있지만, 스트림은 일반적으로 단방향(읽기 혹은 쓰기)으로만 가능하다.
  • 채널은 비동기적으로 읽고 쓸 수 있다.
  • 채널은 항상 버퍼에서 부터 읽거나 쓴다.

네트워크 통신을 위해 java.nio에서 구현되어 있는 클래스는 다음과 같다.

  • DatagramChannel : UDP를 이용해 데이터를 읽고 쓴다.
  • SocketChannel : TCP를 이용해 데이터를 읽고 쓴다.
  • ServerSocketChannel : 들어오는 TCP 연결을 수신listening할 수 있다. 들어오는 연결마다 SocketChannel이 만들어진다.

Buffer

버퍼는 NIO에서 채널과 상호작용할 때 사용된다. 데이터는 버퍼를 통해 채널로 읽혀지거나 쓰여진다.

버퍼에 데이터를 쓸 때 버퍼는 쓰여진 데이터의 양을 기록한다. 만약 데이터를 읽어야한다면 flip() 메서드를 호출해서 버퍼를 쓰기 모드에서 읽기 모드로 전환해야 한다. 읽기 모드에서 버퍼를 사용하면 버퍼에 쓰여진 모든 데이터를 읽을 수 있습니다. 데이터를 읽은 후에는 버퍼를 지우고 다시 쓸 준비를 해야한다. clear() 혹은 compact()를 호출함으로써 전체 버퍼를 지울 수 있다. (clear() 메서드는 버퍼 전체를 지우고, compact() 메서드는 이미 읽은 데이터만 지운다.)

Selector

셀렉터Selector 는 어느 채널이 IO event 를 가지고 있는지를 알려준다. Selector.select() 는 I/O 이벤트가 발생한 채널을 반환한다. 만약 반환할 채널이 없다면 블록상태로 대기한다.

하나의 스레드에서 여러 채널을 관리할 수 있기 때문에 셀렉터를 사용하면 단일 스레드에서 여러 네트워크 연결을 관리할 수 있다.

SelectionKey

셀렉션 키SelectionKey는 셀렉터가 select() 메서드를 통해 이벤트가 발생한 채널을 가져올 때 반환된다. 이 셀렉션 키가 가지고 있는 몇가지 프로퍼티들이 존재하는데, 구독하는 이벤트의 집합, 처리 가능한 이벤트의 집합, 연관된 태널과 셀렉터, 부가적으로 첨부한 객체 등 셀렉터와 채널간의 관계와 커뮤니케이션을 위한 정보들을 포함한다.

멀티 플렉싱 기반 서버와 리액터

논블록킹 I/O 그리고 java.nio에서 지원하는 셀렉터를 사용하면 단일 스레드로 여러 네트워크 연결을 가능하게 할 수 있다.

multiflex (3)

스프링에 새롭게 추가된 NIO 지원 모듈인 웹플럭스에서 지원하는 웹서버는 여러가지지만 웹서버를 거쳐 로직을 담당하는 레이어에선 모두 리액터를 기반으로 구현이 되어있다. 리액터는 NIO와는 별개로 동작하는 ‘비동기’ 스트림이지만 멀티 플렉싱과 밀접한 관련이 있는 하나의 특징이 있는데, 바로 유연한 스레드 전환이 가능하다는 것이다.

셀렉터를 사용하여 구현한 웹서버라면, 클라이언트와 연결된 SocketChannel에서 처리할 이벤트가 있을 때, 적절한 핸들러를 찾아 request를 넘겨주고 reponse를 전달받는 과정을 거쳐야한다. 별도의 처리를 하지 않는다면 핸들러로 request를 넘겨주는 스레드는 셀렉터가 이벤트가 발생한 채널을 반환받기 위해 돌고 있는 루프Loop일 것이다.

만약 이 상황에서 request를 처리하는 동안 블록킹 상태가 된다면 이벤트 루프의 블록상태가 해제될 때까지 다른 채널에서 이벤트들이 핸들링되지 못하게 될 것이다. 만약 셀렉터가 request를 핸들러에 전달하고 핸들러가 다른 스레드에서 이를 처리한다면 셀렉터는 계속 다른 이벤트에 응답할 수 있다.

리액터는 비동기를 강제하지 않지만, subscribeOn()publishOn()과 같은 메서드를 사용하여 스레드를 손쉽게 전환할 수 있다. 때문에 리액터를 사용하면 별도의 스레드 관련 구현을 하지 않고도 셀렉터가 다른 이벤트에 응답을 할 수 있도록 할 수 있다.

또한 리액터가 갖는 또 다른 특징 중 하나인 구독시점부터 리액터 이벤트가 처리된다는 장점도 존재한다. 리액터를 사용하여 로직을 구현하는 과정은 기존의 프로시져 형태보다 더욱 더 데이터가 처리되는 순서에 집중하기 쉽다. 각각의 과정은 스트림의 변환/전환 메서드를 통해 이뤄지기 때문에 Flux를 사용해 여러 데이터를 처리하는 경우에도 각각의 데이터의 처리에 집중하여 로직을 구현할 수 있다.

같이보면 좋은 글 : http://jeewanthad.blogspot.com/2013/02/reactor-pattern-explained-part-1.html

​ http://jeewanthad.blogspot.com/2013/02/reactor-pattern-explained-part-2.html

​ http://jeewanthad.blogspot.com/2013/03/reacter-pattern-explained-part-3.html