채널 이벤트 계층 개발

ProObject 런타임은 이벤트 드리븐(Event-Driven)으로 방식으로 동작하며, 이벤트는 채널 이벤트이벤트 두 종류로 구분되어 사용된다. 본 장에서는 이 중 네트워크 레벨에서 이루어지는 채널 이벤트 계층의 채널 이벤트와 채널 이벤트 핸들러의 개념과 개발 방법에 대해서 설명한다.

버전250에서 부터는 Custom Channel Handler 지원이 중단되었다. 채널 핸들러를 직접 만들기보다 더 하이 레벨인 서비스 수준에서 프로그래밍하길 권장한다.

1. 개요

ProObject 런타임에 서비스의 요청은 반드시 제일 앞단에 있는 채널 이벤트 계층에서 처리된다.

채널 이벤트 계층은 실제 외부 네트워크와의 통신과 프로세스/스레드간의 통신을 모두 포함하는 채널 이벤트에 대한 처리를 한다. 채널 이벤트는 읽기 가능, 쓰기 가능, 쓰기 요청, 연결 요청, 연결 완료 등과 같은 이벤트로, Java NIO의 Selector에서 처리하는 멀티플렉싱(Multiplexing)의 개념과 비슷하다.

채널 이벤트는 채널마다 별도로 생성되고, 채널 이벤트를 처리하는 이벤트 핸들러(Event Handler)도 채널마다 생성되어 할당된다.

채널 이벤트는 다음과 같이 크게 5가지로 구분된다.

  • 읽기 가능(Readable)

    채널로부터 데이터가 읽기가 가능함을 나타내는 것으로, 소켓을 예로 들지만 소켓 읽기 버퍼(Read Buffer)에 데이터가 전달된 상태를 의미한다.

  • 쓰기 요청(Write Request)

    런타임 엔진이 특정 채널에 데이터를 쓰기를 원하는 경우 발생되는 이벤트로, 개발자가 원할 때 직접 이 이벤트를 발생시켜주어야 한다. 아웃바운드 서비스 요청의 경우에는 런타임 엔진이 자동으로 해당 이벤트를 발생시킨다.

  • 쓰기 가능(Writable)

    채널의 쓰기 버퍼(Write Buffer)에 빈 공간이 생겨 쓰기가 가능함을 알려주는 이벤트로, 쓰기 버퍼가 비워져있다면 항상 이 이벤트가 발생된다.

  • 연결 완료(Connected)

    외부 서버로 연결을 요청하였을 때, 그 연결이 완료된 경우 발생되는 이벤트로, 외부 서버와 연결이 없는 경우에는 발생되지 않는다.

  • 연결 수락(Accepted)

    외부 서버가 런타임 엔진으로 연결을 요청하였을 때 발생되는 이벤트로, 서버 소켓 채널(Server Socket Channel) 에서만 발생되는 이벤트이다.

위의 채널 이벤트 외의 임의의 채널 이벤트를 생성하는 것은 불가능하다.

채널 이벤트 핸들러는 이벤트가 발생되었을 때 해당 이벤트를 처리할 수 있는 메소드가 자동으로 호출된다.

ProObject에서 채널 이벤트를 처리하기 위해 다음의 2가지의 채널 이벤트 핸들러를 제공한다.

  • 채널 억셉트 핸들러(Channel Accept Handler)

    서버 소켓 채널(Server Socket Channel)에서 발생될 수 있는 Accepted Channel Event를 처리하기 위한 이벤트 핸들러이다. Accepted Event는 오직 서버 소켓 채널을 통해서 발생하므로, 사실상 서버 소켓 채널 전용의 채널 이벤트 핸들러로 볼 수 있다. 자세한 내용은 채널 억셉트 핸들러를 참고한다.

  • 채널 이벤트 핸들러(Channel Event Handler)

    연결 요청 이외의 다른 모든 채널 이벤트를 처리하기 위한 이벤트 핸들러로 보통 채널 이벤트 핸들러라는 명칭을 쓰면 이 핸들러를 의미한다. 이 종류의 핸들러는 읽기 가능, 쓰기 요청, 쓰기 가능, 연결 완료 에 대한 채널 이벤트를 모두 처리할 수 있다. 자세한 내용은 채널 이벤트 핸들러 개발을 참고한다.

이벤트를 처리할 채널 이벤트 핸들러를 작성한 경우 런타임 엔진에 채널 매니저(Channel Manager)에 채널과 채널 이벤트 핸들러를 등록하는 과정이 필요하다.

2. 채널 매니저

채널 매니저는 채널과 채널 이벤트 핸들러를 연결하여, 채널 이벤트를 채널 이벤트 핸들러가 처리할 수 있도록 한다. 따라서 채널 이벤트를 처리하려면 채널 매니저를 이용해 채널을 등록하는 과정이 필수적이며, 필요한 경우에는 처리할 채널 이벤트의 종류를 변경해야 한다.

채널 매니저의 모든 API는 static 메소드로 작성되어 있고 ChannelManager 클래스를 통해 API를 호출한다. 본 절에서는 채널 매니저에 채널을 등록/해제하고 채널 이벤트 종류 변경하는 API에 대해서 설명한다.

2.1. 채널 등록

채널 매니저를 통해 채널과 해당 채널의 이벤트를 처리하는 채널 이벤트 핸들러를 사용하려면 반드시 채널 매니저에 등록하는 과정이 필요하다. 이때 등록할 채널과 채널 이벤트 핸들러 종류에 따라 호출해야 하는 메소드가 다르므로 상황에 따라 선택한다.

  • 채널 억셉트 핸들러 등록

    채널 이벤트를 처리할 채널 억셉트 핸들러를 등록한다. 채널 억셉트 핸들러는 서버 소켓 채널과 연결을 수락한다.

    void register(AbstractSelectableChannel channel, ChannelAcceptHandler handler)
    매개변수 설명

    channel

    채널 이벤트가 발생할 서버 소켓 채널 객체를 전달한다.

    handler

    채널 이벤트를 처리할 이벤트 핸들러의 객체를 전달한다.

    여기에 전달되는 객체는 반드시 ChannelAcceptHandler 클래스를 상속받아 구현된 객체여야 한다.

  • 채널 이벤트 핸들러 등록

    채널 이벤트 핸들러를 등록한다. 채널을 등록하면 정수값이 반환되는데 이는 등록된 채널의 고유 아이디(ID)로, 필요한 경우에는 채널 아이디로 다양한 작업들을 수행할 수 있다.

    int register(AbstractSelectableChannel channel, ChannelEventHandler handler, int ops)
    매개변수 설명

    channel

    서버 소켓에서 연결 수락되어 전달되거나, 자신이 생성한 채널 객체를 전달한다.

    사용이 가능한 채널은 SourceChannel, SinkChannel, SocketChannel, DatagramChannel 등이 있으며, Channel에 대한 자세한 내용은 JDK의 Channel 관련 항목을 참고한다.

    handler

    채널 이벤트를 처리하기 위한 채널 이벤트 핸들러 객체를 전달한다.

    전달되는 객체는 반드시 Channel Event Handler 클래스를 상속받아 구현된 객체여야 한다.

    ops

    어떤 종류의 채널 이벤트를 처리할지 여부를 지정한다.

    JDK의 SelectionKey의 interestOps와 동일한 값을 사용하므로, 처리를 원하는 채널 데이터 종류를 지정한다. 여러 채널 이벤트 종류를 Bitwise Or( | ) 연산을 이용해 여러 채널 이벤트를 처리할 수 있다.

    • SelectionKey.OP_READ

      읽기 가능(Readable) 채널 이벤트를 처리한다.

    • SelectionKey.OP_WRITE

      쓰기 가능(Writable) 채널 이벤트를 처리한다.

    • SelectionKey.OP_CONNECT

      연결 완료(Connected) 채널 이벤트를 처리한다. 외부 서버에 직접 connect 를 요청하지 않은 경우에는 처리하지 않아도 된다.

2.2. 채널 등록 해제

채널을 더 이상 사용하지 않을 경우에는 채널 매니저에서 채널의 등록을 해제해야 한다. 소켓 채널 이벤트 핸들러(SocketChannelEventHandler)의 close 메소드에는 이 과정이 자동으로 수행되므로 호출하지 않아도 되나, ChannelEventHandler 클래스를 직접 상속받은 경우에는 이 작업을 명시적으로 처리해주어야 한다.

  • 채널 아이디를 이용한 등록 해제

    채널을 등록할 때 반환된 채널의 아이디를 전달해서 해당 채널을 해제할 수 있다.

    void unregister(AbstractSelectableChannel channelId, boolean closeChannel)
    매개변수 설명

    channelId

    등록을 해제할 채널의 아이디를 지정한다.

    채널의 아이디는 채널을 등록할 때 반환되며, 채널 이벤트 핸들러에서 getChannelId()의 메소드를 통해서도 얻을 수 있다.

    closeChannel

    등록을 해제함과 동시에 채널을 닫을지 여부를 지정한다.

  • 채널 객체를 이용한 등록 해제

    채널 객체를 전달해서 채널을 해제할 수 있다.

    void unregister(AbstractSelectableChannel channel, boolean closeChannel)
    매개변수 설명

    channel

    등록을 해제할 채널의 객체를 지정한다.

    closeChannel

    등록을 해제함과 동시에 채널의 리소스를 함께 회수할지 여부를 지정한다.

2.3. 채널 이벤트 변경

채널에서 처리해야 하는 채널 이벤트의 종류가 달라질 경우에는 채널 매니저에 현재 처리해야 할 이벤트 종류를 다시 알려주어야 한다.

일반적인 경우에는 채널에 데이터가 도착한 경우에 대해서만 처리를 진행하므로, 대부분의 경우에는 읽기 가능(Readable, SelectionKey.READ)을 처리할 것이라고 채널 매니저에 등록한다. 이후 데이터를 읽어들어 서비스를 요청해서 처리한 후에는 서비스의 응답을 채널을 통해 돌려주어야 하는 경우가 발생한다. 이 경우에는 채널로 데이터를 바로 쓰도록 처리하면 되지만, 한 번에 데이터를 모두 보내지 못하면 현재 채널에 쓰기가 가능한 상태가 이루어지기를 기다려야 한다.

이때 처리할 채널 이벤트를 쓰기 가능(Writable, SelectionKey.WRITE)으로 변경해서 채널이 쓰기 가능해 질 때 다시 쓰기를 진행해야 한다. 채널 이벤트 종류는 채널 아이디나 채널 객체를 통해 변경이 가능하며, 이에 따라 사용하는 메소드가 달라진다.

  • 채널 아이디를 이용한 감시 이벤트 변경

    채널을 등록할 때 반환된 채널의 아이디를 전달해서 감시 이벤트 종류를 변경할 수 있다.

    void update(int channelID, int interestOps)
    매개변수 설명

    channelId

    감시 이벤트를 변경할 채널의 아이디를 지정한다.

    채널의 아이디는 채널을 등록할 때 반환되며, 채널 이벤트 핸들러에서 getChannelId()의 메소드를 통해서도 얻을 수 있다.

    interestOps

    변경할 감시 이벤트 종류를 지정한다.

    다음의 3가지 종류를 지정할 수 있다.

    • SelectionKey.READ

    • SelectionKey.WRITE

    • SelectionKey.READ | SelectionKey.WRITE

  • 채널 객체를 이용한 감시 이벤트 변경

    채널 객체를 전달해서 감시 이벤트 종류를 변경할 수 있다.

    void update(AbstractSelectableChannel channel, int interestOps)
    매개변수 설명

    channel

    감시 이벤트를 변경할 채널의 객체를 지정한다.

    interestOps

    변경할 감시 이벤트 종류를 지정한다.

    다음의 3가지 종류를 지정할 수 있다.

    • SelectionKey.READ

    • SelectionKey.WRITE

    • SelectionKey.READ | SelectionKey.WRITE

2.4. 타임아웃 지정

채널 이벤트 핸들러는 타임아웃(Timeout)으로 읽기 가능 채널 이벤트나 쓰기 가능 이벤트 등에 대해 최대 대기 시간을 지정할 수 있다. 타임아웃을 지정하고 일정한 시간이 초과된 경우에는 onTimeout 메소드가 호출되므로, 필요한 작업이 있는 경우 onTimeout 메소드에 처리할 내용을 작성한다.

  • 채널 아이디를 이용한 타이아웃 지정

    채널을 등록할 때 반환된 채널의 아이디를 전달해서 타임아웃 시간을 지정할 수 있다.

    void update(int channelID, long timeout, TimeUnit timeUnit)
    매개변수 설명

    channelId

    감시 이벤트를 변경할 채널의 아이디를 지정한다.

    채널의 아이디는 채널을 등록할 때 반환되며, 채널 이벤트 핸들러에서 getChannelId()의 메소드를 통해서도 얻을 수 있다.

    timeout

    지정할 타임아웃 시간을 전달한다.

    0 이하의 값을 전달하는 경우에는 타임아웃이 해제되며, 양수인 경우에는 정해진 시간이 초과된 이후 onTimeout 메소드가 호출된다.

    timeUnit

    타임아웃 시간의 단위를 지정한다.

  • 채널 객체를 이용한 타이아웃 지정

    채널 객체를 전달해서 감시 이벤트 종류를 변경할 수 있다.

    void update(AbstractSelectableChannel channel, long timeout, TimeUnit timeUnit)
    매개변수 설명

    channel

    감시 이벤트를 변경할 채널의 객체를 지정한다.

    timeout

    지정할 타임아웃 시간을 전달한다.

    0 이하의 값을 전달하는 경우에는 타임아웃이 해제되며, 양수인 경우에는 정해진 시간이 초과된 이후 onTimeout 메소드가 호출된다.

    timeUnit

    타임아웃 시간의 단위를 지정한다.

3. 채널 이벤트 핸들러 개발

본 절에서는 채널 억셉트 핸들러와 채널 이벤트 핸들러를 개발하기 위해서 사용하는 메소드와 유의사항, 예제코드에 대해서 설명한다.

3.1. 채널 억셉트 핸들러

채널 억셉트 핸들러(Channel Accept Handler)는 서버 소켓 채널을 처리하기 위한 전용 채널 이벤트 핸들러로 Accepted Channel Event만 처리한다. 만약 ProObject 런타임 엔진을 TCP 서버로 기동시려면 반드시 이 채널 억셉트 핸들러를 작성해서 외부 서버의 연결 요청을 받아들을 수 있도록 해야한다.

채널 억셉트 핸들러는 SocketChannelAcceptHandler 클래스를 상속받아 구현해야 하며, 반드시 아래의 메소드를 오버라이딩해야 한다.

public void onAccept(AbstractSelectableChannel serverChannel, AbstractSelectableChannel clientChannel)
매개변수 설명

serverChannel

요청을 수락한 서버 소켓 채널(ServerSocketChannel) 객체가 전달된다.

대부분 특별히 사용할 일은 없으나, 필요한 경우에 한해 캐스팅하여 사용하도록 한다.

clientChannel

서버 소켓 채널을 통해 연결이 수락된 소켓 채널 객체가 전달된다.

메소드를 오버라이딩한 후에는 해당 clientChannel을 처리할 채널 이벤트 핸들러를 채널과 함께 채널 매니저에 등록하는 과정이 반드시 필요하다. 채널 매니저에 등록하는 과정이 생략될 경우 연결만 맺어지고 그 어떠한 처리도 이루어지지 않지 않게 된다.

다음은 채널 억셉트 핸들러에 대한 이해를 위해 참고용으로 작성된 코드이다.

public class ExampleAcceptHandler extends SocketChannelAcceptHandler {
    private ProObjectLogger logger = ChannelEventLogger.getLogger();

    public void onAccept(AbstractSelectableChannel serverChannel, AbstractSelectableChannel clientChannel) {
        try {
            ExampleChannelEventHandler channelEventHandler = new ExampleChannelEventHandler(clientChannel);
            channelEventHandler.init(SelectionKey.OP_READ); //-- (A) ---//
            ChannelManager.register(clientChannel, channelEventHandler, SelectionKey.OP_READ);
        } catch (Throwable e) {
            logger.severe("Exception is occured!", e);
        }
    }
}

위의 예제를 보면 연결이 수락된 clientChannel의 채널 이벤트를 처리할 ExampleChannelEventHandler 객체를 생성한 후 채널 이벤트 핸들러를 초기화하고 채널을 채널 매니저에 등록한다(A). init 메소드는 채널 이벤트 핸들러를 등록하고, 기본으로 처리할 채널 이벤트의 종류를 채널 이벤트 핸들러에 알려주는 역할을 담당한다. 해당 메소드에 대한 자세한 설명은 채널 이벤트 핸들러 개발을 참고한다.

init 메소드 내부에서 채널 핸들러를 등록하는 과정이 포함되어 있다면 아래의 register 코드는 작성하지 않아도 된다.

3.2. 채널 이벤트 핸들러 개발

서버 소켓 채널을 제외한 다른 채널들이 채널 이벤트를 처리하려면 반드시 채널 이벤트 핸들러(Channel Event Handler)를 작성해서 채널을 등록하는 과정이 필요하다. 이때 작성되는 채널 이벤트 핸들러는 ChannelEventHandler 추상 클래스를 상속받아 작성한다.

ChannelEventHandler는 가장 기본적인 채널 핸들러로, 엔진 내부에서 미리 정의하고 있는 내용이 거의 없기 때문에 자유로운 커스터마이징이 가능 하지만 모든 상태를 직접 개발자가 제어하므로 개발 난이도가 높다. 따라서 기능을 미리 작성해둔 템플릿 채널 이벤트 핸들러(Template Channel Event Handler)를 이용하여 개발하는 것을 권장한다.

채널 이벤트 핸들러에서 기본적으로 오버라이딩되어야 하는 메소드는 다음과 같다.

public void init(int interestOps) throws IOException
public void connect(SocketAddress socketAddress, Object additionalInfo)

public void onConnect(AbstractSelectableChannel channel)
public void onRead(AbstractSelectableChannel channel)
public void onWrite(AbstractSelectableChannel channel, Object data, UserContext userContext, long requestId)
public void onWritable(AbstractSelectableChannel channel)
public void onError(AbstractSelectableChannel channel, Throwable error)
public void onClose(AbstractSelectableChannel channel) throws Exception
public void onTimeout(AbstractSelectableChannel channel)

3.2.1. 직접 호출 메소드

애플리케이션 이벤트에서 직접적으로 호출할 수 있는 메소드는 다음과 같다. 템플릿 채널 이벤트 핸들러를 이용할 경우에는 작성하지 않아도 된다.

  • init

    채널들을 채널 매니저에 등록하고, 기본적으로 처리할 채널 이벤트 종류를 명시하기 위해 사용된다.

    void init(int interestOps) throws IOException
    매개변수 설명

    interestOps

    기본적으로 처리할 채널 이벤트의 종류를 명시한다.

    채널 이벤트의 종류는 채널 이벤트 핸들러 등록을 참고한다.

  • connect

    소켓 채널이 외부 서버에 연결이 필요한 경우 해당 연결을 처리한다.

    void connect(SocketAddress socketAddress, Object additionalInfo)
    매개변수 설명

    socketAddress

    연결할 외부 서버의 주소를 지정한다.

    additionalInfo

    연결을 요청할 때 전달된 추가 정보를 지정한다.

3.2.2. 콜백(Callback)호출 메소드

채널 이벤트가 발생하였을 때 콜백(Callback)으로 호출되는 메소드는 다음과 같다.

  • onConnect

    연결 완료(Connected) 이벤트를 받도록 채널 매니저에 채널이 등록된 경우 외부 서버와 연결이 맺어졌을 때 자동적으로 호출된다.

    void onConnect(AbstractSelectableChannel channel)
    매개변수 설명

    channel

    연결이 완료된 채널 객체가 전달된다.

    템플릿 채널 이벤트 핸들러에서는 connect() 메소드를 호출할 때 자동으로 연결 완료 채널 이벤트를 받도록 채널 이벤트를 변경하므로 신경쓰지 않아도 되지만, ChannelEventHandler를 통해 구현한 경우에는 이 작업을 별도로 처리해주어야 한다.

    메소드는 ProObject 런타임이 서버로 기동하는 경우에는 호출되지 않으며, 런타임이 클라이언트로 동작하는 경우에만 유효하다. 소켓으로 통신하지 않거나, 아웃바운드 서비스에 이용되지 않는 경우에는 작성하지 않아도 된다.

  • onRead

    읽기 가능(Readable) 이벤트를 받도록 채널 매니저에 채널이 등록된 경우 채널에 메시지가 도착하면 자동적으로 호출된다.

    void onRead(AbstractSelectableChannel channel)
    매개변수 설명

    channel

    읽기 가능 채널 이벤트가 발생한 채널 객체가 전달된다.

    메소드를 오버라이딩할 때에는 반드시 Non-Blocking 방식으로 처리가 이루어지도록 코드를 작성하여야 하며, 이에 대한 내용은 유의사항을 참고한다.

  • onWrite

    런타임 엔진으로부터 데이터 송신을 요청하는 쓰기 요청(Write) 이벤트가 발생된 경우 호출된다.

    쓰기 요청 이벤트는 다른 객체가 직접 발생시키는 이벤트이기 때문에 채널 매니저에 등록하는 과정이 없으며, 쓰기 요청 채널 이벤트가 발생하게 되면 무조건적으로 호출된다.

    void onWrite(AbstractSelectableChannel channel, Object data, UserContext userContext, long requestId)
    매개변수 설명

    channel

    쓰기 요청 채널 이벤트가 발생한 채널 객체가 전달된다.

    data

    쓰기를 원하는 데이터 객체가 전달되며, 쓰기 요청 채널 이벤트를 발생시킬 때 전달한 객체가 전달된다.

    아웃바운드가 서비스를 처리할 때에는 byte[]가 전달되며, 특별히 요청 정보 객체(Request Context) 내에 요청 메시지 타입(RequestMessageType)을 NONE으로 지정한 경우에는 데이터 오브젝트(DataObject)가 전달될 수도 있다.

    userContext

    이벤트 체인(Event-Chain) 내에서 공유되는 사용자 정보 객체가 전달된다.

    이벤트 체인에 대한 자세한 설명은 이벤트 계층 개발을 참고한다.

    requestId

    채널 이벤트의 유일한 값(Unique Value)으로, 서로 다른 채널 이벤트들을 구분하기 위해 사용된다.

    일반적으로 크게 사용될 일은 없으나 이벤트 체인에서 채널 이벤트를 발생시킨 곳으로 응답을 돌려주어야 하는 경우에 사용한다. 자세한 설명은 이벤트 계층 개발이벤트 연동을 참고한다.

    메소드를 오버라이딩 할 때에는 반드시 Non-Blocking 방식으로 처리가 이루어지도록 코드를 작성하여야 하며, 이에 대한 내용은 유의사항을 참고한다.

  • onWritable

    쓰기 가능(Readable) 이벤트를 받도록 채널 매니저에 채널이 등록된 경우 해당 이벤트가 발생되면 자동적으로 호출된다. 보통 onWrite에서 데이터를 완전히 다 보내지 못한 경우에 한해서만 onWritable이 호출되도록 처리하는 경우가 많다.

    void onWritable(AbstractSelectableChannel channel)
    매개변수 설명

    channel

    쓰기 요청 채널 이벤트가 발생한 채널 객체가 전달된다.

    메소드를 오버라이딩 할 때에는 반드시 Non-Blocking 방식으로 처리가 이루어지도록 코드를 작성하여야 하며, 이에 대한 내용은 유의사항을 참고한다.

  • onError

    등록된 채널에서 예외가 발생한 경우 호출된다.

    void onError(AbstractSelectableChannel channel, Throwable error)
    매개변수 설명

    channel

    예외가 발생한 채널 객체가 전달된다.

  • onClose

    채널 이벤트 핸들러의 close 메소드가 호출되었을 경우에 호출된다.

    void onClose(AbstractSelectableChannel channel) throws Exception
    매개변수 설명

    channel

    닫히는 채널의 채널 객체가 전달된다.

  • onTimeout

    채널 이벤트 핸들러에 타임아웃이 지정된 경우 해당 타임아웃 시간이 초과되면 호출된다. 채널 이벤트 핸들러의 close 메소드가 호출된 경우에 호출된다.

    void onTimeout(AbstractSelectableChannel channel)
    매개변수 설명

    channel

    타임아웃이 발생한 채널 객체가 전달된다.

3.2.3. 예제

다음은 ChannelEventHandler를 이용해 간단한 TCP 메시지를 송수신하는 예제이다.

public class ExampleHandler extends ChannelEventHandler {
    private SocketChannel socketChannel;
    private ByteBuffer    readBuffer;
    private ByteBuffer    writeBuffer;

    public ExampleHandler(SocketChannel socketChannel) throws IOException {
        this.socketChannel = socketChannel;
        init(SelectionKey.OP_READ);
    }

    @Override
    public void init(int interestOps) throws IOException {
        setChannel(socketChannel);
        ChannelManager.register(socketChannel, this, interestOps);

        readBuffer = ByteBuffer.allocateDirect(HEADER_SIZE);
    }

    @Override
    public void onRead(AbstractSelectableChannel channel) {
        try {
            if(readBuffer.hasRemaining()) {
                if(socketChannel.read(readBuffer) <= 0) {
                    close();
                }
            }

            if(readBuffer.hasReamining()) {
                // handle header
            }
        } catch (Throwable e) {
            onError(channel, e);
        }
    }

    @Override
    public void onWrite(AbstractSelectableChannel channel, Object data, UserContext userContext, long requestId) {
        try {
            byte[] bytes = (byte[])data;
            writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            ChannelManager.register(socketChannel, this, SelectionKey.OP_WRITE);
        } catch (Throwable e) {
            onError(channel, e);
        }
    }

    @Override
    public void onWritable(AbstractSelectableChannel channel) {
        try {
            if(writeBuffer.hasRemaining() == false) {
                writeBuffer = null;
                ChannelManager.update(getChannel(), SelectionKey.OP_READ);
            }
        } catch (Throwable e) {
            onError(channel, e);
        }
    }

    @Override
    public void onConnect(AbstractSelectableChannel channel) { }

    @Override
    public void onError(AbstractSelectableChannel channel, Throwable error) {
        if(logger.isLoggable(Level.SEVERE)) {
            logger.severe("Exception is occured!", error);
        }
        close();
    }
}

3.2.4. 유의사항

채널 이벤트 핸들러를 개발할 때에는 반드시 다음의 사항에 유의해야 한다.

  1. 채널 이벤트 핸들러는 Non-blocking 방식으로 코드를 작성해야 한다.

    블록킹(Blocking) 방식을 이용하여 핸들러가 구현되는 경우에는 이벤트 처리 스레드(Event Handler Thread)가 블록킹되는 것과 동일한 의미이므로 그 어떠한 요청도 받아들이지 못하는 상태가 되거나 전체적인 성능이 하락하는 결과가 발생할 수 있다. 때문에 채널 이벤트 핸들러를 개발할 때에는 반드시 Java NIO를 이용한 처리 방식과 동일한 방식으로 핸들러를 작성해야 한다.

  2. 채널 이벤트 핸들러는 Busy Waiting을 허용하지 않는다.

    예를 들어 다음과 같이 onRead 메소드에서 데이터를 읽을 경우 데이터가 정상적으로 수신되지 못하면 무한 루프에 빠져 그 어떠한 작업도 처리하지 못할 수 있다.

    public void onRead(AbstractSelectableChannel channel) {
        while(readBuffer.hasRemaining() == false) {
            socket.read(readBuffer);
        }
    }

    따라서 다음과 같이 추후에 새로운 읽기 가능 채널 이벤트가 발생하였을 때 onRead 메소드가 재호출되어 남은 메시지를 읽어들일 수 있도록 작성되어야 한다.

    public void onRead(AbstractSelectableChannel channel) {
        if(readBuffer.hasRemaining() == false) {
            socket.read(readBuffer);
        }
    }

4. 템플릿 채널 이벤트 핸들러

채널 이벤트 핸들러(Chanel Event Handler)를 이용하여 모든 코드를 작성하는 것은 매우 어렵기 때문에 ProObject에서는 보다 작성하기 쉽도록 일부 기능이 미리 구현되어 있는 템플릿 채널 이벤트 핸들러(Template Channel Event Handler)를 제공한다.

템플릿 채널 이벤트 핸들러를 이용할 경우 반드시 정해진 프로그램이 모델과 상황에 대해서만 처리가 가능하기 때문에 보다 다양한 처리가 필요하다면 템플릿 채널 이벤트 핸들러를 상속받아 구현하는 것은 적절하지 않을 수 있다. 그 경우에는 템플릿 채널 이벤트 핸들러를 상속받지 않아도 된다.

ProObject에서는 다음의 기본 템플릿으로 제공한다.

  • SocketChannelEventHandler

    일반적인 네트워크 소켓을 통해 데이터를 주고받는 경우에 사용하는 채널 이벤트 핸들러이다. 외부로 나가는 connect와 onConnect 등은 물론 onClose까지 구현되어 있어 데이터의 송/수신만 신경쓰면 되므로 보다 편리하게 채널 이벤트를 처리할 수 있다.

  • MessageChannelEventHandler

    소켓 통신에서의 데이터 송수신을 보다 더 용이하게 처리할 수 있는 기능을 제공하는 채널 이벤트 핸들러이다. 오고 가는 데이터를 모두 메시지의 개념으로 인식하고 있으며, 원하는 경우 추가적인 콜백 객체를 전달해 콜백 방식의 처리를 지원한다. 편리하게 채널 이벤트를 처리할 수 있으나 커스터마이징할 수 있는 부분은 적은 편이다.

4.1. SocketChannelEventHandler

소켓 통신에 고려해야 하는 대부분의 다양한 기능의 처리가 구현된 채널 이벤트 핸들러이다. 특히 ProObject 런타임이 클라이언트로써 다른 서버에 연결을 요청해 통신을 수행하는 경우 유용한데 연결 유지와 연결 실패에 대한 대부분의 처리를 모두 지원한다.

대부분의 특징은 모두 ChannelEventHandler와 동일하나 애플리케이션 이벤트에서 직접적으로 호출할 수 있는 메소드는 구현을 고려하지 않아도 무방하며, 예제 코드는 다음과 같다. 이때 소켓 채널 이벤트 핸들러에서 "연결 유지 기능"을 제공한다.

public class ExampleHandler extends SocketChannelEventHandler {
    private SocketChannel socketChannel;
    private ByteBuffer    readBuffer;


    public ExampleHandler() {
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        setChannel(socketChannel);
        init(SelectionKey.OP_READ);
        connect(new InetSocketAddress(REMOTE_IP, REMOTE_PORT), null);
    }

    public void onConnect(AbstractSelectableChannel channel) {
        super.onConnect(channel);
        readBuffer = ByteBuffer.allocateDirect(HEADER_SIZE);
    }

    public void onRead(AbstractSelectableChannel channel) {
        if(readBuffer.hasReamining()) {
            if(socketChannel.read(readBuffer) <= 0) {
                close();
            }
        }

        if(readBuffer.hasReamining()) {
            // handle header
        }
    }

    ...
}

연결 유지 기능

연결 유지 기능은 ProObject 런타임이 클라이언트가 되어 다른 서버와 연결을 맺으려 시도하거나, 연결이 도중에 끊어진 경우 연결을 다시 처리하기 위해 사용하는 기능이다. 이 경우에는 반드시 채널 이벤트 핸들러에서 'setKeepAlive(true); ' 메소드를 호출하여 연결 유지 옵션을 활성화되어야 하며, 연결을 맺는 도중이라면 별도의 처리없이도 자동으로 연결을 다시 맺으려 시도하게 된다.

다음은 연결 유지에 관련된 예제 코드로, 연결 유지에 관련된 메소드들을 호출하여 이 기능을 이용할 수 있다.

public class FailOverExample extends SocketChannelEventHandler {
    public FailOverExample(AbstractSelectableChannel channel) {
        setKeepAlive(true);
        setReconnectCount(-1);
        setReconnectPeriod(1000);
        setChannel(channel);
    }

    ...

    public void onRead(AbstractSelectableChannel channel) {
        ...
    }

    ...
}

다음은 연결 유지 기능을 위한 메소드에 대한 설명이다.

  • 채널 유지 기능 사용 여부

    채널 유지 기능을 사용 여부를 지정한다.

    public void setKeepAlive(boolean keepAlive)
    매개변수 설명

    keepAlive

    • true : 연결 유지 기능을 활성화한다.

    • false : 연결 유지 기능을 비활성화한다. (기본값)

  • 채널 재연결 시도 최대 횟수

    채널의 재연결을 시도할 최대 횟수를 지정한다.

    public void setReconnectCount(int count)
    매개변수 설명

    count

    • 양수 : 지정한 횟수만큼만 연결을 재시도한다.

    • 0 : 재시도를 하지 않는다. (기본값)

    • 음수 : 무한히 재시도를 시도한다.

  • 채널 재연결 시도 간격

    채널의 재연결을 시도하는 간격을 지정한다.

    public void setReconnectPeriod(long period)
    매개변수 설명

    period

    채널의 재시도 기간을 밀리초 단위로 지정한다. (기본값: 30초)

4.2. MessageChannelEventHandler

MessageChannelEventHandler는 개발의 편의성을 보다 높이기 위해 작성된 템플릿 핸들러로, SocketChannelEventHandler의 자식 클래스로 구현되어 있다. 일반적인 채널 이벤트 핸들러는 모두 채널에 대한 이벤트만 처리하는 것과 다르게, MessageChannelEventHandler는 채널에서 송수신의 단위를 메시지로 인식하고 메시지의 송수신에 대한 이벤트를 처리하는 모델을 지니고 있다.

일반적인 채널 이벤트 핸들러는 항상 송수신된 데이터를 채널 이벤트가 발생할 때마다 예외 상황에 대해 고려한다. 하지만 MessageChannelEventHandler는 읽을 메시지의 크기가 도착했는지 쓰고자 하는 byte 배열의 내용이 모두 전송되었는지를 이벤트로만 바라본다. 그러므로 송수신을 할 때 비동기적으로 데이터를 읽고 쓰는데 발생하는 예외 상황에 대해 고려를 적게 해도 된다는 장점이 있다.

MessageChannelEventHandler는 보다 용이하게 채널 이벤트를 다룰 수 있도록 송수신이 완료되었을 때 콜백으로 다른 Listener를 등록한다. 다른 채널 이벤트 핸들러가 이벤트를 받았을 때 항상 같은 메소드가 호출되므로 현재의 상태를 관리해야 하는 불편함을 덜어주어 쉽게 채널 이벤트를 처리할 수 있다. 하지만 Listener가 많아지면 이벤트에 따른 진입점이 다양지고, 콜백의 길이가 매우 깊어져 코드의 정리가 어렵고, 디버깅이 어려워질 수 있다. MessageChannelEventHandler는 자신이 원하지 않는 읽기/쓰기 이벤트가 발생했을 경우에는 항상 채널을 닫도록 동작이 구현되어 있으므로 이에 유의하도록 한다.

본 절에서는 MessageChannelEventHandler의 사용법에 대해서 설명한다.

4.2.1. 채널 핸들러 생성

MesageChannelEventHandler를 사용해서 개발을 진행할 때에는 일단 ChannelEventHandler로 사용할 클래스를 생성한 후 MessageChannelEventHandler를 상속받아야 한다. 클래스를 상속받았다면 onMessageReceiveonMessageSent 두 개의 추상 메소드를 오버라이딩해야 한다.

MessageChannelEventHandler는 두 개의 Constructor를 지니고 있으며, 상황에 따라 맞는 Constructor를 이용해 호출하도록 한다.

MessageChannelEventHandler(SocketChannel channel)
MessageChannelEventHandler(SocketChannel channel, int initialHeaderLength)
매개변수 설명

channel

Acceptor에서 전달받은 AbstractSelectableChannel 객체를 캐스팅하여 전달한다.

initialHeaderLength

최초로 읽어들일 헤더의 길이를 지정한다.

음수로 지정하거나 지정하지 않을 경우에는 자동으로 메시지를 읽으려 시도하지 않으므로 원하는 동작을 Constructor 내에 정의해야 한다. Constructor 내에서 별도로 작업을 지정하지 않는 경우 정의한 채널 핸들러는 아무런 동작을 하지 않다가 채널을 닫아버리는 등의 동작을 하게 되므로 유의하도록 한다.

4.2.2. 메시지 읽기

채널을 통해 메시지를 얻으려면 readMessage API를 이용한다.

readMessage는 정해진 길이만큼의 메시지를 다 읽으면 onMessageReceive 메소드를 자동으로 호출된다. ReadMessageListener 객체를 두 번째 인자로 지정하게 되면 onMessageReceive 메소드가 호출되지 않으며, ReadMessageListener에 구현된 onComplete 메소드를 호출한다. 메시지를 읽다가 실패한 경우에는 onError 메소드가 불리게되지만, ReadMessageListener가 전달되면 ReadMessageListener의 onError 메소드가 먼저 호출된 이후에 MessageEventHandler의 onError 메소드가 호출된다. Listener를 지정해도 자기 자신의 onError 메소드는 반드시 불리게 된다. onError 메소드를 오버라이드하지 않으면 채널을 자동으로 닫아버리는 동작을 처리하게 되므로 주의한다.

void readMessage(int length)
void readMessage(int length, ReadMessageListener listener)

readMessage에서 지정한 길이만큼의 메시지를 읽게되면 지정한 Listener의 onComplete나 자신의 onMessageReceive 메소드가 호출된다. onMessageRecieve는 반드시 작성해야 하는 추상 메소드로, 채널과 읽어들인 데이터가 포함된 ByteBuffer를 함께 전달한다.

void onMessageReceive(AbstractSelectableChannel channel, ByteBuffer buffer)

ByteBuffer로 전달된 객체에는 읽어들인 데이터가 byte[] 형태로 저장되어 있으므로, ByteBuffer의 데이터를 꺼내 원하는 대로 사용하면 된다. ReadMessageListener를 지정하였다면 onMessageReceive 대신 전달한 Listener의 onComplete 메소드가 호출되게 되며, 기본적인 동작은 onMessageReceive와 동일하다.

오류가 발생한 경우에는 오류를 처리하기 위해 Listener의 onError가 호출되는데, onError에서 오류 처리에 대한 부분을 수행한다.

interface ReadMessageListener {
    public void onComplete(AbstractSelectableChannel channel, ByteBuffer buffer);
    public void onError(AbstractSelectableChannel channel, Throwable exceptoin);
}

Listener는 ReadMessageListener를 상속받아 작성하며 예제는 다음과 같다.

Header header = ChannelEventHandlerUtil.unmarshalHeader(headerDataObjectClassName, bytes, ProMapperMessageType.FLD);
readMessage(header.getBodyLength(), new ReadMessageListener() {
    @Override
    public void onError(AbstractSelectableChannel channel, Throwable exception) {
        // 오류 처리.
    }

    @Override
    public void onComplete(AbstractSelectableChannel channel, ByteBuffer buffer) {
        // 메시지 처리.
    }
});

4.2.3. 메시지 쓰기

채널을 통해 메시지를 받을 때에는 writeMessage API를 이용한다.

writeMessage를 통해 메시지를 보낼 수 있으며 정해진 길이만큼의 메시지를 다 보내면 onMessageSent 메소드를 자동으로 호출하게 된다.

ReadMessageListener 객체를 두 번째 인자로 지정하게 되면 onMessageSent 메소드가 호출되지 않으며, WriteMessageListener에 구현된 onComplete 메소드를 호출한다. 메시지를 읽다가 실패한 경우에는 onError 메소드가 호출되지만, WriteMessageListener가 전달되면 WriteMessageListener의 onError 메소드가 먼저 호출된 이후에 MessageEventHandler의 onError 메소드가 호출된다. MessageChannelEventHandler의 onError 메소드는 Listener를 지정해도 항상 불리는 구조로 되어 있다. onError 메소드를 오버라이드하지 않았다면 Listener의 onError 메시지를 처리한 이후 채널을 자동으로 닫아버리는 동작을 처리하게 되므로 주의한다.

void writeMessage(byte[] buffer)
void writeMessage(byte[] buffer, WriteMessageListener listener)
void writeMessage(ByteBuffer buffer)
void writeMessage(ByteBuffer buffer, WriteMessageListener listener)

writeMessage에서 지정한 길이만큼 메시지를 쓰게 되면 지정한 Listener의 onComplete나 자신의 onMessageSent 메소드가 불리게 된다. onMessageSent 는 반드시 작성해야 하는 추상 메소드로 데이터를 보낸 채널 객체를 전달해준다.

void onMessageSent(AbstractSelectableChannel channel)

메시지가 전송된 경우에는 별도로 전달하는 데이터가 없으므로, 매개변수를 통해 채널 외에는 다른 내용을 전달하지는 않는다.

WriteMessageListener를 지정하였다면 onMessageSent 대신 전달한 Listener의 onComplete 메소드가 호출되며, 기본적인 동작은 onMessageSent와 동일하다.

오류가 발생한 경우에는 오류를 처리하기 위해 Listener의 onError가 호출되는데, onError에서 오류 처리에 대한 부분을 수행한다.

interface WriteMessageListener {
    public void onComplete(AbstractSelectableChannel channel, ByteBuffer buffer);
    public void onError(AbstractSelectableChannel channel, Throwable exceptoin);
}

Listener는 위의 WriteMessageListener를 상속받아 작성하며 예제는 다음과 같다.

// 메시지를 보낸 이후 다른 메시지를 기다린다.
byte[] bytes = new bytes[bodyLength];
...
writeMessage(bytes, new WriteMessageListener() {
    @Override
    public void onError(AbstractSelectableChannel channel, Throwable exception) {
        // 오류 처리.
    }

    @Override
    public void onComplete(AbstractSelectableChannel channel, ByteBuffer buffer) {
        readMessage(...);  // ACK 메시지를 읽어들이도록 지정한다.
    }
});

4.2.4. 예제 코드

다음은 MessageChannelEventHandler를 상속받아 작성한 참고용 예제 코드이다.

public class ExampleMessageChannelEventHandler extends MessageChannelEventHandler {
    private ProObjectLogger logger = ChannelEventLogger.getLogger();

    public ExampleMessageChannelEventHandler(SocketChannel channel)
     throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        this(channel, 4);
    }

    @Override
    public void onMessageReceive(AbstractSelectableChannel channel, ByteBuffer buffer) {
        try {
            int bodyLength = buffer.getInt();
            readMessage(bodyLength, new ReadMessageListener() {
                @Override
                public void onError(AbstractSelectableChannel channel, Throwable exception) {
                    logger.severe("[ExampleMessageChannelEventHandler] Exception is occured while receiving header!\r\n"
                            + "Client IP   : {0}\r\n"
                            + "Client Port : {1}", getRemoteIp(), String.valueOf(getRemotePort()), exception);
                }

                @Override
                public void onComplete(AbstractSelectableChannel channel, ByteBuffer buffer) {
                    byte[] bytes = new byte[buffer.limit()];
                    buffer.get(bytes);
                    buffer = null;

                    writeMessage(bytes);
                }
            });
        } catch (Throwable e) {
            logger.severe("[ExampleMessageChannelEventHandler] Exception is occured while unmarshaling header!\r\n"
                    + "Client IP   : {0}\r\n"
                    + "Client Port : {1}", getRemoteIp(), String.valueOf(getRemotePort()), e);
        }
    }

    @Override
    public void onMessageSent(AbstractSelectableChannel channel) {
        close();

        logger.info("[ExampleMessageChannelEventHandler] Response complete. Channel closed.\r\n"
                + "Client IP   : {0}\r\n"
                + "Client Port : {1}", getRemoteIp(), String.valueOf(getRemotePort()));
    }
}

5. 채널 이벤트 핸들러 배포

채널 이벤트 핸들러는 채널 계층에 속하지만 ProObject 런타임에서는 이벤트 핸들러로 인식하므로 이벤트와 이벤트 핸들러는 동일하게 배포가 이루어진다. 본 절에서는 채널 이벤트 핸들러를 배포하는 방법에 대하여 설명한다.

5.1. 바이너리 배포

채널 이벤트 핸들러는 이벤트 드리븐(Event-Driven) 처리 중에서 채널 이벤트를 처리하는 계층이다.

ProObject의 런타임은 채널 이벤트 핸들러를 애플리케이션에 속한 이벤트 처리자의 한 종류로 인식한다. 따라서 이벤트 채널 핸들러 바이너리도 바이너리의 배포 위치인 다음의 경로에 배포한다. 디렉터리 구조에 대한 자세한 설명은 애플리케이션 디렉터리 구조를 참고한다.

${APP_HOME}/event/

5.2. 채널 이벤트 핸들러 등록

채널 이벤트 핸들러를 런타임에서 활용하려면 채널 이벤트 핸들러를 런타임에 등록하는 과정이 필요하다. 서버 소켓 채널을 통해 항상 채널 이벤트 핸들러 객체를 런타임에 생성만 하는 경우에는 등록하는 과정을 진행하지 않아도 된다.

채널 이벤트 핸들러는 ChannelAcceptHandlerChannelEventHandler 중 어느 것을 상속받았느냐에 따라 등록하는 방법이 달라진다.

채널 이벤트 핸들러는 다음의 경로의 파일에 등록된다.

${APP_HOME}/config/channel.xml

다음은 channel.xml의 내용으로 XML의 네임스페이스는 존재하지 않는다.

<channel-config>
   <!-- 채널 이벤트 핸들러 -->
</channel-config>

5.2.1. 채널 억셉트 핸들러 등록

ChannelAcceptHandler는 <channel-accept-handler> 태그를 이용하여 등록한다.

<channel-config>
    <channel-accept-handler>
        <name>exampleAccepter</name>
        <class-name>com.tmax.proobject.example.channel.ExampleAccepter</class-name>
    </channel-accept-handler>
</channel-config>

다음은 각 항목에 대한 설명이다.

태그 설명

<name>

채널 이벤트 핸들러 아이디를 지정한다.

채널 이벤트 핸들러의 아이디는 유일한 값으로 지정한 이름은 서버 채널을 미리 열 때에 활용된다.

<class-name>

채널 이벤트 핸들러의 클래스의 이름을 지정한다.

클래스 이름은 패키지 이름을 포함하여 지정해야 한다.

5.2.2. 채널 이벤트 핸들러 등록

ChannelEventHandler는 <channel-event-handler> 태그를 이용하여 등록한다.

<channel-config>
    <channel-event-handler>
        <name>exampleHandler</name>
        <class-name>com.tmax.proobject.example.channel.ExampleHandler</class-name>
        <keep-connection>false</keep-connection>
        <reconnect-count>0</reconnect-count>
        <reconnect-period>300000</reconnect-period>
    </channel-event-handler>
</channel-config>

다음은 각 항목에 대한 설명이다.

태그 설명

<name>

채널 이벤트 핸들러의 유일한 아이디를 지정하며, 다른 아이디와 겹치지 않도록 주의한다.

여기에서 지정한 이름은 외부 서버와 미리 연결을 지정할 때 활용한다.

<class-name>

채널 이벤트 핸들러의 클래스의 이름을 지정한다.

패키지 이름을 포함하여 지정해야 한다.

<keep-connection>

채널 핸들러가 연결을 유지할지 여부를 설정한다.

이 설정이 활성화된 경우에는 연결된 IP, Port로 요청을 할 경우 이미 맺어진 연결로 메시지를 보내려 시도하며, 오류가 발생했을 경우 재접속을 시도하게 된다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setKeepConnection 메소드와 동일한 동작을 한다. 단, 이 채널 이벤트 핸들러가 외부 서버에 접속하려고 할 때에는 의미있는 옵션이며, 그 외의 경우에는 사용되지 않는다.

<reconnect-count>

외부 서버에 연결에 실패하거나, 오류가 발생한 경우 최대 재연결 시도 횟수를 지정한다.

<keep-connection>이 활성화되어 있어야 하며, ProObject 런타임이 외부 서버에서 클라이언트로 접속하려는 경우 사용된다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setReconnectCount 메소드와 동일하게 동작한다.

<reconnect-period>

외부 서버에 연결에 실패하거나, 오류가 발생한 경우 재연결 시도 주기를 지정한다.

<keep-connection>이 활성화되어 있어야 하며, ProObject 런타임이 외부 서버에서 클라이언트로 접속하려는 경우 사용된다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setReconnectPeriod 메소드와 동일하게 동작한다.

5.3. 채널 자동 생성 설정

채널 이벤트 핸들러의 등록을 완료한 후 채널 설정 파일에서 미리 서버 포트를 열어두거나 특정 서버와 미리 연결을 지정해서 채널을 자동으로 생성할 수 있다.

해당 정보는 다음 경로의 파일에 설정한다. 디렉터리 구조에 대한 자세한 설명은 애플리케이션 디렉터리 구조를 참고한다.

${APP_HOME}/config/channel.xml

5.3.1. 서버 채널 바인딩

ChannelAcceptHandler를 등록한 경우 <server-channel>에 다음과 같이 작성하여 서버 채널을 미리 바인딩할 수 있다.

<channel-config>
    <channel-accept-handler>
        <name>exampleAccepter</name>
        <class-name>com.tmax.proobject.example.channel.ExampleAccepter</class-name>
    </channel-accept-handler>
    <server-channel>
        <port>5000</port>
        <accept-handler>exampleAccepter</accept-handler>
    </server-channel>
</channel-config>

다음은 각 항목에 대한 설명이다.

태그 설명

<port>

바인딩할 포트의 번호를 지정한다.

<accept-handler>

설정한 서버 포트로 accept 요청이 전달된 경우 해당 이벤트를 처리할 ChannelAcceptHandler의 이름을 지정한다.

이때에는 등록한 ChannelAcceptHandler의 이름(ID에 해당)을 넣어준다.

5.3.2. 클라이언트 채널 연결

ChannelEventtHandler를 등록한 경우 <client-channel>을 다음과 같이 작성해 해당 채널 이벤트 핸들러를 이용하여 연결을 미리 맺어둘 수 있다.

<channel-config>
    <channel-event-handler>
        <name>exampleHandler</name>
        <class-name>com.tmax.proobject.example.channel.ExampleHandler</class-name>
    </channel-event-handler>
    <client-channel>
        <name>example_Client_1</name>
        <mode>R</mode>
        <ip>5000</port>
        <port>5000</port>
        <channel-handler>exampleHandler</channel-handler>
        <keep-connection>false</keep-connection>
        <reconnect-count>0</reconnect-count>
        <reconnect-period>300000</reconnect-period>
    </client-channel>
</channel-config>

다음은 각 항목에 대한 설명이다.

태그 설명

<name>

클라이언트 채널의 이름을 지정한다. 실제로 런타임에 쓰이지는 않지만 설정을 용이하게 하는 역할을 한다.

<mode>

감시할 기본 채널 이벤트의 종류를 지정한다.

  • R : 연결하자마자 데이터를 먼저 받아들여야 하는 경우에 지정한다.

  • W : 연결하자마자 데이터를 반드시 써야 하는 경우에 지정한다.

  • RW : 둘 다 연결되자마자 처리해야 하는 경우에 지정한다.

<ip>

연결할 서버의 IP 주소를 지정한다.

<port>

연결할 서버의 포트 번호를 지정한다.

<channel-handler>

클라이언트 채널로 이벤트가 발생한 경우 이를 처리할 채널 이벤트 핸들러를 지정한다. 등록한 ChannelEventHandler의 이름을 넣어준다.

<keep-connection>

채널 핸들러가 연결을 유지할지 여부를 설정한다.

이 설정이 활성화된 경우에는 연결된 IP, Port로 요청을 할 경우 이미 맺어진 연결로 메시지를 보내려 시도하며, 오류가 발생했을 경우 재접속을 시도하게 된다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setKeepConnection 메소드와 동일하게 동작한다.

ChannelEventHandler의 설정이 있는 경우에는 여기에 설정한 내용이 우선시된다. 단, 채널 이벤트 핸들러가 외부 서버에 접속하려고 하는 경우에만 의미있는 옵션이며, 그 외의 경우에는 사용되지 않는다.

<reconnect-count>

외부 서버에 연결에 실패하거나, 오류가 발생한 경우 최대 재연결 시도 횟수를 지정한다. <keep-connection>이 활성화되어 있어야 하며, ProObject 런타임이 외부 서버에서 클라이언트로 접속하려는 경우 사용되는 옵션이다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setReconnectCount 메소드와 동일하게 동작한다.

ChannelEventHandler의 설정이 있는 경우에는 여기에 설정한 내용이 우선시된다.

<reconnect-period>

외부 서버에 연결에 실패하거나, 오류가 발생한 경우 재연결 시도 주기를 지정한다. <keep-connection>이 활성화되어 있어야 하며, ProObject 런타임이 외부 서버에서 클라이언트로 접속하려는 경우 사용되는 옵션이다. 템플릿 채널 이벤트 핸들러"연결 유지 기능"에 setReconnectPeriod 메소드와 동일하게 동작한다.

ChannelEventHandler의 설정이 있는 경우에는 여기에 설정한 내용이 우선시된다.

채널 이벤트 핸들러를 통해 프로토콜 처리를 마친 후 채널 이벤트 계층은 이벤트 계층으로 애플리케이션 이벤트를 발생시킬 수 있다.

채널 이벤트는 이벤트 체인을 시작하고, 이벤트 계층이 발생한 이벤트를 재개시키거나, 필요한 경우에는 이벤트 체인을 종료하는 작업을 수행한다. 이벤트 계층과 상호작용하지 않을 경우에는 이벤트 체인과 관계가 없으므로 이에 대해 신경쓰지 않아도 되지만 이벤트 계층과 상호작용이 필요한 경우에는 반드시 이벤트 체인 개념을 이해하고 있어야 한다. 이벤트 체인(Event-Chain)에 대한 자세한 설명은 개요를 참고한다.

애플리케이션 이벤트와의 연동은 ChannelEventManager의 정적 메소드를 호출해서 수행한다. 본 절에서는 ChannelEventManager를 이용해서 애플리케이션 이벤트 계층과 연동하는 방법을 설명한다.

애플리케이션 이벤트와의 연동은 startEventresumeEvent 메소드를 이용한다.

startEvent는 이벤트 체인을 새로 생성하여, 애플리케이션의 이벤트가 최초로 시작되었음을 알려주는 역할을 담당한다. startEvent 메소드는 반드시 ProObjectRequest 객체를 요구하는데, 이 객체에 대한 자세한 설명은 Request Context를 참고한다.

startEvent는 세션을 넘겨주어 같은 세션으로 활용할지 여부를 지정하여 선택할 수 있으므로 용도에 맞게 API를 호출하여 사용한다.

연동 후 이벤트 체인이 발생하고 난 후에는 반드시 endEvent를 호출해주어야 한다.

6.1.1. 이벤트 체인 생성

최초 이벤트를 발생시키는 경우에 ChannelEventManager.startEvent를 사용한다.

  • 미리 맺어둔 세션이 없는 경우

    ChannelEventManager.startEvent(Event event, String ip, int port, ProObjectRequest request)
    매개변수 설명

    event

    연동하여 발생시킬 이벤트 객체를 생성하여 전달한다.

    ip

    요청이 들어온 클라이언트의 IP 주소를 지정한다.

    port

    요청이 들어온 클라이언트의 포트 번호를 지정한다.

    request

    채널로 들어온 요청에 대한 정보를 담고 있는 ProObjectRequest 객체를 지정한다. ProObjectRequest에 대한 자세한 설명은 서비스 개발을 참고한다.

  • 이미 맺어진 세션과 연동하려는 경우

    ChannelEventManager.startEvent(Event event, String ip, int port, ProObjectRequest request, String sessionKey)
    ChannelEventManager.startEvent(Event event, String ip, int port, ProObjectRequest request, ProObjectSession session)
    매개변수 설명

    event

    연동하여 발생시킬 이벤트 객체를 생성하여 전달한다.

    ip

    요청이 들어온 클라이언트의 아이피 주소를 지정한다.

    port

    요청이 들어온 클라이언트의 포트 번호를 지정한다.

    request

    채널로 들어온 요청에 대한 정보를 담고 있는 ProObjectRequest 객체를 지정한다. ProObjectRequest에 대한 자세한 설명은 서비스 개발을 참고한다.

    sessionKey

    세션의 ID를 지정하는 것으로, HTTP Web Session의 ID와 동일하다.

    session

    같이 묶을 세션의 객체를 지정한다.

    세션 객체는 ProObjectSession.getSession(sessionKey)를 통해 얻어올 수 있다.

6.1.2. 응답 이벤트의 발생 / 이벤트 합류

발생된 이벤트에 처리한 이후 응답을 돌려줄 경우에는 ChannelEventManager.resumeEvent를 사용한다. 응답 이벤트는 아니지만 채널 이벤트를 발생시킨 이벤트의 체인에 같이 묶이고 싶은 경우에도 ChannelManager.resumeEvent를 사용한다.

  • 이벤트 체인을 묶고 싶은 경우

    ChannelEventManager.resumeEvent(Event event, UserContext userContext)
    매개변수 설명

    event

    이벤트 체인에 묶으면서 새롭게 발생시킬 이벤트 객체를 지정한다.

    userContext

    이벤트 체인으로 묶을 유저 컨텍스트 객체를 지정한다.

    onWrite가 호출되었을 때 같이 전달된 유저 컨텍스트 객체를 지정한다.

  • 응답 이벤트를 발생시키는 경우 (채널 이벤트 아이디)

    채널 이벤트 아이디를 전달해서 기존의 이벤트에 응답을 돌려줄 수 있다. 채널 이벤트 아이디는 onWrite일 때 전달된 requestId이다.

    ChannelEventManager.resumeEvent(long requestId, Event event)
    매개변수 설명

    requestId

    onWrtie가 호출되었을 때 같이 전달된 채널 이벤트 아이디를 지정한다.

    event

    채널 이벤트를 발생시킨 이벤트의 응답이 될 응답 이벤트 객체를 지정한다.

  • 응답 이벤트를 발생시키는 경우 (요청 이벤트)

    채널 이벤트를 발생시킨 이벤트를 전달해서 응답을 돌려줄 수 있다.

    ChannelEventManager.resumeEvent(long requestEvent, Event responseEvent, UserContext userContext)
    매개변수 설명

    requestEvent

    채널 이벤트를 발생시킨 이벤트의 객체를 지정한다.

    responseEvent

    채널 이벤트를 발생시킨 이벤트의 응답이 될 응답 이벤트 객체를 지정한다.

    userContext

    이벤트 체인으로 묶을 유저 컨텍스트 객체를 지정한다.

    onWrite가 호출되었을 때 같이 전달된 유저 컨텍스트 객체를 지정한다.

ChannelEventManager의 startEvent 메소드를 이용해 이벤트 체인을 생성하였다면 반드시 이 이벤트 체인을 종료시켜야 한다. 이벤트 체인이 정상적으로 종료되지 않으면, 일부 리소스가 해제되지 않아 메모리를 상주하는 문제가 발생할 수 있다. 만약 startEvent를 한 번이라도 호출하였다면 반드시 endEvent를 호출해주어야 한다.

이 메소드는 이벤트 핸들러에서 호출할 수도 있으며, 채널 이벤트 핸들러 내에서도 호출할 수 있다.

ChannelEventManager.endEvent(UserContext userContext)
매개변수 설명

userContext

종료시킬 이벤트 체인의 사용자 정보 객체를 전달한다.

서비스와의 연동은 기본적으로 이벤트의 연동과 동일하므로 ChannelManager를 이용한 API에 대한 설명은 이벤트 연동을 참고한다.

  • 서비스 요청 처리

    서비스를 요청하는 경우에는 ServiceRequestEvent를 생성하여 서비스를 요청해야 한다.

    단순히 서비스를 요청하는 경우에는 아래와 같이 이벤트 객체를 생성하며, ChannelEventManager.startEvent를 통해 이벤트를 연동하는 방식을 이용한다.

    ServiceRequestEvent request = new ServiceRequestEvent(new ServiceName("applicationName.serviceGroupName.serviceName"),
    input, promapperMessageType);
  • 서비스 응답 처리

    아웃바운드 서비스 요청을 처리하기 위한 용도로 채널 이벤트 핸들러를 작성한 경우, 서비스에 대한 응답을 돌려주어야 할 경우가 있다. 이 경우에는 ChannelManager.resumeEvent API를 이용하여 응답을 돌려줄 수 있다.

    ServiceResponseEvent response = new ServiceReponseEvent(request, output, promapperMessageType);
    ChanelEventManager.resumeEvent(requestId, response);