IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

Merlin으로 자바 플랫폼에 nonblocking 입출력 가능
새롭게 추가된 기능들

Aruna Kalagnanam, 소프트웨어 엔지니어, IBM
Balu G, 소프트웨어 엔지니어, IBM
2002년 3월

자바 기술 플랫폼에서는 오랫동안 nonblocking 입출력 메카니즘이 지연되어 왔다. 다행히도 Merlin (JDK 1.4)은 거의 모든 상황에 적용되는 마법 지팡이를 가지고 있고, 블록화된 입출력을 푸는 것이 이 마법사의 전공이다. 소프트웨어 엔지니어인 Aruna Kalagnanam와 Balu G는 Merlin의 새로운 입출력 패키지인 java.nio (NIO)의 nonblocking 기능을 소개하고, NIO가 할 수 있는 것을 보여 주기 위해 소켓 프로그래밍의 예를 사용한다.

수많은 클라이언트의 요청을 적절한 시간 내에 처리하는 서버의 능력은 입출력 스트림을 얼마나 효과적으로 사용하느냐에 달려 있다. 동시에 수백대의 클라이언트에게 서비스를 제공하는 서버는 입출력 서비스를 동시에 사용할 수 있어야 한다. JDK 1.4 (Merlin) 이전까지 자바 플랫폼은 nonblocking 입출력 호출을 지원하지 않았다. 자바 언어로 작성된 서버는 클라이언트에 대해 거의 1대 1 비율의 스레드를 가지기 때문에 엄청난 스레드 과부하를 가져 오기 쉽고, 이는 성능상의 문제와 확장성 결여라는 결과를 낳는다.

이 문제를 해결하기 위해 새로운 클래스들이 최신 출시판의 자바 플랫폼에 도입되었다. Merlin의 java.nio 패키지에는 스레드 과부하를 해결하기 위한 방법들이 가득 들어 있는데, 가장 중요한 것이 SelectableChannelSelector 클래스이다. channel은 클리어언트와 서버간의 통신수단을 나타낸다. selector는 윈도우 메시지 루프와 유사한데 selector는 다른 클라이언트들로 부터 다양한 이벤트를 캡쳐하여 각각의 이벤트 처리자에게 보낸다. 이 글에서 우리는 이 두 클래스가 함께 작동하여 자바 플랫폼에 대한 nonblocking 입출력 메커니즘을 생성하는 방법을 보여주겠다.

Merlin 이전의 입출력 프로그래밍

Merlin 이전의 간단한 서버 소켓 프로그램을 살펴보는 것으로 시작해보자. ServerSocket 클래스의 중요한 기능은 다음과 같다.:

  • 연결 접수
  • 클라이언트의 요청 읽기
  • 요청에 대한 서비스 제공

코드 일부를 사용하여 이 단계들을 각각 살펴보자. 우선 우리는 새로운 ServerSocket 프로그램을 작성한다.:


ServerSocket s = new ServerSocket();

그 다음, 들어오는 호출을 접수하고자 한다. 여기에서는 accept() 호출이 이를 수행하지만, 여러분이 주의해야 할 작은 함정이 있다.:


Socket conn = s.accept( );

accept() 호출은 서버 소켓이 클라이언트의 연결 요청을 받아들일 때까지 블록화된다. 일단 연결이 이루어지면 서버가 LineNumberReader를 사용해 클라이언트 요청을 읽는다. LineNumberReader는 버퍼가 가득찰 때까지 데이터를 큰 단위로 읽어 나가기 때문에 호출은 읽기 상태에서 블록화된다. 다음 예제 코드는 실행중인 LineNumberReader를 보여준다. (블록과 모든 것)


InputStream in = conn.getInputStream();
InputStreamReader rdr = new InputStreamReader(in);
LineNumberReader lnr = new LineNumberReader(rdr);
Request req = new Request();
while (!req.isComplete() )
{
   String s = lnr.readLine();
   req.addLine(s);
}

InputStream.read()는 데이터를 읽는 또 다른 방법이다. 불행히도 read 메소드 역시 write 메소드처럼 데이터가 사용될 때까지 블록화된다.

그림 1은 서버의 일반적인 작업을 보여준다. 굵은 선이 블록화 작업을 나타낸다.

그림 1. 작동중인 일반 서버
A blocking 1/O diagram

JDK 1.4 이전에는 스레드를 자유롭게 사용하는 것이 블록화를 피하는 가장 일반적인 방식이었다. 그러나 이 솔루션은 스레드 과부하라는 문제를 일으켰는데, 이는 성능과 확장성 양쪽에 영향을 미쳤다. 그러나 Merlin과 java.nio 패키지가 나옴에 따라 모든 것이 변화되었다.

이어지는 섹션들에서는 java.nio의 기초 사항들을 살펴보고, 위에서 설명한 서버 소켓 예제를 수정하는데 우리가 배운 것 중 일부를 적용해 보겠다.

Reactor 패턴

NIO 설계 뒤에 있는 주된 힘은 Reactor 패턴이다. 분산 시스템에서 서버 애플리케이션은 서비스 요청을 보내는 여러 클라이언트를 처리해야 한다. 그러나 특정 서비스를 호출하기 전에 서버 애플리케이션은 들어 오는 각 요청을 나누어 상응하는 서비스 제공자에게 보내야 한다. Reactor 패턴은 바로 이 기능을 수행한다. Reactor 패턴은 이벤트 중심 애플리케이션이 하나 혹은 여러 클라이언트로부터 한 애플리케이션으로 동시에 전달되는 서비스 요청들을 나누어 전송하도록 한다.

Reactor 패턴의 핵심 기능
  • 이벤트 나누기
  • 이벤트를 상응하는 이벤트 처리자에게 보내기

Reactor 패턴은 이런 측면에서 Observer 패턴과 밀접하게 관련되어 있다: 하나의 주제가 변경되면 이에 관련된 모든 요소들에게 이 사실이 알려진다. 그러나 Observer 패턴은 한 곳의 이벤트와 관련되어 있는 반면 Reactor 패턴은 여러 곳의 이벤트와 연관되어 있다.

Reactor 패턴에 대한 상세 사항은 참고자료를 참조한다.

Channels과 Selectors

NIO의 nonblocking 입출력 메커니즘은 selectorschannels를 근간으로 구축되었다. Channel 클래스는 서버와 클라이언트간의 통신 메커니즘을 나타낸다. Reactor 패턴에서 보면 Selector 클래스는 Channels 의 다중화기 (multiplexor)에 해당한다. Selector 클래스는 들어오는 클라이언트 요청을 나누어 각 요청에 해당하는 각각의 처리자에게 이들을 보낸다.

Channel 클래스와 Selector 클래스의 각각의 기능과 이들이 협력하여 nonblocking 입출력을 만들어내는 방법을 자세히 살펴보자.

채널의 역할
채널은 하드웨어 장비, 파일 ,네트워크 소켓, 혹은 프로그램 컴포넌트와 같이 읽기나 쓰기등 한 개 이상의 뚜렷한 입출력 작업을 수행할 수 있는 엔터티에 대한 개방된 연결을 나타낸다. NIO 채널은 비동기적으로 닫히고 중단(interrupt)될 수 있다. 따라서 한 스레드가 한 채널에서 하나의 입출력 작업으로 블록화하면 다른 스레드가 그 채널을 닫을 수도 있다. 비슷하게 한 스레드가 한 채널의 입출력 작업으로 블록화되면 다른 스레드가 블룩화된 스레드를 중단시킬 수 있다.

그림 2. java.nio.channels의 클래스 계층
A class hierarchy diagram for the java.nio package

그림 2에 나타난 것과 같이, java.nio.channels 패키지에는 많은 채널 인터페이스가 있다. 우리는 주로 java.nio.channels.SocketChanneljava.nio.channels.ServerSocketChannel 인터페이스에 관심이 있다. 이 채널들은 각각 java.net.Socketjava.net.ServerSocket을 대체하는 것으로 사용될 수 있다. 우리는 nonblocking 모드에서의 채널 사용에 초점을 맞추고 있지만, 채널은 blocking과 nonblocking 모드 양쪽에서 사용될 수 있다.

nonblocking 채널 생성하기
기본적인 nonblocking 방식의 소켓 읽기 쓰기 작업을 구현하기 위해 우리는 두 개의 새로운 클래스를 다루어야 한다. java.net 패키지에서 제공하는 InetSocketAddress 클래스와 java.nio 채널 패키지에서 제공하는 SocketChannel 클래스가 그것이다. 전자는 어디에 접속해야 하는지를 지정하고, 후자는 실제 읽기 및 쓰기 작업을 수행한다.

이 섹션에 나와 있는 예제 코드는 기초적인 서버 소켓 프로그램을 개발하기 위해 변경된 nonbloking 방식을 보여준다. 두 개의 새로운 클래스가 추가된 것에서부터 시작하여 이 샘플 코드들과 첫번째 예제에서 사용된 샘플 코드간의 차이에 주목하기 바란다.

 


String host = ......;
   InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
	
SocketChannel channel = SocketChannel.open();
   channel.connect(socketAddress);

Buffer의 역할 The role of the buffer
Buffer 는 특정한 원시 데이터 유형으로 된 데이터를 가지고 있는 추상 클래스이다. Buffer는 기본적으로 고정된 크기의 배열로 된 Wrapper이고 자신의 내용을 접근 가능하도록 만드는 getter/setter 메소드를 가지고 있다. Buffer 클래스는 다음과 같은 많은 하위 클래스를 가진다.

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

ByteBuffer는 다른 유형과의 읽기 및 쓰기를 지원하는 유일한 클래스이다.다른 클래스들은 특정 유형에 한정되어 있기 때문이다. 일단 연결되면 ByteBuffer 객체를 이용해 채널에서 데이터를 읽거나 채널에 데이터를 작성할 수 있다. ByteBuffer에 대한 상세 정보는 참고자료를 참조한다.

채널을 nonblocking으로 만들기 위해 우리는 다음과 같이 채널에 configureBlockingMethod(false)를 호출한다.


channel.configureBlockingMethod(false);

blocking 모드에서 한 스레드는 작업이 완료될 때까지 읽기나 쓰기로 블록화된다. 읽기 과정 동안 데이터가 소켓에 완전히 도착하지 않으면 스레드는 모든 데이터를 이용할 수 있을 때까지 읽기 작업에 블록화된다.

nonblocking 모드에서 스레드는 데이터 양이 얼마가 되든 읽어나갈 것이고 결국 다른 작업을 수행하기 위해서 복귀할 것이다. 만일 configureBlockingMethod()가 참으로 전달되면, 채널은 Socket에 대해 blocking 모드에서의 읽기 쓰기 작업과 정확히 동일한 작업을 수행하게 될 것이다. 위에서 언급했듯이 하나의 주요한 차이는 이러한 blocking모드에서의 읽기 쓰기들은 다른 스레드에 의해 중단될 수도 있다는 것이다.

Channel 만으로는 nonblocking 입출력 구현을 만드는 데 충분하지 않다. Channel 클래스는 nonblocking 입출력을 위해 Selector 클래스와 협력하여 작업해야 한다.

Selector의 역할

Selector 클래스는 Reactor 패턴 시나리오에서의 Reactor 역할을 수행한다. Selector는 이벤트를 여러 SelectableChannels다중화한다. 클라이언트에서 이벤트가 도착하면 Selector는 이들을 나누어 상응하는 Channels에 이벤트를 보낸다.

Selector를 생성하는 가장 간단한 방법은 아래와 같이 open() 메소드를 사용하는 것이다.:


Selector selector = Selector.open();

Channel이 Selector를 만나다

클라이언트 요청을 서비스해야 하는 각 Channel은 먼저 연결을 구성해야 한다. 아래 코드는 Server라는 이름의 ServerSocketChannel을 만들어 이를 로컬 포트에 연결시킨다:


ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetAddress ia = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(ia, port );
serverChannel.socket().bind(isa);

클라이언트 요청을 서비스해야 하는 각 Channel은 다음에 자신을 Selector에 등록해야 한다. Channel은 자신이 처리할 이벤트에 따라 등록되어야 한다. 예를 들어, 접속을 접수해야 하는 Channel은 다음과 같이 등록되어야 한다:


SelectionKey acceptKey = 
    channel.register( selector,SelectionKey.OP_ACCEPT);

Channel이 Selector에 등록된 것은 SelectionKey 객체로 표시된다. 다음 세 조건 중 하나가 충족될 때까지 Key는 유효하다:

  • Channel이 종료되었다
  • Selector가 종료되었다
  • Key 자체가 자신의 cancel() 메소드를 호출하여 취소되었다. .

Selector는 select() 호출에 블록화된다. 그리고 새로운 연결이 이루어져 다른 스레드가 자신을 깨우거나 다른 스레드가 원래의 블록화된 스레드를 중단시킬 때까지 기다린다.

서버 등록하기

Server는 아래에서 보여지듯 들어오는 모든 연결을 받아들이기 위하여 Selector에 자신을 등록하는 ServerSocketChannel을 말한다.


SelectionKey acceptKey = serverChannel.register(sel, SelectionKey.OP_ACCEPT);

   while (acceptKey.selector().select() > 0 ){

     ......

Server가 등록되고 나면 우리는 Key 세트를 반복 적용하여 각각을 그 유형에 따라 처리한다. Key가 처리된 후에 그 key는 대기 key 목록에서 삭제되는데, 이는 다음과 같다:


Set readyKeys = sel.selectedKeys();
    Iterator it = readyKeys.iterator();
while (it.hasNext()) 
{

SelectionKey key = (SelectionKey)it.next();
  it.remove();
  ....
  ....
  ....
 }

Key가 받아들여질 수 있으면 연결은 받아들여지며 채널은 읽기나 쓰기 작업과 같은 향후의 이벤트에 등록되어진다. Key가 읽기 가능하거나 쓰기 가능하면, 서버는 그 말미에 데이터를 읽거나 쓸 준비가 되었다고 표시한다.


SocketChannel socket;
if (key.isAcceptable()) {
    System.out.println("Acceptable Key");
    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
    socket = (SocketChannel) ssc.accept();
    socket.configureBlocking(false);
    SelectionKey another = 
      socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
if (key.isReadable()) {
    System.out.println("Readable Key");
    String ret = readMessage(key);
    if (ret.length() > 0) {
      writeMessage(socket,ret);
    }
		    
}
if (key.isWritable()) {
    System.out.println("Writable Key");
    String ret = readMessage(key);
    socket = (SocketChannel)key.channel();   
    if (result.length() > 0 ) {
      writeMessage(socket,ret);
    }
    }

아브라카다브라 - nonblocking 서버 소켓이 나타나라, 쨘!

JDK 1.4에서의 nonbloking 입출력에 대한 이 소개글의 마지막 부분은 여러분의 몫이다. : 바로 예제를 실행시키는 것이다.

이 간단한 nonblocking 서버 소켓의 예에서 서버는 클라이언트에서 보낸 파일명을 읽고 파일 내용을 화면출력하며 내용을 작성하여 클라이언트에게 다시 보낸다.

이 예제를 실행시키려면 다음과 같은 것이 필요하다:

  1. Install JDK 1.4를 설치한다. (참고 자료)

  2. 소스 파일 을 여러분 디렉토리에 복사한다.

  3. java NonBlockingServer로 서버를 컴파일하고 실행시킨다.

  4. java Client로 클라이언트를 컴파일하고 실행시킨다.

  5. 클래스 파일이 있는 디렉토리에 텍스트나 자바 파일명을 입력한다.

  6. 서버는 파일을 읽고 그 내용을 클라이언트에게 보낸다.

  7. 클라이언트는 서버에게서 받은 데이터를 출력한다. (사용된 ByteBuffer의 제한 때문에 1024 바이트만 읽힐 것이다)

  8. 종료 명령어를 입력하여 클라이언트를 종료시킨다.

결론
Merlin이 제공하는 새로운 입출력 패키지는 광범위한 분야를 다룬다. Merlin의 새로운 nonblocking 입출력 구현의 주요 장점은 두 부분이다. 스레드는 더 이상 읽기나 쓰기에 블록화되지 않고, Selector는 복수의 접속을 처리할 수 있어 서버 애플리케이션의 부하가 상당히 줄어든다.

우리는 새로운 java.nio 패키지의 이 두 주요 장점을 강조하였다. 우리는 여러분이 여기에서 배운 것을 여러분의 실제 애플리케이션 개발 작업에 적용하기를 바란다.

참고 자료

목 차:
Merlin 이전의 입출력 프로그래밍
Reactor 패턴
Channels과 Selectors
아브라카다브라 - nonblocking 입출력이 나타나라, 쨘
결론
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Java design patterns 101
Java sockets 101
Magic with Merlin
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Aruna Kalagnanam는 IBM India labs에서 e-비즈니스 통합 기술 부문의 소프트웨어 엔지니어로 일하고 있다.


Balu G는 IBM India labs에서 e-비즈니스 통합 기술 부문의 소프트웨어 엔지니어로 일하고 있다.

이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의