서비스 개발

본 장에서는 ProObject의 설계 사상과 서비스의 개발 방법에 대해서 설명한다.

1. 개요

ProObject에서 말하는 서비스는 일반적으로 웹 브라우저를 통해 들어온 요청을 처리해 응답을 돌려주는 작업들을 의미한다. ProObject를 이용하여 작성되는 애플리케이션과 각종 서비스들은 금융 시스템, 카드 시스템, 학사 시스템 등과 같은 시스템들로 복잡도가 매우 높다. ProObject는 실제 서비스를 구현할 때의 복잡도를 최대한 낮출 수 있도록 단순화된 프로그래밍 모델과 여러 가지 기능들을 제공한다.

ProObject는 모든 것을 객체(Object)로 보고 하나의 서비스 요청을 하나의 서비스 오브젝트(Service Object)라는 객체와 매핑시킨다. 하나의 서비스를 처리하기 위해 필요한 업무 로직들을 하나의 서비스 오브젝트 타입의 클래스에 모두 작성하고, 실제 서비스가 수행될 때에는 해당 클래스의 인스턴스(Instance)가 생성되어 클래스에 작성된 업무 로직들을 수행시킨다. 서비스를 개발함에 있어 객체 지향 프로그래밍(OOP: Object Oriented Programming)과 동일한 방식으로 서비스를 작성할 수 있어 접근하기가 용이하고, 서비스를 편리하게 작성할 수 있다.

1.1. 주요 특징

ProObject에서의 서비스는 웹 브라우저를 통해 들어온 요청을 처리해 응답을 돌려주는 것이 기본이나 실제로는 다양한 의미를 갖는 용어로 사용된다.

웹이 발달함에 따라 웹 애플리케이션은 단순히 웹 브라우저를 통해 호출되는 HTTP에 대한 요청을 처리를 하는 역할만을 담당하지 않고, 웹 소켓(WebSocket)과 TCP와의 연동까지도 포함하는 등 영역이 크게 확장되었다. ProObject에서 말하는 서비스는 단순 HTTP 호출뿐만이 아니라, 일반적인 웹 애플리케이션에서 사용되는 모든 개념을 포괄할 수 있도록 모든 것은 서비스라는 사상을 기본으로 한다.

ProObject 런타임 서버로 들어오는 모든 요청은 서비스 요청으로 인식하며 호출되는 서비스는 각 프로토콜의 특성들과의 의존성(Dependency)이 없도록 구성된다. 따라서 ProObject에서는 서비스는 그 어떠한 경로로 전달되어도 상관없이 HTTP로 전달된 것으로 가정하여 서비스를 작성한다.

ProObject의 서비스는 모든 것은 객체로 인식하도록 설계되어 있다. 따라서 개발자들은 서비스를 일반 클래스를 작성하듯이 익숙한 방식으로 작성할 수 있다. 서비스가 호출되면 ProObject 런타임은 서비스 오브젝트 클래로부터 서비스 오브젝트 인스턴스를 생성한 후 인스턴스의 진입점에 해당하는 메소드를 호출해서 서비스 요청을 처리한다.

ProObject의 서비스는 다음의 특징을 가지고 있다.

  • 요청-응답 모델(R-R Model : Request-Response Model)을 따름

    ProObject의 서비스는 요청이 들어오기 전에는 수행되지 않으며, 서비스의 요청은 반드시 결과를 돌려주어야 한다. 일반적인 서비스를 요청한 요청자는 결과를 받지 않을 수도 있으나, ProObject 서비스는 요청자의 수신 여부에 상관없이 항상 결과를 돌려주어야 한다. 서비스의 종료 시점은 결과를 돌려주는 시점이나 오류가 발생하는 시점으로 고정된다.

  • 상태 보존 없음(Stateless)

    ProObject의 서비스는 수행될 때마다 항상 모든 값이 초기값으로 설정된다. 이전에 서비스를 처리한 변수를 재활용하지 않기 때문에 잘못 계산된 중간값을 새로운 서비스가 참조하여 업무에 오류가 발생할 수 있는 현상을 방지할 수 있다.

위의 특징들은 여러 문제들을 미연에 방지하는 것이 가능하므로 실제 서비스를 작성할 때에는 해당 특징을 그대로 유지할 것을 권장한다.

1.2. 서비스 분류

ProObject에서는 호출하는 방법에 따라 Complex Service, Event Service, Outbound Service를 제공한다.

  • Complex Service

    Complex Service는 여러 가지 업무 로직들과 복잡한 기능이 얽히도록 구현된 서비스로 서비스 그룹의 작업 스레드에 스케줄링되어 수행되는 서비스를 말한다.

    일반적으로 작성하는 모든 서비스는 Complex Service에 해당되며, Blocking I/O, 트랜잭션을 포함해 업무 로직을 작성하는데 있어 불필요한 작업을 하지 않도록 한다. 자세한 내용은 Complex Service를 참고한다.

  • Event Service

    Event Service는 서비스 그룹의 작업 스레드가 아니라 이벤트 처리 스레드에서 수행되는 서비스이다.

    이벤트 레이어(Event Layer)에서 이벤트 드리븐(Event-Driven) 방식으로 수행되는 서비스로, 항상 싱글 스레드에서만 수행되는 서비스이다. 별도의 워커 스레드로 배분되지 않으므로 동기화 오버헤드가 없어 간단한 로직을 처리하는 데 있어 Complex Service보다 높은 처리량을 보인다는 장점이 있다.

    그 외에도 서비스 오브젝트 간의 연동만이 가능한 Complex Service와는 달리, 이벤트 핸들러(Event Handler)와 외부 채널까지의 연동을 매우 손쉽게 처리할 수 있으므로 강력한 커스터마이징이 가능하다. 또한 동시에 자원에 접근하는 자원에 대한 경쟁 자체가 존재할 수 없기 때문에 간단하면서도 동기화가 필요 없는 경우에는 적합하다. 그러나 Blocking I/O의 처리를 하는 경우 ProObject의 성능의 심각한 악영향을 끼칠 수 있기 때문에 Tibero 외의 데이터베이스를 이용할 수 없다.

    또한 트랜잭션 보장이 불가능하다는 단점이 있으므로 복잡한 처리나 무결성이 보장되어야 하는 업무를 대상으로는 서비스를 처리할 수 없다. 자세한 내용은 Event Service를 참고한다.

  • Outbound Service

    Outbound Service는 현재 서비스가 속한 서버가 아니라 별도의 다른 외부 서버에 서비스 요청을 보내야 하는 경우 사용한다.

    Outbound Service는 실제로 구현된 서비스 업무 로직은 존재하지 않으며, 요청을 보낼 외부 서버의 IP와 포트 그리고 프로토콜을 처리하기 위해 채널 이벤트 핸들러(Channel Event Handler), 송수신의 메시지 타입을 설정한다. 자세한 내용은 Outbound Service를 참고한다.

2. Complex Service

ProObject의 Complex Service는 서비스 그룹에 속한 작업 스레드 풀에 스케줄링되어 수행되므로, 멀티 스레딩으로 동시에 여러 작업들을 수행하는 것이 가능하다. Complex Service를 사용하는 코드의 작성에 특별한 제약은 두지 않고 서비스의 기본 룰만 따를 것을 권고하기 때문에 부하에 대한 부담 없이 코드를 작성할 수 있다.

다음은 ProObject 런타임에서 서비스 작성을 위해서 제공하는 기능에 대한 설명이다.

  • 다양한 데이터 오브젝트 팩토리 지원

    일반적인 Event Service와 달리 Complex Service는 Blocking 방식의 I/O를 허용함으로써 반드시 동기적으로 작업이 처리되어야 하는 여러 가지 데이터베이스 벤더들과의 I/O를 제공한다.

  • 트랜잭션 보장

    Complex Service 작업이 정상적으로 처리되어 결과값을 돌려주었다면, ProObject 런타임 엔진은 결과값을 자동적으로 데이터베이스에 커밋(commit)하기 때문에 개발자는 특별히 트랜잭션에 대한 별다른 고민을 하지 않아도 된다. 서비스가 정상적으로 수행되지 못하여 예외(exception)가 발생하는 경우 런타임 엔진은 자동적으로 트랜잭션을 롤백(rollback)함으로써, 잘못된 데이터가 커밋되는 것을 방지한다.

    필요한 경우 서비스 간의 트랜잭션을 보장함으로써 서비스 수행 도중 호출한 다른 서비스가 실패한 경우 현재 서비스가 수행했던 트랜잭션도 함께 롤백된다. 트랜잭션 보장에 대한 내용은 간단히 API를 통해서 수정할 수 있으며, 필요한 경우 설정으로 트랜잭션들을 묶거나 분리할 수 있다.

  • 이미지 로그 기능 제공

    서비스 요청 및 결과를 전달하는 경우 전달되는 데이터들을 데이터베이스나 파일 등에 저장한다. 서비스에 실패한 경우에는 동일한 입력으로 서비스를 수행하거나 서비스 수행 결과를 남겨 오류를 추적하고 관리할 수 있다.

  • 선/후처리 기능 제공

    서비스가 수행되기 전이나 서비스의 응답이 전달되기 전에 선/후처리를 설정할 수 있다. 선/후처리를 통해 공통 로그나 로그성 데이터를 남길 수도 있고 암복호화 처리를 할 수 있다.

2.1. 서비스 코드 작성

본 절에서는 Complex Service를 작성하는 경우 사용되는 SerivceObjectServiceExecutor 클래스의 내부 동작의 구현 방법에 대하여 기술한다.

2.1.1. SerivceObject

Complex Service는 SerivceObject 인터페이스를 상속받아 구현하도록 설계되어 있으며, 서비스가 요청될 때 반드시 'service’라는 이름의 메소드가 호출된다. 따라서 Complex Service를 사용하는 경우 서비스를 요청할 때 실행할 로직을 service 메소드 내에 구현해야 한다.

다음은 ServiceObject 인터페이스 코드에 대한 설명이다.

public interface ServiceObject<InputDO, OutputDO> {
    OutputDO service(InputDO input);
}

ServiceObject 인터페이스는 첫 번째 인자값은 서비스의 입력 타입, 두 번째 인자값이 서비스의 출력으로 설정된다. 기본적으로 service 메소드를 추상 메소드로 제공하고 있으므로, ProObject의 모든 서비스는 service 메소드를 오버라이드(Override)해야 한다. 이 메소드는 Java의 main 메소드와 동일한 기능을 한다.

실제로 서비스가 호출되면 service 메소드가 호출되며, 사용자에게 전달된 입력 값은 service 메소드의 매개변수로 전달된다. 이때 타입은 ServiceObject 인터페이스의 InputDO 타입으로 설정되며, 설정하지 않았을 경우에는 Object 타입으로 설정되므로 캐스팅하여 사용한다. service 메소드는 OutputDO 타입을 반환하도록 되어 있는데, 이 값이 서비스의 출력으로 설정된다.

ServiceObject 인터페이스를 작성하는 경우 다음 사항에 주의한다.

  • 실제 리턴한 객체가 사용하는 OutputDO 혹은 등록된 서비스의 OutputDO와 타입이 다를 경우 ClassCastExcetpion이 발생하게 되므로 타입을 반드시 맞추어야 한다. 서비스를 등록하는 경우 OutputDO 타입을 설정하고 확인하는 방법은 메타 설정을 참고한다.

  • 서비스를 등록하는 경우 사용하는 InputDO와 ServiceObject 클래스 내에서 사용하는 InputDO가 다를 경우에도 ClassCastException이 발생하므로 유의한다.

  • ServiceObject에서 실제 수행할 내용은 service 메소드 내에 구현한다. 필요한 경우에는 자신이 사용할 로직들을 다른 메소드를 작성하여 호출하거나 멤버 필드들을 작성하여 호출한다.

  • ProObject의 서비스는 기존 로직에 대해 영향도가 없게 하기 위해 상태값을 저장하지 않도록 작성(Stateless)되어야 하기 때문에 static 필드는 사용하지 않는다. static 필드는 클래스에 하나만 존재하기 때문에 항상 기본적으로 상태를 저장하는 구조가 만들어진다.

위와 같은 내용들을 바탕으로 다음과 같이 간단한 서비스를 구현할 수 있다.

public class ExampleServiceObject implements ServiceObject<InputDO, OutputDO> {
    @Override
    OutputDO service(InputDO input) {
        ...
        OutputDO output = new OutputDO();
        return output;
    }
}

2.1.2. ServiceExecutor

ServiceExecutor는 실제 서비스 오브젝트(ServiceObject)를 생성하고, 서비스에서 수행할 메소드를 호출한다. 해당 클래스는 ProStudio에서 작성할 경우에는 자동 생성된다.

Complex Service의 ServiceExecutor는 ServiceExecutor 클래스를 상속받아 구현되며, 아래와 같이 작성된다.

public class ExampleServiceObjectExecutor extends ServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input);
        }

        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

일반적으로 rcall이 없는 일반적인 서비스는 위와 같이 코드가 생성된다. rcall이 있는 경우 서비스 수행의 진입점이 service 메소드가 아닌 경우에는 다음과 같이 코드가 생성된다.

public class ExampleServiceObjectExecutor extends ServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input);
        case "result" :
            return example.result(input);
        }

        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

2.2. 업무 로직 개발

ProObject에서의 업무 로직을 개발하는 방법에 대한 자세한 내용은 Business Object를 참고한다.

2.3. 데이터 입출력

ProObject에서는 데이터 오브젝트(DataObject)를 사용해서 데이터의 대한 입출력 처리를 하는 경우 데이터 오브젝트 팩토리(DataObjectFactory) 객체에 매핑한다. 실제 데이터의 내용과 입출력 처리를 하기 위해서는 두 객체를 별도로 개발해야 한다. 자세한 내용은 데이터 오브젝트/데이터 오브젝트 팩토리 개발을 참고한다.

서비스에서 수행할 모든 로직은 해당 서비스의 서비스 오브젝트 내에 작성되어야 한다.

다른 서버의 서비스 처리가 필요하거나 다른 서비스 그룹 또는 다른 애플리케이션의 서비스의 로직을 수행하는 경우가 있을 수 있다. 예를 들어 카드 회사에서 은행에서 카드 대금을 인출하려면 카드 애플리케이션에서 은행 애플리케이션으로 서비스를 요청해서 대금을 인출해야 한다. 이 경우 ProObject에서 제공하는 ServiceManager API를 사용하면 서비스를 연동할 수 있다.

ServiceManager를 사용하여 서비스를 연동하는 경우 "ApplicationName.ServiceGroupName.ServiceName"으로 서비스를 설정해야 한다. 따라서 호출할 서비스가 속한 애플리케이션 이름, 서비스 그룹 이름, 서비스 이름을 알고 있어야 한다.

서비스를 연동하는 경우 트랜잭션을 묶거나 HTTP 등으로 요청할 때 전달되는 여러 정보들을 설정하여 함께 전달할 수 있다. 또한 현재 수행 중인 서버가 아니라 다른 노드의 서버로 요청을 보내는 경우 서비스를 호출할 IP와 포트 번호 등을 설정할 수도 있다. 해당 내용에 대한 자세한 설명은 트랜잭션 설정요청 정보 설정을 참고한다.

call

call 방식은 다른 서비스를 동기적(Synchronous)으로 호출하는 경우 사용한다.

call 방식으로 서비스를 호출하게 되면 Caller는 Callee에서 응답이 올 때까지 해당 스레드에서 블록된 상태로 응답을 기다리게 되며, 응답이 도착하면 Callee 서비스의 출력 객체가 반환된다.

동기적으로 수행되는 경우 너무 깊은 깊이로 서비스가 연속으로 호출되면 워커 스레드가 모두 점유되어 더 이상 서비스의 수행이 불가능한 경우가 발생할 수 있으므로 주의해야 한다. 이 경우에는 서버를 재기동하거나 타임아웃(Timeout)이 발생할 때까지 기다릴 수 밖에 없다.

다음은 ServiceManager.call의 데이터 처리 과정에 대한 설명이다.

figure servicemanager call
ServiceManager.call의 수행도

call 방식으로 서비스를 연동할 경우에는 다음과 같이 코드를 작성한다.

output = (OutputDO)ServiceManager.call("proobject.example.Callee", input);

지원하는 메소드의 예제는 다음과 같다.

  • 기본형

    // 단순 기본 형태
    OutputDO output = ServiceManager.call("proobject.example.Callee", input);
    
    // 트랜잭션 타입 추가
    OutputDO output = ServiceManager.call("proobject.example.Callee", input, TransactionType.JOIN);
  • 요청 정보 추가

    // 단순 기본 형태
    RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
    OutputDO output = ServiceManager.call(requestContext, "proobject.example.Callee", input);
    
    // 트랜잭션 타입 추가
    RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
    OutputDO output = ServiceManager.call("proobject.example.Callee", input, TransactionType.JOIN);

acall

acall 방식은 다른 서비스를 비동기적(Asynchronous)으로 호출하는 경우에 사용한다. call 방식과는 달리 Caller가 Callee의 응답을 대기하기 위해 스레드에서 블록되지 않고 다른 업무 로직을 처리할 수 있어 일반적인 call 보다 처리량을 높일 수 있다.

acall은 Callee의 응답을 받는지 여부에 따라 acallacall_noreply 두 가지의 API를 제공한다.

  • ServiceManager.acall

    acall은 서비스를 요청한 후 다른 작업을 동시에 처리하는 경우 사용된다. 응답을 기다려야 하는 경우에는 acall을 호출하여 WaitObject를 반환받은 후에 호출한 서비스의 응답을 받아올 수 있다.

    아래 수행도에 보듯이 다른 스레드로 서비스를 요청한 후에도 자신의 스레드에서 작업을 계속 진행할 수 있으며, 이후 Callee의 응답이 필요한 시점에 Callee의 응답을 받아 작업을 수행한다.

    figure servicemanager acall callee first
    ServiceManager.acall의 수행도 (Callee가 먼저 끝난 경우)

    다음은 Callee의 서비스가 늦게 종료되어 Caller가 먼저 Callee의 종료를 기다리는 경우 수행도이다.

    figure servicemanager acall caller first
    ServiceManager.acall의 수행도 (Callee가 나중에 끝난 경우)

    WaitObject를 통해 응답을 얻는 경우에는 ServiceManager를 이용하거나 반환된 WaitObject 객체를 이용해 응답을 대기한다.

    ServiceManager를 이용해서 응답을 대기할 때에는 ServiceManager.getReply() 메소드를 호출한다. 메소드의 두 번째 인자에 타임아웃 시간을 설정할 수 있다. 타임아웃 시간이 지나도 응답이 오지 않았다면 다시 서비스 로직이 재개되므로 응답이 정상적으로 왔는지 검사한 후 작업을 수행한다.

    WaitObject wo = ServiceManager.acall("proobject.example.Callee", input);
    ...
    if(ServiceManager.getReply(wo, 60*1000) == null) {
        // Timeout!
    } else {
        OutputDO output = wo.get();
    }

    ServiceManager를 이용하지 않고 WaitObject를 통해 결과를 받아올 때에는 아래의 예제 코드와 같이 작성한다. 이 외에도 waitUntilDone, isDone 등의 여러 메소드를 통해 다양한 작업을 수행할 수 있다.

    WaitObject wo = ServiceManager.acall("proobject.example.Callee", input);
    ...
    try {
        wo.get();
    catch (Throwable e) {
        logger.severe("Callee gave me an exception!", e);
        throw e;
    }

    지원하는 메소드의 예제는 다음과 같으며, 트랜잭션에 관련한 처리는 지원하지 않는다.

    • 기본형

      // 단순 기본 형태
      WaitObject waitObject = ServiceManager.acall("proobject.example.Callee", input);
    • 요청 정보 추가

      // 단순 기본 형태
      RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
      WaitObject waitObject = ServiceManager.acall(requestContext, "proobject.example.Callee", input);
    • 전문 역전 방지형(Sticky Service)

      자세한 설명은 Sticky Service 설정을 참고한다.

      // 단순 기본 형태
      WaitObject waitObject = ServiceManager.acall("proobject.exampleCallee", input, "abcd");
      
      // 요청 정보 형태
      WaitObject waitObject = ServiceManager.acall(requestContext, "proobject.example.Callee", input, "abcd")
  • ServiceManager.acall_noreply

    acall_noreply는 Callee의 결과가 필요하지 않을 경우에만 사용하는 서비스 연동 방식이다.

    acall_noreply은 서비스 응답을 받아올 수 있는 방법이 제공되지 않으므로 호출한 서비스의 응답 결과에 따른 영향이 없는 경우에 사용한다.

    다음은 ServiceManager.acall_noreply을 사용해서 서비스를 호출한 경우의 프로세스이다.

    figure servicemanager acall noreply
    ServiceManager.acall_noreply의 수행도

    코드 내에서 acall_noreply를 호출할 때에는 아래와 같이 코드를 작성한다.

    ServiceManager.acall_noreply("proobject.example.Callee", input);

    지원하는 메소드의 예제는 다음과 같으며, 트랜잭션에 관련한 처리는 지원하지 않는다.

    • 기본형

      // 단순 기본 형태
      WaitObject waitObject = ServiceManager.acall_noreply("proobject.example.Callee", input);
    • 요청 정보 추가

      // 단순 기본 형태
      RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
      ServiceManager.acall_noreply(requestContext, "proobject.example.Callee", input);
    • 전문 역전 방지형(Sticky Service)

      자세한 설명은 Sticky Service 설정을 참고한다.

      // 단순 기본 형태
      WaitObject waitObject = ServiceManager.acall_noreply("proobject.exampleCallee", input, "abcd");
      
      // 요청 정보 형태
      WaitObject waitObject = ServiceManager.acall_noreply(requestContext, "proobject.example.Callee", input, "abcd")

rcall

rcall은 call 방식과 유사하게 서비스를 동기적(Synchronous)으로 요청하는 경우에 사용한다. call 방식은 Callee의 응답을 대기하기 위해 스레드를 점유하는 데 반해 rcall 방식은 Callee의 응답을 대기하기 위해 스레드를 점유하지 않는다는 특징이 있다.

다음은 rcall 방식의 처리 방식에 대한 설명이다.

figure servicemanager rcall
ServiceManager.rcall의 수행도
  1. Thread A는 Service α 작업을 할당받아 처리를 진행하다가 Thread B로 Service β를 요청한다.

  2. Thread A는 Service β를 요청한 이후 더 이상 그 어떠한 작업을 수행하지 않고 유후 상태로 전환된다.

  3. Thread A에 새로운 서비스에 대한 요청이 들어오게 되면 해당 서비스를 먼저 처리한다. 그림에서 새로운 Service γ를 처리하는 모습을 확인할 수 있다.

  4. Thread B에 할당된 Service β의 작업이 종료되면 다시 Service α가 중단된 곳부터 작업을 처리하기 위해 새로운 스레드(동일한 스레드일 수 도 있다)에 할당되어 작업이 재개된다.

  5. 이때 호출하였던 Service β의 응답이 Service α에게 입력으로 전달되며, Service α는 Service β를 호출한 이후의 로직에서부터 다시 작업을 시작하므로 마치 Service β를 동기적으로 호출한 것과 동일한 효과를 얻게 된다.

위 그림에서 보듯이 일반적인 동기 호출(call)과는 다르게 서비스를 호출한 기간 동안 다른 작업을 수행하지 않고, 필요에 따라 다른 서비스를 스케줄링하여 처리하기 때문에 Callee의 수행 시간이 길어져서 Caller가 오랜 시간동안 대기해야 하는 경우에는 처리량(Throughput)이 상승한다.

  1. 작업이 재개된 이후에는 이전에 수행했던 모든 콜스택과 스택 변수들이 초기화되므로 필요한 경우 스택 변수를 서비스 오브젝트에 저장하는 작업이 필요하다.

  2. 작업이 재개될 때 스케줄링되는 스레드가 다를 수 있기 때문에 스레드 로컬(ThreadLocal)과 같은 특수한 작업들은 처리할 수 없으므로 유의한다.

ProObject의 rcall은 Java의 특성상 이전의 중단된 부분에서 재시작하는 부분이 명령어(Instruction)와 같은 Low-Level 단계에서 제공되지 못하기 때문에 위의 그림과 동일하게 동작하지는 않는다. Java는 명령어 단계에서 작업을 나눌 수 없기 때문에 서비스를 작성할 때에 서비스가 재개되는 부분의 메소드를 분할해 두어야 한다.

rcall을 호출하는 메소드에서는 rcall을 호출한 이후 반드시 return 코드를 작성하여 서비스를 수행하는데 사용한 모든 콜스택(CallStack)을 제거해야 한다. 이후 다른 메소드에 자신이 호출한 서비스의 응답이 돌아온 이후 수행할 작업들을 정의해야 하며, 다른 서비스를 호출할 때에 자신이 수행할 메소드의 이름을 함께 전달해야 한다.

다음은 rcall로 서비스를 호출하는 코드의 예이다.

ServiceManager.rcall("proobject.example.Callee", input, "nextEntryPoint", 60*1000);

nextEntryPoint에는 rcall로 호출한 서비스의 응답이 돌아오면 호출할 메소드의 이름이다. 마지막에 전달되는 매개변수는 호출하는 서비스를 최대 얼마만큼 대기할지 설정하는 옵션이다(음수를 설정할 경우에는 무한히 대기하는 상태가 된다).

호출한 서비스에서 오류가 발생했을 때 처리할 로직을 분리할 경우에는 다음과 같이 코드를 작성한다.

ServiceManager.rcall("proobject.example.Callee", input, "nextEntryPoint", "handleError", 60*1000);

호출된 서비스가 정상적으로 완료되면 "nextEntryPoint", 오류가 발생한 경우에는 "handleError" 메소드가 호출된다.

특별히 오류 처리 메소드를 설정하지 않은 경우에는 서비스의 응답이 돌아올 때 호출되는 메소드와 동일한 메소드가 호출된다. 위와 같은 작업을 한 후 ServiceExecutor(ServiceExecutor)에 별도의 작업이 필요하나 ProStudio를 사용하는 경우에는 이 작업은 무시해도 된다.

public class ExampleServiceObjectExecutor extends ServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input);
        case "nextEntryPoint" :
            return example.nextEntryPoint(input);
        case "handleError" :
            return example.handleError(input);
        }

        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

다음은 rcall을 호출한 경우 응답을 처리하는 메소드(nextEntryPoint, handleError)의 예제로 진입점이 되는 메소드 위에는 항상 @RcallReturn으로 Annotation을 붙여준다.

@RcallReturn
OutputDO nextEntryPoint(CalleeOutputDO output) {
    ...
    OutputDO output = new OutputDO();
    return output;
}

@RcallReturn
OutputDO handleError(Throwable error) {
    // handle error...
    OutputDO output = new OutputDO();
    return output;
}

오류 처리 메소드 없이 nextEntryPoint에서만 응답을 처리하는 경우 다음과 같이 코드를 작성한다.

@RcallReturn
OutputDO nextEntryPoint(Object calleeOutput) {
    if(calleeOutput instanceof Throwable) {
        // handle error...
    } else {
       ...
    }
    ...
    OutputDO output = new OutputDO();
    return output;
}

이와는 별개로 rcall의 경우 자신이 대기하는 대상의 응답이 돌아오면 설정한 진입점에서부터 서비스를 재개하기 때문에 WaitObject를 통해 다른 작업을 기다리는 동작을 추가적으로 지원한다.

다음과 같이 WaitObject 객체를 넣어 작성한다.

ServiceManager.rcall(waitObject, "nextEntryPoint", 60*1000);

오류가 발생한 경우 이를 처리할 메소드를 별도로 설정하는 경우 다음과 같이 작성한다.

ServiceManager.rcall(waitObject, "nextEntryPoint", "handleError", 60*1000);

지원하는 메소드의 예제는 다음과 같으며 트랜잭션에 관련한 처리는 지원하지 않는다.

  • 기본형

    // 단순 기본 형태
    ServiceManager.rcall("proobject.example.Callee", input, "afterCall", "handleError", 60*1000);
  • 요청 정보 추가

    // 단순 기본 형태
    RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
    ServiceManager.rcall(requestContext, "proobject.example.Callee", input, "afterCall", "handleError", 60*1000);
  • WaitObject 리턴콜

    // 단순 기본 형태
    ServiceManager.rcall(waitObject, "afterCall", "handleError", 60*1000);

forward

forward 방식은 일반적인 서비스 연동 방법이 아니라 파이프라이닝(Pipelining)을 지원하기 위한 연동 방식이다.

일반적으로 다른 서비스를 호출하여 응답을 받아오는 방식과 달리 forward는 서비스를 호출은 하되 응답을 자신이 내보내지 않는 특이한 서비스 연동 방식이다.

forward 방식은 다른 서버와의 연동을 제공하지 않으며 오직 자신의 서버에 속한 서비스를 호출하는 것만을 허용하므로 다른 서버의 서비스를 호출하는 경우에는 정상적으로 응답이 나가지 않거나 트랜잭션이 보장되지 않는 등의 문제가 발생할 수 있으므로 유의해야 한다.

다음은 forward 연동방식 프로세스에 대한 설명이다.

figure servicemanager forward
ServiceManager.forward의 수행도

Thread A에서 Service α 작업을 할당받아 처리를 진행하다가 Thread B로 Service β를 호출한 후 응답을 돌려주지 않다가 Thread B에서 수행된 Service β가 응답을 최종적으로 돌려준다.

forward를 호출한 서비스(caller)는 forward를 호출한 당시에는 Service β가 나중에 실행되어야하는 서비스라는 것을 등록만하고, Service β 요청은 보내지 않는다.

그리고 caller의 모든 로직을 끝까지 수행한 후에 Service β 요청을 보낸다.

Thread A는 Service β를 요청한 이후 어떠한 작업을 수행하지 않고 유후 상태(Idle State)로 전환된다.

이때 forward로 호출된 서비스는 일반적인 서비스를 반환하듯이 자신의 응답을 돌려주면 되지만 실제로는 자신을 호출한 Service α의 응답을 돌려주는 동작을 수행한다.

이러한 특성을 이용하여 아래와 같이 후속 서비스들을 설정하고 전달하는 방식을 통해 전체적인 전체 작업을 파이프라이닝할 수 있다.

figure servicemanager forward pipelining
forward를 통해 pipelining된 모습

코드로 작성할 때에는 다음과 같이 작성하면 되며, forward를 호출한 서비스에서는 결과를 리턴하여도 실제로 결과가 전달되지 않으므로 'return null;'로 코드를 작성하여도 무방하다.

ServiceManager.forward("proobject.example.Callee", input);

forward의 Caller와 Callee 서비스의 출력 DO 타입은 반드시 같아야 한다.

forward 방식으로 서비스를 요청한 후 Caller 서비스는 바로 스레드를 유휴 상태로 만들어야 하기 때문에 서비스를 처리하고 있던 모든 콜스택을 초기화해야 하며, 이는 rcall과 동일하다.

다음은 지원하는 메소드의 예제로 요청 정보를 변경하는 등의 작업은 지원하지 않는다. HTTP와 관련된 내용들을 변경하는 경우 자기 자신의 요청 정보를 변경하여 사용한다.

  • 기본형

    // 단순 기본 형태
    ServiceManager.forward("proobject.example.Callee", input);
    
    // 트랜잭션 타입 추가
    ServiceManager.forward("proobject.example.Callee", input, TransactionType.JOIN);

rendezvous

rendezvous는 호출할 수 없는 서비스와의 연동이 필요한 경우 사용하는 연동 방식이다.

일반적으로 Caller 서비스에서 연동할 서비스를 직접 호출하는 것과 달리 직접 호출은 못하지만 해당 서비스가 호출되었다는 것을 알고 서비스 수행을 이어나가야 할 경우가 있다. Caller는 rendezvous 호출을 통해 자신이 특정 서비스를 기다리고 있음을 알리고 해당 WaitObject에 rcall로 대기한다. Callee의 경우 수행되면서 rendezvous를 통해 자신이 호출되었음을 알리면 Caller의 WaitObjec가 완료되면 다시 수행한다.

다음은 rendezvous의 처리 방식에 대한 설명이다.

figure servicemanager rendezvous
ServiceManager.rendezvous의 수행도

그러나 다음과 같은 경우에는 Dynamic Rendezvous 연동 방식을 사용한다.

  • Rendezvous에 참가하는 서비스가 상황에 따라 매우 가변적으로 변해 런타임에 Rendezvous할 서비스가 달라지는 경우

  • 여러 개의 Rendezvous가 가능한 서비스들 중 가장 먼저 응답이 전달되는 N 개만의 Rendezvous만 성공해도 문제가 없는 경우

다음은 Rendezvous와 Dynamic Rendezvous 연동 방식에 대한 설명이다.

  • Rendezvous

    서비스를 작성하는 시점부터 rendezvous를 통한 연동이 결정되어 있고, 변동의 여지가 없다면 servicegroup.xml을 설정한 후 rendezvous AP를 이용할 수 있다.

    servicegroup.xml에 다음과 같이 설정한 후 rendezvous API를 호출해 두 서비스를 연동할 수 있다.

    // Caller ServiceMeta에 추가해야 할 설정
    <rendezvous-service> proobject.example.Callee </rendezvous-service>
    
    //Callee ServiceMeta에 추가해야 할 설정
    <rendezvous> TRUE </rendezvous>
    • Caller

      // CALLER Service proobject.example.Caller
      // rendezvous 할 서비스의 이름과 매칭할 Key를 넣어준다
      WaitObject waitObject = ServiceManager.rendezvous("proobject.example.Callee", key);
       // rendezvous를 통해 받은 waitobject 와 rendezvous 성공시 수행할 메소드 이름, rendezvous를 기다릴 시간을 인자로 넣어준다.
      ServiceManager.rcall(waitObject,"nextMethod", 60 * 1000);
      
      return null;
    • Callee

      // CALLEE Service proobject.example.Callee
      // rendezvous 할 서비스의 이름과 매칭할 Key를 넣어준다
      ServiceManager.rendezvous("proobject.example.Caller", key);
      
      // Callee의 Business Logic
      // ...
  • Dynamic Rendezvous

    만나야 하는 서비스가 동적으로 변경되는 경우에는 정적으로 servicegroup.xml에 내용을 기술하는 것이 어려울 수 있다. 이 경우에는 servicegroup.xml에 설정하지 않고 API를 기술해서 참여하는 서비스들을 Dynamic Rendezvous 기능을 사용해서 동적으로 직접 설정하는 것이 가능하다.

    Dynamic Rendezvous는 Caller와 Callee가 모두 Rendezvous에 참가하는 서비스들을 명시적으로 항상 알아야 하며, 조건을 항시 알아야 한다. 위에서 기술한 일반적인 Rendezvous와는 다르게 몇 개의 서비스와 Rendezvous를 이루어야 하는지를 나타내는 minEncounterCount 매개변수가 전달될 수 있는 API가 별도로 필요하다.

    다음은 API의 원형이다.

    ServiceManager.rendezvous(Object matchKey, int minEncounterCount, String hostServiceName, String ... serviceNames)
    
    ServiceManager.rendezvous(Object matchKey, String hostServiceName, String... serviceNames)
    // 이 경우 minEncouterCount는 serviceNames.length()로 판단

    다음은 3개의 Rendezvous 서비스와 만날 수 있는 가능성이 있으면서, 먼저 도착한 2개의 서비스와 Rendezvous가 되어야 하는 경우의 예제이다.

    Caller는 Rendezvous의 결과들을 받아야 하는 서비스라는 것에 유의하고 모든 Caller와 Callee는 다음과 같이 코드를 작성해주어야 한다.

    • Caller

      // CALLER
      ServiceManager.rendezvous(new Key(1), 2, "test.main.Caller" ,{"test.main.Callee1", "test.main.Callee2", "test.main.Callee3"});
    • Callee

      // CALLEE
       ServiceManager.rendezvous(new Key(1), 2, "test.main.Caller" ,{"test.main.Callee1", "test.main.Callee2", "test.main.Callee3"});

2.4.1. 트랜잭션 설정

트랜잭션 설정은 JOIN과 SPLIT 옵션이 있으며 서비스를 연동할 때 매개변수를 설정하여 전달할 수 있다.

  • JOIN 옵션

    현재 서비스와 같은 트랜잭션으로 묶이는 것을 말하며, Caller 서비스가 실패한 경우 Callee의 서비스도 자동적으로 실패하여 롤백 절차를 거치게 된다. 반대로 Caller 서비스가 성공하는 경우에는 Callee의 서비스 실패 여부와 관계없이 커밋 진행되므로 예외처리에 반드시 신경을 써야한다.

  • SPLIT 옵션

    서비스 간의 트랜잭션을 분리하는 것으로 Callee와 Caller는 서로의 성공/실패와는 관계없이 자신의 서비스의 성공/실패에 따라 트랜잭션을 커밋하거나 롤백한다.

트랜잭션 설정은 acall, acall_noreply에서 지원하지 않는다.

ServiceManager.call("proobject.example.Callee", input, TransactionType.JOIN);
...
ServiceManager.call("proobject.example.Callee", input, TransactionType.SPLIT);

아무것도 설정하지 않을 경우 TransactionType은 기본적으로 SPLIT인 것으로 처리된다.

2.4.2. 요청 정보 설정

다른 서버로 서비스를 호출하거나 Cookie, QueryString, REST API 등의 값을 추가, 변경, 삭제하는 경우에는 다음과 같이 코드를 작성하여 요청 정보를 변경할 수 있다.

RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
ServiceManager.call(requestContext, "proobject.example.Callee", input);

위와 같이 RequestContext 객체를 얻어 정보들을 변경한 이후 서비스를 연동할 때 맨 앞 매개변수로 넣어주면 변경된 요청 정보를 담아 서비스가 수행된다. RequestContext에 대한 자세한 정보는 Request Context를 참고한다.

2.4.3. 현재 요청 정보 접근

현재 수행 중인 서비스에서 HTTP의 각종 Cookie, QueryString, REST API 등에 대한 값을 알아내거나 Caller 등에 대한 정보를 얻기 위해서는 다음과 같은 방법으로 현재 요청 정보를 확인할 수 있다.

RequestContext requestContext = ServiceManager.getCurrentRequestContext();

이후 해당 requestContext에 접근하여 원하는 정보들을 이용할 수 있다. RequestContext에 대한 자세한 정보는 Request Context를 참고한다.

2.4.4. Sticky Service 설정

스티키 서비스(Sticky Service) 란 특정 스레드를 지정해서 서비스 요청이 들어온 순서대로 수행되는 것을 보장하기 위한 기능이다. 논리적으로 Sticky Service Id를 설정해서 idle한 스레드 중 하나에 할당한다. 해당 id로 호출된 서비스는 지정된 스레드가 순서대로 수행하며 만약에 더이상 수행할 서비스가 없다면 자기에게 할당된 id를 제거한다.

다음과 같이 코드를 작성해서 id를 설정할 수 있다.

// abcd 가 Sticky Service Id 이다.
ServiceManager.acall("proobject.example.Callee", input, "abcd");

현재는 acall, acall_noreply API 에만 Sticky Service Id를 설정 할 수 있다.

3. Event Service

Event Service는 이벤트 계층(Event Layer)의 싱글 스레드에서 수행되는 서비스로 다양한 제약들이 존재한다. 따라서 반드시 필요한 경우에만 Event Service를 작성해야 하며, Event Service가 오동작을 할 경우 ProObject 런타임 자체의 성능이 기하급수적으로 저하될 수 있으므로 주의가 필요하다.

Event Service를 작성할 때에는 반드시 아래와 같은 사항들에 대하여 충분히 고려되어야 한다.

  • 트랜직션 없음

    동기적인 작업이 처리될 수 없어 일반적으로 흔히 사용하는 트랜잭션은 사용이 불가능하다. 대신 자동 반영(Auto Commit)만이 제공되므로 DB I/O를 수행하는 경우 유의해야 한다.

  • Time-Consuming Job(CPU Heavy Job) 불허

    Event Service는 싱글 스레드에서 수행되기 때문에 Time-Consuming Job과 같이 오랜 시간 수행되는 경우 다른 이벤트의 처리가 불가능해 런타임 엔진의 성능이 저하될 수 있다.

  • Blocking 작업 불허

    Event Service는 싱글 스레드에서 수행되기 때문에 Blocking I/O로 인해 스레드가 대기 상태로 들어갈 경우 런타임 엔진은 다른 작업도 처리할 수 없다.

3.1. 서비스 코드 작성

본 절에서는 Event Service를 작성하는 경우 사용되는 EventSerivceObjectEventServiceExecutor 클래스의 내부 동작의 구현 방법에 대하여 기술한다.

3.1.1. EventServiceObject

EventServiceObject는 Event Service가 수행하는 작업에 대한 로직을 담고 있는 객체이다. Event Service의 ServiceObject는 EventServiceObject 인터페이스를 상속받아 작성한다.

public class ExampleServiceObject implements EventServiceObject<InputDO, OutputDO> {
    @Override
    OutputDO service(InputDO input, UserContext userContext) {
        ...
    }
}

EventServiceObject 인터페이스는 Java의 Generic을 이용해 서비스의 입출력의 데이터 타입을 설정하고 입출력에 사용할 데이터 오브젝트(DO)를 설정할 수 있다.

EventServiceObject는 반드시 service 메소드를 오버라이드(Override)해야 하는데, service 메소드는 Java의 main 메소드와 동일한 기능을 한다. 따라서 실제로 서비스가 호출되면 service 메소드가 호출되며 사용자가 전달된 입력값은 service 메소드의 매개변수로서 전달된다. 이때 타입은 ServiceObject 인터페이스의 InputDO 타입으로 설정되며, 설정하지 않았을 경우에는 Object 타입으로 전달받아 클래스 캐스팅을 사용하여 구현한다.

service 메소드의 리턴은 ServiceObject 인터페이스의 OutputDO 타입으로 설정되며, 설정하지 않을 경우에는 Object 타입으로 설정하여 사용한다.

service 메소드는 Complex Service와는 다르게 매개변수에 UserContext 타입의 객체가 하나 더 전달되는데, UserContext는 이벤트들을 처리하는데 있어 필요한 변수들을 부가적으로 저장하기 위해 사용한다. UserContext에 대한 자세한 설명은 이벤트 계층 개발을 참고한다.

EventServiceObject 인터페이스를 작성하는 경우 다음의 사항에 주의한다.

  • 실제 리턴한 객체가 OutputDO 혹은 등록된 서비스의 OutputDO와 타입이 다를 경우 ClassCastExcetpion이 발생하게 되므로 타입을 반드시 맞춰야 한다. 서비스를 등록하는 경우 OutputDO 타입의 설정과 확인 방법에 대해서는 메타 설정을 참고한다.

  • 서비스를 등록하는 경우 InputDO와 ServiceObject 클래스 내에서 사용하는 InputDO가 다를 경우에도 마찬가지로 ClassCastException이 발생하므로 유의한다.

  • ServiceObject에서 실제 수행할 내용은 service 메소드 내에 구현한다. 필요한 경우에는 사용할 로직들을 다른 메소드를 작성하여 호출하거나 멤버 필드들을 작성하여 호출한다.

  • ProObject의 서비스는 Stateless하게 작성하여야 하므로 static 필드나 static 메소드는 사용하지 않는다.

3.1.2. EventServiceExecutor

EventServiceExecutor는 Complex Service의 ServiceExecutor와 마찬가지로 실제 서비스가 호출되었을 때 서비스 객체의 메소드를 호출하는 역할을 한다.

기본적으로는 무조건 service 메소드가 호출되지만 Event Service 연동을 하는 경우에는 다른 메소드가 호출될 가능성이 있기 때문에 반드시 작성해야 한다. Event Service의 EventServiceExecutor는 EventServiceExecutor 클래스를 상속받아 구현한다. 일반적으로 ProStudio를 이용하는 경우에는 자동으로 생성된다.

다음은 EventServiceExecutor의 구현 예이다.

public class ExampleServiceObjectExecutor extends EventServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method, UserContext userContext) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input, userContext);
        }
        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

다른 이벤트나 서비스와의 연동하지 않은 경우에는 위와 같이 작성한다. 이벤트나 서비스와 연동을 처리해 다른 메소드가 호출될 수 있는 경우에는 전달된 메소드 이름에 해당하는 메소드를 호출해주어야 한다.

public class ExampleServiceObjectExecutor extends EventServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method, UserContext userContext) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input, userContext);
        case "result" :
            return example.result(input, userContext);
        }
        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

EventServiceExecutor 클래스는 EventServiceObject를 제대로 생성하고 있는지 반드시 점검해야 한다. EventServiceExecutor에서 다른 EventServiceObject를 잘못 생성하거나 메소드를 잘못 호출하는 경우 서비스가 이상 동작할 수 있다.

3.2. 업무 로직 개발

ProObject에서의 업무 로직 개발에 대한 자세한 내용은 Business Object를 참고한다.

Event Service는 싱글 스레드에서 수행되는 서비스로 Blocking 방식으로 처리되는 작업이다. 따라서 연산이 과하게 많은 작업을 수행할 경우 ProObject 런타임 엔진의 성능을 하락시킬 가능성이 높다. 따라서 Event Service에서는 간단한 작업 위주의 업무 로직만을 수행하도록 코드를 작성해야 한다.

3.3. 데이터 입출력

Event Service의 데이터 입출력은 비동기 데이터 오브젝트(Async DataObject)의 API를 사용한다. 비동기적인 데이터 입출력이 필요한 경우에 대한 자세한 설명은 비동기 데이터 오브젝트를 참고한다.

Event Service는 Tibero 데이터베이스에 한해 비동기적인 데이터 입출력을 지원한다. 다른 데이터베이스의 벤더를 사용하는 경우 Event Service에서는 데이터 입출력을 처리할 수 없다.

3.4. 이벤트와 서비스 연동

본 장에서는 Event Service에서 다른 이벤트나 서비스와 연동하는 방법에 대해서 설명한다.

Event Service는 단순히 자신의 작업만 수행하는 것이 아니라 이벤트 영역에서 처리할 이벤트를 발생시키거나 Complex Service, Event Service, Outbound Service를 호출할 수 있다. 이벤트를 연동하기 위해서는 EventServiceManager의 API를 사용해야 한다.

Event Service에서 처리되는 연동 방식은 모두 비동기적인 방식으로 이는 이벤트 드리븐(Event-Driven)으로 수행되는 싱글 스레드에서 수행된다. 일반적인 이벤트 드리븐의 처리는 모두 비동기적으로 작업을 수행하고, 추후 작업을 재개할 때에는 콜백(Callback) 방식을 취한다.

ProObject의 이벤트 연동 방식도 이와 동일하게 비동기적인 방식으로 추후 작업을 재개하기 위한 콜백함수를 등록한다.

3.4.1. 이벤트 연동

이벤트 연동을 처리할 때 발생시킨 이벤트의 처리 결과를 받아야 하는지 여부에 따라 사용하는 API가 달라진다.

  • 이벤트 처리 결과가 필요하지 않은 경우

    단순히 이벤트를 발생시키는 경우에는 이벤트 객체를 생성한 이후 다음과 같이 처리한다.

    ExampleEvent event = new ExampleEvent();
    EventServiceManager.postEvent(event);

    위와 같이 이벤트를 발생시키면 Event Service는 별도로 처리 결과를 받지 않고 작업을 계속 진행한다.

  • 이벤트 처리 결과가 필요한 경우

    발생시킨 이벤트의 처리 결과를 알아야 하는 경우에는 이벤트를 발생시킬 때 이벤트 처리 결과가 도착했을 때 이를 처리할 콜백 메소드를 설정해 주는 방식을 이용한다. 이는 Complex Service의 rcall과 유사한 방식으로 이벤트의 처리 결과가 돌아오기 전까지 서비스는 잠시 중단된 상태로 대기한다(서비스 연동 참고). 이렇게 이벤트를 발생시킨 경우에는 아무리 결과를 반환하여도 응답이 전달되지 않으며 이벤트 처리 결과를 받은 이후에 작업을 수행하고 반환한 값이 서비스의 응답으로 전달된다.

    따라서 이벤트 처리 결과를 받기 위한 API를 이용한 경우에는 'return null;'을 이용하여 메소드를 종료한다.

    ExampleEvent event = new ExampleEvent();
    EventServiceManager.postEvent(event, "onComplete");

    이벤트를 처리하다 오류가 발생한 경우 메소드에서 이를 처리하려면 다음과 같이 코드를 작성한다.

    ExampleEvent event = new ExampleEvent();
    EventServiceManager.postEvent(event, "onComplete", "onError");

    위의 예제를 기준으로 추후 ExampleEvent의 이벤트 처리가 완료되고 onComplete 메소드가 처리 중 오류가 발생한 경우에는 onError 메소드가 호출된다. 이때 이벤트가 돌아온 경우 호출할 메소드를 설정하기 위해서는 EventServiceExecutor에서 해당 메소드를 수행할 수 있도록 코드를 수정해야 한다.

    public class ExampleServiceObjectExecutor extends EventServiceExecutor {
        public ExampleServiceObjectExecutor() {
            serviceObject = new ExampleServiceObject();
        }
    
        @Override
        public Object execute(Object input, String method, UserContext userContext) throws Throwable {
            ExampleServiceObject example = (ExampleServiceObject)serviceObject;
    
            switch(method) {
            case ServiceExecutor.DEFAULT_SERVICE_METHOD :
                return example.service(input);
            case "onComplete" :
                return example.onComplete(input);
            case "onError" :
                return example.onError(input);
            }
    
            throw new NoSuchMethodException("ExampleService - " + method);
        }
    }

    EventServiceObject에서 작성되는 메소드 위에는 반드시 PostEventResponse이라는 Annotation을 설정해야 한다.

    @PostEventResponse
    public OutputDO response(Object input) {
        ...
    }

3.4.2. 서비스 연동

Event Service에서도 다양한 서비스들과 연동을 처리를 위해 acallrcall 두 가지의 연동 방식만 제공한다.

ServiceManager를 사용하여 서비스를 연동하는 경우 "ApplicationName.ServiceGroupName.ServiceName"으로 서비스를 설정해야 한다. 따라서 호출할 서비스가 속한 애플리케이션 이름, 서비스 그룹 이름, 서비스 이름을 알고 있어야 한다.

서비스에 대한 응답을 받지 않을 경우에 사용하는 연동 방법이다.

acall은 응답 여부에 상관없이 일단 서비스를 호출하는 경우 사용되며, WaitObject 객체가 반환되지만 해당 객체를 바로 사용할 수는 없다. 응답을 받으려면 반드시 rcall을 호출해야 한다. 필요한 경우에는 다수의 서비스를 acall로 호출한 다음, And나 Or 등으로 엮어 다수의 서비스들을 한꺼번에 비동기적으로 기다릴 수도 있다.

EventServiceManager.acall("proobject.example.Callee", input);

지원하는 메소드의 예제는 다음과 같으며 트랜잭션에 관련한 처리는 지원하지 않는다.

  • 기본형

    // 단순 기본 형태
    WaitObject waitObject = EventServiceManager.acall("proobject.example.Callee", input);
  • 요청 정보 추가

    // 단순 기본 형태
    RequestContext requestContext = EventServiceManager.getServiceRequestContext("proobject.example.Callee");
    OutputDO output = EventServiceManager.acall(requestContext, "proobject.example.Callee", input);

Complex Service의 rcall과 거의 동일한 방식의 기능을 제공하는 연동 방식이다(서비스 연동 참고).

Complex Service는 오래 걸리는 서비스들을 대상으로 스레드 점유를 피하기 위하여 사용되나, Event Service에서는 비동기적으로 서비스의 응답을 받아 작업을 처리하기 위해 사용된다. Event Service의 rcall은 단순 콜백 위치를 설정해주는 개념이다. 일반적인 사용법은 Complex Service와 동일하다.

EventServiceManager.rcall("proobject.example.Callee", input, "nextEntryPoint", 60*1000);

nextEntryPoint에는 rcall로 호출한 서비스의 응답이 돌아오면 호출할 메소드의 이름이다.

호출한 서비스에서 오류가 발생하는 경우 처리 로직을 분리하려면 다음과 같이 코드를 작성한다.

EventServiceManager.rcall("proobject.example.Callee", input, "nextEntryPoint", "handleError", 60*1000);

호출된 서비스가 정상적으로 완료된 후에는 "nextEntryPoint"가 메소드를 오류가 발생한 경우에는 "handleError" 메소드가 호출된다.

특별히 오류 처리 메소드를 설정하지 않은 경우에는 서비스의 응답이 돌아올 때 호출되는 메소드와 동일한 메소드가 호출된다.

public class ExampleServiceObjectExecutor extends EventServiceExecutor {
    public ExampleServiceObjectExecutor() {
        serviceObject = new ExampleServiceObject();
    }

    @Override
    public Object execute(Object input, String method, UserContext userContext) throws Throwable {
        ExampleServiceObject example = (ExampleServiceObject)serviceObject;

        switch(method) {
        case ServiceExecutor.DEFAULT_SERVICE_METHOD :
            return example.service(input, userContext);
        case "nextEntryPoint" :
            return example.nextEntryPoint(input, userContext);
        case "handleError" :
            return example.handleError(input, userContext);
        }
        throw new NoSuchMethodException("ExampleService - " + method);
    }
}

위와 같이 rcall을 호출한 경우 응답을 처리하는 메소드를 다음과 같이 작성한다. 진입점이 되는 메소드 위에는 항상 @PostEventResponse으로 Annotation을 설정한다.

@PostEventResponse
OutputDO nextEntryPoint(CalleeOutputDO output) {
    ...
    OutputDO output = new OutputDO();
    return output;
}

@PostEventResponse
OutputDO handleError(Throwable error) {
    // handle error...
    OutputDO output = new OutputDO();
    return output;
}

nextEntryPoint 만을 이용하는 경우에는 다음과 같이 코드를 작성한다.

@PostEventResponse
OutputDO nextEntryPoint(Object calleeOutput) {
    if(calleeOutput instanceof Throwable) {
        // handle error...
    } else {
       ...
    }
    ...
    OutputDO output = new OutputDO();
    return output;
}

rcall의 경우 자신이 대기하는 대상의 응답이 돌아오면 설정한 진입점에서부터 서비스를 재개하기 때문에 WaitObject를 통해 다른 작업을 기다릴 수 있다. 다음은 WaitObject 객체를 넣어 작성한 예이다.

EventServiceManager.rcall(waitObject, "nextEntryPoint", 60*1000);

오류가 발생한 경우 이를 처리할 메소드를 별도로 설정하는 경우 다음과 같이 작성할 수 있다.

EventServiceManager.rcall(waitObject, "nextEntryPoint", "handleError", 60*1000);

지원하는 메소드의 예제는 다음과 같으며, 트랜잭션에 관련한 처리는 지원하지 않는다.

  • 기본형

    // 단순 기본 형태
    EventServiceManager.rcall("proobject.example.Callee", input, "afterCall", "handleError", 60*1000);
  • 요청 정보 추가

    // 단순 기본 형태
    RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");
    EventServiceManager.rcall(requestContext, "proobject.example.Callee", input, "afterCall", "handleError", 60*1000);
  • WaitObject 리턴콜

    // 단순 기본 형태
    EventServiceManager.rcall(waitObject, "afterCall", "handleError", 60*1000);
3.4.2.1. 요청 정보 설정

다른 서버로 서비스를 호출하거나 Cookie, QueryString, REST API 등의 값을 추가, 변경, 삭제하는 경우 다음과 같이 코드를 작성한다.

RequestContext requestContext = EventServiceManager.getServiceRequestContext("proobject.example.Callee");
EventServiceManager.acall(requestContext, "proobject.example.Callee", input);

위와 같이 RequestContext 객체를 얻어 정보를 변경한 이후 서비스를 연동할 때 맨 앞 매개변수로 넣어주면 변경된 요청 정보를 담아 서비스가 수행된다. RequestContext에 대한 자세한 정보는 Request Context를 참고한다.

3.4.2.2. 현재 요청 정보 접근

현재 수행 중인 서비스에서 HTTP의 각종 Cookie, QueryString, REST API 등에 대한 값을 알아내거나 Caller 등에 대한 정보를 얻기 위해서는 다음과 같이 코드를 작성한다.

RequestContext requestContext = EventServiceManager.getCurrentRequestContext();

이후 해당 requestContext에 접근하여 원하는 정보들을 이용할 수 있다. RequestContext에 대한 자세한 정보는 Request Context를 참고한다.

4. Outbound Service

Outbound Service는 외부로 전송되는 서비스이므로 별도의 클래스를 작성할 필요가 없어 대부분 설정으로만으로 사용할 수 있다.

정적으로 외부로 호출할 IP 주소, 포트 번호, 사용할 채널 이벤트 핸들러가 고정되어 있다면 메타 설정을 참고하여 설정을 진행한 후 설정한 Outbound Service를 호출한다.

동적으로 외부의 IP 주소나 포트 번호 등이 달라질 경우에는 서비스를 호출하기 전에 서비스 호출 정보를 동적으로 변경해야 한다. 서비스 호출 정보는 RequestContext 내에 존재하므로, 아래와 같이 작성하여 RequestContext를 먼저 얻어 사용한다. RequestContext에 대한 자세한 정보는 Request Context를 참고한다.

RequestContext requestContext = ServiceManager.getServiceRequestContext("proobject.example.Callee");

Outbound Service는 설정으로 외부의 ProObject 서비스를 내부 서버에 있는 것처럼 직접 호출하기 위한 기능으로 설정을 통해 연결 중인 ProObject에 없는 서비스를 내부 서버에 있는 것처럼 호출한다. 실제 로직은 리모트 노드가 수행한다.

다음의 경로에 remote.address.properties와 remote_servicegroup.xml을 설정해야 한다.

${PROOBJECT_HOME}/config/

remote.address.properties 설정

다음은 remote.address.properties의 설정 예이다.

test.main.call1.address=211.15.44.21:6776,46.123.23.16:8053
test.main.address=221.14.56.21:8080
test.address=125.124.111.24:6776
test.sub.NotInMainCall.address=192.168.15.48:6776,192.168.15.112:6776

서버 정보는 '{이름}.address = 주소' 형태로 설정한다. 주소는 'IP:PORT' 형태로 적으며 주소 간 구분자는 콤마(,)를 사용한다. 이름은 'applicationName.ServiceGroupName.ServiceName', 'ApplicationName.ServiceGroupName', 'ApplicationName' 3가지로 모두 정의가 가능하다.

다음은 외부 서비스를 직접 호출하는 경우 동작과 주의할 점에 대한 설명이다.

  1. remote.address.properties에 설정한 이름에 대해 여러 주소를 넣는 경우 라운드-로빈 방식으로 서비스를 요청하는 경우 마다 주소를 사용한다.

  2. remote.address.properties에서 이름을 검색하는 경우 좁은 영역부터 순차적으로 하게 된다. 즉, AppName.SGName.ServiceName을 다 만족하는 Key를 우선 검색하며, 검색되지 않으면 AppName, SGName, AppName 순으로 검색한다.

  3. 이 옵션을 사용할 리모트 서비스에 대한 서비스 그룹은 현재 노드에 존재해서는 안 된다. 위의 예시에서 test.sub.NotInmainCall, 즉 sub 서비스 그룹은 현재 노드에 없어야 리모트 노드를 검색한다. 만약 현재 노드에 해당 서비스 그룹이 존재한다면 이 옵션을 통하지 않고 현재 노드에 대해서 서비스 콜을 진행한다.

remote_servicegroup.xml 설정

리모트 서버에 있는 서비스의 메타를 설정하기 위해서 해당 서비스의 전체 이름, input-dto, output-dto의 클래스 정보가 필요하다. service-type은 OUTBOUND로 설정한다.

다음은 remote_servicegroup.xml의 설정 예이다.

<ns18:service-group xmlns:ns2="http://www.tmax.co.kr/proobject/message"
 xmlns:ns4="http://www.tmax.co.kr/proobject/dataobject"
 xmlns:ns3="http://www.tmax.co.kr/proobject/application/runtime"
 xmlns:ns6="http://www.tmax.co.kr/proobject/dto"
 xmlns:ns5="http://www.tmax.co.kr/proobject/resource"
 xmlns:ns30="http://www.example.org/testData"
 xmlns:ns8="http://www.tmax.co.kr/proobject/mapping"
 xmlns:ns7="http://www.tmax.co.kr/proobject/dto/validator"
 xmlns:ns13="http://www.tmax.co.kr/proobject/serviceobject"
 xmlns:ns9="http://www.tmax.co.kr/proobject/flow"
 xmlns:ns12="http://www.tmax.co.kr/proobject/dataobjectfactory"
 xmlns:ns11="http://www.tmax.co.kr/proobject/bizobject"
 xmlns:ns10="http://www.tmax.co.kr/proobject/aspectj"
 xmlns:ns17="http://www.tmax.co.kr/proobject/sourcecode"
 xmlns:ns16="http://www.tmax.co.kr/proobject/serviceobject-automatic"
 xmlns:ns15="http://www.tmax.co.kr/proobject/jobobject"
 xmlns:ns14="http://www.tmax.co.kr/proobject/taskobject"
 xmlns:ns19="http://www.tmax.co.kr/proobject/testcase"
 xmlns:ns18="http://www.tmax.co.kr/proobject/servicegroup"
 xmlns:ns20="http://www.example.org/externalObjectConfig"
 xmlns:ns24="http://www.tmax.co.kr/proobject/serverConfig"
 xmlns:ns23="http://www.tmax.co.kr/proobject/contents"
 xmlns:ns22="http://www.tmax.co.kr/proobject/prominer"
 xmlns:ns21="http://www.tmaxsoft.co.kr/proobject/testsuite"
 xmlns:ns28="http://www.tmaxsoft.co.kr/proobject/testresult"
 xmlns:ns27="http://www.tmax.co.kr/proobject/property"
 xmlns:ns26="http://www.tmax.co.kr/proobject/probuilder_config"
 xmlns:ns25="http://www.tmaxsoft.co.kr/proobject/testOperation"
 xmlns:ns29="http://www.tmax.co.kr/proobject/siteConfig">
 <ns18:service-object>
  <ns18:name>test.sub.NotInMainCall</ns18:name>
  <ns18:input-dto>
    com.tmax.proobject.test.runtime.application.test.dto.StringDataObject
  </ns18:input-dto>
  <ns18:output-dto>
    com.tmax.proobject.test.runtime.application.test.dto.StringDataObject
  </ns18:output-dto>
  <ns18:service-type>OUTBOUND</ns18:service-type>
 </ns18:service-object>
</ns18:service-group>

5. Business Object

서비스에서 실제 업무를 처리하기 위해서는 서비스 내에 업무 로직을 작성해야 한다.

비즈니스 오브젝트(Business Object)는 업무 로직이 객체화된 것으로 업무를 처리하는 데 필요한 임시 변수들과 로직을 담는 메소드들로 구성된다. 서비스는 작성된 서비스를 처리하기 위한 유일한(Unique) 작업들이 정의되나, 여러 서비스에서 중복으로 실행되어야 하는 업무들은 객체로 별도 작성되어 여러 곳에서 재활용한다.

  • 서비스 오브젝트에서 업무 로직을 작성하는 경우

    다음의 경우는 서비스 오브젝트에서 업무 로직을 작성한다.

    • 현재 서비스 오브젝트 외에 업무 로직을 활용하는 경우가 없는 경우

    • 별도의 객체를 만들기에는 넘겨야 하는 매개변수가 지나치게 많은 경우

      서비스 오브젝트에서 업무 로직을 진행하는 경우 service 메소드에서 업무 로직을 작성하거나 다른 메소드에서 작업을 처리해도 문제가 없다. 따라서 아래와 같이 일반적인 Java 클래스를 작성하는 것 처럼 작성해도 문제가 되지 않는다.

      public class ExampleServiceObject implements ServiceObject<InputDO, OutputDO> {
          @Override
          public OutputDO service(InputDO input) {
              doBusiness(input);
              ...
              // other Business ...
              ...
          }
      
          public void doBusiness(InputDO input) {
              ...
          }
      }
  • 비즈니스 오브젝트를 통해 로직을 작성하는 경우

    다음의 경우는 비즈니스 오브젝트에서 업무 로직을 작성한다.

    • 같은 서비스 그룹에 속한 다른 서비스 오브젝트에서 같은 업무 로직을 공유하는 경우

    • 비슷한 종류의 업무 로직들을 한 객체에서 관리하고 싶은 경우

    비즈니스 오브젝트에서 로직을 작성하는 경우 BusinessObject 인터페이스를 상속받아 일반적인 Java 클래스를 작성하는 것과 동일하게 작성한다. ProObject의 서비스는 모두 Stateless하도록 설계되어 있기 때문에 static 변수에 대해서는 사용하지 않을 것을 강력히 권고한다.

    public class ExampleBusiness implements BusinessObject
        public static void staticDoBusiness(inputDO input) {
            ...
        }
    
        public void doBusiness(InputDO input) {
            ...
        }
    }

    위와 같이 작성한 비즈니스 오브젝트는 아래와 같이 서비스 오브젝트 내에서 객체를 생성하여 호출하거나, static 메소드를 호출하여 사용할 수 있다.

    public class ExampleServiceObject implements ServiceObject<InputDO, OutputDO> {
        @Override
        public OutputDO service(InputDO input) {
            ExampleBusiness business = new ExampleBusiness();
            business.doBusiness(input);
            ...
            ExampleBusiness.staticDoBusiness(input);
            ...
        }
    }

6. Request Context

Request Context는 요청에 필요한 다섯 개의 객체를 내부적으로 지니고 있으며, 필요한 경우 수정해서 사용할 수 있다.

요청 객체 설명

requester

NetworkContext

서비스를 요청한 클라이언트의 정보를 저장한다.

responster

NetworkContext

서비스의 결과를 받을 클라이언트의 정보를 저장한다.

request

ProObjectRequest

서비스에 대한 요청 정보를 저장한다.

session

ProObjectSession

세션 정보를 저장한다. WebSession과 동일한 기능을 제공한다.

fileTransferHandler

ChannelHandlable

파일 전송에 사용된 채널 핸들러를 저장한다.

본 절에서는 각 객체를 수정해서 사용하는 방법을 설명한다.

6.1. NetworkContext 변경

Requester와 Responser는 각각 서비스를 요청한 클라이언트와 서비스의 결과를 반환할 클라이언트의 정보를 저장하고 있는 객체이다. 일반적으로 현재 수행 중인 서비스의 응답을 돌려줄 클라이언트를 변경하는 것은 불가능하다. 대신 서비스를 요청한 클라이언트의 정보는 수정이 가능하다.

RequestContext 객체를 얻었다면 다음과 같이 서비스를 요청한 클라이언트와 서비스의 결과를 반환할 클라이언트 정보를 조회할 수 있다.

NetworkContext requester = requestContext.getRequester();
NetworkContext requester = requestContext.getResponser();

RequestContext의 Requester 정보를 수정하는 경우 다음과 같이 서비스를 요청할 서버에 대한 정보를 변경할 수 있다.

NetworkContext requester = requestContext.getEditableRequester();
requester.setIPAddress("220.94.140.80");
requester.setPort(80);

이때 ProObject 간의 통신을 진행할 때 별도로 설정을 변경하지 않았다면 6776 포트를 사용한다.

외부로 요청을 보낼 때 ProObject가 아닌 경우에는 프로토콜을 처리하는 채널 이벤트 핸들러를 변경해주어야 하며, 등록된 채널 이벤트 핸들러의 이름을 설정하여 원하는 채널 이벤트 핸들러를 사용할 수 있다.

requester.setChannelEventHandlerName("user-protocol-handler");

채널 이벤트 핸들러 작성과 등록에 관한 내용은 채널 이벤트 계층 개발을 참고한다.

6.2. ProObjectRequest 변경

ProObjectRequest는 서비스에 대한 전반적인 요청 정보를 담고 있는 객체이다. 서비스를 요청하는 경우 ProObjectRequest에 정보를 저장하거나 RequestContext로부터 정보를 받아 서비스 처리에 활용할 수 있다.

다음은 ProObjectRequest를 구성하는 변수에 대한 설명이다. 필요한 경우 서비스 내에서 정보를 얻어 사용하고, 서비스를 요청하는 경우 변경해서 사용한다.

변수 이름 설명

sequence

현재 수행 중인 서비스의 채번된 넘버이다.

statusCode

서비스에서 반환할 http status code를 설정한다.

설정하지 않는 경우 서비스에 성공하는 경우 200, 실패하는 경우 500이나 404(애플리케이션/서비스그룹/서비스를 찾지 못한 경우) 코드가 반환된다. 추가로 on Error 서비스의 경우 202가 반환된다.

globalDeployVersion

  • 서비스 수행 중 : 현재 수행 중인 서비스의 배포 버전을 설정한다.

  • 서비스 요청 시 : 요청할 서비스의 배포 버전을 설정한다.

application

현재 수행 중인 애플리케이션의 이름이다.

service

현재 수행 중인 서비스의 이름이다.

channelType

서비스를 요청한 채널의 타입이다.

다음의 3가지 타입 중 하나를 값으로 갖는다.

  • HTTP : 일반 HTTP로 전달된 요청이다.

  • TCP : 사용자 정의 채널 핸들러를 통해 요청이 들어온 경우 설정된다.

  • PROOBJECT : 다른 ProObject 런타임으로부터 아웃바운드 요청이 들어온 경우 설정된다.

transferKey

서비스를 수행하기 전에 파일을 업로드받은 경우 파일을 전송할 때 사용된 키이다(일반적인 경우 사용하지 않는다).

uploadFiles

  • 서비스 수행 중 : 서비스가 수행되기 전 업로드받은 파일들의 목록이다.

  • 서비스 요청 시 : 서비스를 요청할 때 업로드시킬 파일들의 목록이다.

uploadBaseDirectory

  • 서비스 요청 시 : 서비스를 요청할 때 업로드할 파일들의 기본 디렉터리이다.

downloadDirectory

  • 서비스 요청 시 : 요청한 서비스로부터 파일을 다운로드받는 경우 다운로드한 파일들이 저장될 디렉터리를 설정한다.

downloadBaseDirectory

  • 서비스 수행 중 : 서비스를 수행하기 전 다운로드받은 파일들의 기본 디렉터리이다.

downloadFiles

  • 서비스 수행 중 : 서비스의 응답을 돌려준 후 클라이언트가 다운로드받아야 하는 파일들의 목록이다.

  • 서비스 요청 시 : 요청한 서비스로부터 다운로드된 파일들의 목록을 나타낸다.

requestMessageType

  • 서비스 수행 중 : 서비스를 요청할 때 받은 메시지의 타입이다.

  • 서비스 요청 시 : 서비스를 요청할 때 보낼 메시지의 타입을 설정한다.

responseMessageType

  • 서비스 수행 중 : 서비스를 종료할 때 보낼 메시지의 타입이다.

  • 서비스 요청 시 : 서비스를 종료할 때 받을 메시지의 타입을 설정한다.

webHeader

  • 서비스 수행 중 : 서비스가 요청되었을 때 전달받은 HTTP 헤더들의 정보이다.

  • 서비스 요청 시 : 서비스를 요청할 때 함께 전달할 HTTP 헤더들의 정보를 설정한다.

webResponseHeader

  • 서비스 수행 중 : 서비스에 대한 응답을 돌려줄 때 HTTP 헤더로 전달할 정보이다.

  • 서비스 요청 시 : 서비스에 대한 응답이 돌아온 경우 요청했던 서비스에서 설정한 HTTP 헤더들의 정보를 나타낸다.

webLocale

  • 서비스 수행 중 : 서비스가 요청되었을 때 전달받은 웹에서 사용 중인 로케일 정보이다.

  • 서비스 요청 시 : 서비스를 요청할 때 함께 전달할 웹에서 사용 중인 로케일변 정보를 설정한다.

webLocales

  • 서비스 수행 중 : 서비스가 요청되었을 때 전달받은 웹에서 사용 가능한 모든 로케일 정보이다.

  • 서비스 요청 시 : 서비스를 요청할 때 함께 전달할 웹에서 사용 가능한 모든 로케일 정보를 설정한다.

header

서비스를 요청할 때 전달된 헤더 객체의 정보를 전달한다. 사용자 정의 헤더가 application.xml에 설정된 경우 사용자 정의 헤더가 이용된다.

  • 서비스 수행 중 : 전달받은 헤더를 나타내며, 응답 정보를 설정할 경우 서비스에 응답할 때 응답 정보가 함께 전달된다.

  • 서비스 요청 시 : 요청할 서비스에 전달할 헤더를 나타낸다.

uri

웹을 통해 서비스를 요청받은 경우 요청받은 URI를 나타낸다.

methodName

웹을 통해 서비스를 요청받은 경우 요청받은 Http 메소드 타입을 나타낸다.

restArray

웹을 통해 서비스를 요청받은 경우 URI에 나타난 REST API들이 Key Value가 번갈아가며 저장된다.

필요한 경우 서비스를 요청할 때 REST API에 해당하는 "Key : Value" 쌍을 추가로 설정하여 전달해줄 수도 있다. 단, 서비스를 요청할 때 추가한 REST API는 여기에 추가되지 않는다(되도록 사용하지 않도록 한다).

restMap

  • 서비스 수행 중 : 서비스를 요청받은 경우 함께 전달된 REST API들의 Key-Value 쌍을 나타낸다.

  • 서비스 요청 시 : 서비스를 요청할 때 함께 전달할 REST API들의 Key-Value 쌍을 설정한다.

queryStringMap

  • 서비스 수행 중 : 서비스를 요청받은 경우 함께 전달된 Query String들의 Key Value 집합을 나타낸다.

  • 서비스 요청 시 : 서비스를 요청할 때 함께 전달할 Query String들의 Key Value 집합을 설정한다.

rawMessage

  • 서비스 요청 시 : 전달받은 전문을 byte[] 형태로 저장한다.

content

  • 서비스 요청 시 : 전달받은 입력의 내용을 String 형태로 저장한다.

cookie

  • 서비스 요청 시 : 전달받은 Cookie들을 나타낸다.

newCookie

서비스 로직 처리 중에 새로 발급한 쿠키들을 나타낸다.

위의 변수들은 변수 이름에 단순 Getter/Setter를 이용하지만 newCookie는 sendCookie()로 발급하고, getCookieToSend()로 발급한 쿠키의 리스트를 얻을 수 있다.

서비스를 연동하는 경우 저장된 REST API, QueryString, Header는 특별한 설정이 없을 경우 최초로 호출된 서비스에서 설정한 내용이 기본값으로 입력되어 전달된다.

6.3. 서비스 응답 정보 변경

ProObject는 기본적으로 사용자에게 응답을 돌려줄 때 HTTP 상태 코드, 응답 코드와 응답 메시지, 응답 상세 메시지를 설정할 수 있다.

  • HTTP 상태 코드(Status Code)

    HTTP 즉 웹을 통해 호출한 경우에만 유용한 정보로써 HTTP의 표준으로 정의된 상태 코드를 말한다. 일반적으로는 200이 사용되며, 오류가 있는 경우 각각 해당 오류에 맞는 코드가 사용된다. ProObject에서는 일반적 오류인 경우는 500, 애플리케이션이나 서비스 그룹, 서비스를 찾지 못한 경우에는 404 코드가 반환된다.

    HTTP 상태 코드는 ProObjectRequest 객체의 statusCode를 설정할 수 있다.

  • 응답 코드(Response Code)

    HTTP가 아닌 다른 프로토콜의 경우에는 HTTP 상태 코드라는 개념이 존재하지 않기 때문에 응답이 정상인지 아닌지를 판별하는 것이 어렵다. ProObject에서는 응답을 줄 때 현재 응답이 정상인지 아니면 오류인지를 구별하기 위해 기본적으로 정상(NON-0200)과 오류 코드를 반환한다. 필요한 경우에는 설정을 변경하여 원하는 응답 코드를 반환하도록 할 수 있다.

  • 응답 메시지(Response Message)

    응답 코드에 따라 원하는 메시지를 사용자에게 출력하거나 클라이언트에 추가적인 정보를 반환하는 경우에 사용한다. 개발자를 위한 영역으로 ProObject에서는 어떠한 메시지도 출력하지 않는다.

  • 응답 상세 메시지(Response Detail Message)

    응답 메시지의 내용을 보강하기 위해 사용하는 메시지이다. 개발자를 위한 영역으로 ProObject에서는 어떠한 메시지도 출력하지 않는다.

개발자는 응답 코드나 응답 메시지들을 설정하기 위해서는 메시지 번들의 내용을 참고해서 설정하거나 메시지 번들의 내용을 참조하지 않고 상수로 정보를 설정하는 것이 가능하다.

본 절에서는 HTTP 상태 코드와 응답 코드, 응답 메시지, 응답 상세 메시지를에 대한 용어 정의와 설정하는 방법에 대해 설명한다.

서비스 응답 정보를 변경한 이후 서비스를 종료하기 전에 오류가 발생한 경우 설정한 서비스 응답 정보는 모두 초기화된다.

6.3.1. 메시지 번들을 이용한 응답 정보 설정

메시지 번들을 이용하여 응답 정보들을 전달할 때에는 ProObjectRequest 객체를 얻은 이후 해당 객체에 응답 정보를 설정한다. 이 방법을 이용할 때에는 서비스 그룹이나 애플리케이션의 메시지 번들이 이용된다.

  • 응답 코드와 응답 메시지 설정

    해당 방법을 이용할 때에는 메시지 번들의 키(Key)가 응답 코드(Response Code)와 동일해야 한다.

    ProObjectReqeust request = requestContext.getRequest();
    request.setResposneCode(responseCode, messageBundleParam1, messageBundleParam2);
  • 응답 상세 메시지 설정

    응답 상세 메시지를 설정할 때에는 메시지 번들의 키가 미리 정의되어 있어야 한다.

    ProObjectReqeust request = requestContext.getRequest();
    request.setResponseDetailMsgCode(messageBundleKey, messageBundleParam1, messageBundleParam2);

6.3.2. 응답 정보 상수 설정

메시지 번들을 이용하지 않고 상수를 설정하는 경우 ProObjectRequest 객체 내의 Header 객체를 얻어 응답 정보를 설정할 수 있다.

  • 응답 코드 설정

    ProObjectReqeust request = requestContext.getRequest();
    Header header = (Header)request.getHeader();
    header.setResponsecode(responseCode);
  • 응답 메시지 설정

    ProObjectReqeust request = requestContext.getRequest();
    Header header = (Header)request.getHeader();
    header.setResponsecMsg(message);
  • 응답 상세 메시지 설정

    응답 상세 메시지를 설정할 때에는 메시지 번들의 키가 미리 정의되어 있어야 한다.

    ProObjectReqeust request = requestContext.getRequest();
    Header header = (Header)request.getHeader();
    header.setResponseMsgDetails(detailMsg);

7. 서비스 설정

본 절에서는 서비스를 설정하는 방법에 대해서 설명한다.

7.1. 메타 설정

서비스는 다음 디렉터리의 servicegroup.xml에 등록한다. 디렉터리 구조에 대한 자세한 설명은 애플리케이션 디렉터리 구조를 참고한다.

${SVG_HOME}/config/servicegroup.xml

서비스 그룹을 새로 생성할 때에는 반드시 서비스 그룹 정보(서비스 그룹의 이름과 서비스 그룹의 공통 부분)를 servicegroup.xml에 설정해야 한다. 해당 파일의 자세한 내용은 서비스 그룹을 참고한다.

서비스를 등록할 때에는 서비스 그룹의 네임스페이스의 맞추어 작성한다. 다음의 예제는 서비스 그룹 네임스페이스가 ns17인 경우 설정한 예이다.

<ns17:service-object>
 <ns17:name>HelloWorld</ns17:name>
 <ns17:class-name>com.tmax.proobject.example.service.HelloWorld</ns17:class-name>
 <ns17:input-dto>com.tmax.proobject.example.dto.UserServiceInput</ns17:input-dto>
 <ns17:output-dto>com.tmax.proobject.example.dto.UserServiceOutput</ns17:output-dto>
 <ns17:service-type>COMPLEX</ns17:service-type>
</ns17:service-object>

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

항목 설명

<service>

서비스의 이름을 나타낸다. 서비스를 호출할 때에는 반드시 설정된 이름을 사용한다. 서비스 이름은 유일한 값이어야 한다.

<class-name>

서비스 오브젝트의 클래스를 설정한다. 클래스를 설정할 때에는 반드시 패키지까지 포함해서 작성한다. Outbound Service가 아닌 경우에는 반드시 클래스를 설정해야 한다.

<input-dto>

서비스의 입력으로 사용할 데이터 오브젝트의 클래스를 설정한다. 클래스를 설정할 때에는 반드시 패키지까지 포함해서 작성해야 하고, 서비스 오브젝트 클래스에서 설정한 입력 데이터 오브젝트 타입과 일치해야 한다.

<output-dto>

서비스의 출력으로 사용할 데이터 오브젝트의 클래스를 설정한다. 클래스를 설정할 때에는 반드시 패키지까지 포함해서 작성해야 하고, 서비스 오브젝트 클래스에서 설정한 입력 데이터 오브젝트 타입과 일치해야 한다.

<service-type>

서비스의 타입을 설정한다. 다음 중 작성된 서비스 오브젝트의 클래스에 맞는 타입을 설정한다.

  • COMPLEX : 서비스 그룹의 워커 스레드에서 수행되는 일반적인 서비스의 형태로, ServiceObject를 상속받아 Complex Service로 구현한 경우에 설정한다.

  • EVENT : 싱글 스레드인 이벤트 처리 스레드에서 수행되는 서비스로, EventServiceObject를 상속받아 Event Service로 구현한 경우에 설정한다.

  • OUTBOUND : 외부의 다른 서버와 통신하는 경우에 설정하며, 유일하게 클래스를 설정하지 않는다.

<post-initializer>

서비스 그룹의 초기화가 끝난 직후 수행해야 하는 로직이 담긴 클래스를 설정한다. 클래스는 반드시 com.tmax.proobject.model.initializer.ServiceGroupPostInitializer 인터페이스를 상속받아 구현해야 한다.

생성한 바이너리는 서비스 그룹 공용 라이브러리 디렉터리에 배포되어야 한다. 바이너리를 배포한 후에는 이 요소에 패키지 이름을 포함한 클래스 이름을 설정한다.

<pre-processor>

서비스가 수행되기 앞서 먼저 처리되어야 할 로직이 담긴 선처리 클래스를 설정한다. 클래스는 반드시 com.tmax.proobject.model.service.PreProcessor 인터페이스를 상속받아 구현해야 한다.

생성한 바이너리는 서비스 그룹 공용 라이브러리 디렉터리에 배포되어야 한다. 바이너리 배포한 후에는 이 요소에 패키지 이름을 포함한 클래스 이름을 설정한다.

<outbound-ip>

Outbound Service를 사용할 경우에 설정하는 옵션으로 서비스를 요청할 외부의 IP를 나타낸다.

RequestContext를 이용하여 IP를 설정하지 않은 경우 여기에 설정된 IP로 요청을 보낸다.

<outbound-port>

Outbound Service를 사용할 경우에 설정하는 옵션으로 서비스를 요청할 외부의 포트를 나타낸다.

RequestContext를 이용하여 포트를 설정하지 않은 경우 여기에 설정된 포트로 요청을 보낸다.

<outbound-file-port>

Outbound Service를 사용할 경우에 설정하는 옵션으로 서비스를 요청할 경우 파일 송수신이 포함되었을 때 사용할 외부 서버의 파일 포트를 나타낸다. RequestContext를 이용하여 파일 포트가 설정되지 않은 경우에는 여기에 설정된 포트로 파일 송수신을 요청하게 된다.

<outbound-target-service>

ApplicationName.ServiceGroupName.ServiceName을 설정한다.

호출할 서비스의 이름이 ServiceManager를 통해 호출한 Caller의 이름과 동일하지 않은 경우에 사용한다. 이는 단순히 서비스 이름뿐만이 아니라 애플리케이션, 서비스 그룹이 다른 서비스를 요청할 때에도 유용하게 사용된다.

예를 들어 카드사에서 금융권의 서비스를 호출할 때 카드사에서는 "withdrawXXBank"라는 이름의 서비스를 호출하지만 실제 금융권에서는 "withdraw"라는 이름의 서비스가 존재할 때 이용된다.

(기본값: ServiceManager API를 이용하여 전달된 서비스 이름)

<outbound-message>

Outbound Service를 사용할 경우에 설정하는 옵션으로, 서비스를 요청할 때 데이터 오브젝트를 어떤 형식으로 전송할지를 설정한다. 단, 이 옵션은 ProObject 서버에 아웃바운드로 서비스를 호출할 때에만 적용되며, 다른 ProObject가 아닌 다른 서버와의 연동이 필요한 경우에는 RequestContext 의 ProObjectRequest 객체를 변경하여 전송 타입을 설정해야 한다.

다음 메시지 타입 중 하나를 설정한다.

  • JSON : JSON 형태의 네트워크 메시지와 객체가 상호 변환된다. (기본값)

  • XML : XML 형태의 네트워크 메시지와 객체가 상호 변환된다.

  • FLD : 설정길이 형태의 네트워크 메시지와 객체가 상호 변환된다.

  • DELIMITER : 구분자 형태의 네트워크 메시지와 객체가 상호 변환된다.

  • NONE : 객체를 변환하지 않는다.

<outbound-channel-event-handler>

channel.xml에 등록한 채널 핸들러의 이름을 설정한다.

Outbound Service를 사용할 경우에 설정하는 옵션으로 서비스를 요청할 때 어떤 채널 이벤트 핸들러를 사용할지를 설정한다.

설정하지 않을 경우 ProObject 간의 통신으로 간주하고, ProObject 간에 통신하는데 사용하는 채널 이벤트 핸들러가 사용된다. 클래스 이름을 설정하는 것이 아니라 channel.xml에 작성한 채널 이벤트 핸들러의 이름을 설정해야 한다. (기본값: 없음)

<transaction>

서비스 간의 트랜잭션 연동에 관련한 설정을 하는 옵션이다.

설정하지 않을 경우 서비스에서 사용된 모든 입출력은 하나의 트랜잭션으로 간주하여 처리되며, 다른 서비스와의 트랜잭션이 함께 연동되어 처리되지 않는다. 자세한 설명은 "트랜잭션 설정"을 참고한다.

(기본값: 없음 (SPLIT으로 간주됨))

<on-error-service>

서비스 중 오류가 발생하여 서비스 오브젝트의 범위 바깥으로 예외가 던져진 경우, 해당 오류를 처리할 수 있는 별도의 서비스를 설정하는 기능으로 "오류 처리 서비스"라는 이름으로 명명된다.

오류 처리 서비스는 주로 취소 거래를 처리하는데 사용되며 이미 데이터가 데이터베이스에 삽입되었거나, 외부로 전달된 거래 요청이 결정적인(Deterministic) 처리를 진행하게 된 경우 이를 취소시키기 위해 사용한다. 예를 들어 카드사에 대한 승인 취소 등이 하나의 예가 될 수 있으며, 해당 항목에 취소할 거래 이름을 설정할 수 있다.

설정된 취소 거래의 입력 타입은 설정이 불가능하며 반드시 아래와 같은 입력 형식을 따라야만 한다.

  • 입력 : com.tmax.proobject.engine.system.dto.error.ErrorDO

    • errorServiceDO : 예외가 발생한 상황에서 전달되었던 서비스의 입력 DataObject 객체가 전달된다.

    • errorInfoDO : 발생된 예외에 대한 정보들이 전달되며 오류 이름과 반환 코드, StackTrace 등이 담겨 있다.

    • errorServiceFullName : 예외가 발생한 서비스 이름이 전달된다.

오류 처리 서비스의 출력은 자유로운 편으로 오류 처리 서비스에서 응답을 반환한 경우 서비스가 정상적으로 종료된 것처럼 응답을 반환한다.

<is-error-service>

해당 서비스 오브젝트가 오류 처리 서비스인지 여부를 설정한다.

이 옵션이 활성화되어 있지 않으면 오류 처리 서비스인지 여부를 엔진이 정상적으로 판단하지 못하여 각종 문제가 발생할 수 있으므로, 오류 처리 서비스를 설정한 경우에는 반드시 값을 설정해주어야 한다. (기본값: false, 범위: true | false)

<marshaller>

서비스의 입력으로 전달되는 byte[]를 Object로 어떻게 Marshaling할지 여부를 설정한다.

기본값은 SYSTEM으로 ProObject가 Marshal을 자동으로 처리하나, 이외의 옵션을 설정하였다면 Marshal에 대한 책임은 서비스 개발자에게 귀속된다. (기본값: SYSTEM, 범위: SYSTEM | MANUAL)

<unmarshaller>

서비스의 출력으로 전달되는 Object를 byte[]로 어떻게 Unmarshaling할지 여부를 설정한다.

기본값은 SYSTEM으로 ProObject가 Unmarshal을 자동으로 처리하나, 이외의 옵션을 설정하였다면 Unmarshal에 대한 책임은 서비스 개발자에게 귀속된다. (기본값: SYSTEM, 범위: SYSTEM | MANUAL)

트랜잭션 설정

트랜잭션은 서비스별로 설정되며, 서비스의 요소 하위의 <transaction>에 추가해서 사용한다.

태그 설명

<datasource>

트랜잭션에 사용할 데이터소스들을 설정한다.

로컬 트랜잭션(Local Transaction)의 경우에는 특별히 설정하지 않아도 무방하나, 글로벌 트랜잭션(Global Transaction)으로 사용하려면 반드시 사용할 데이터소스의 이름을 모두 설정해야 한다.

수행 예정인 글로벌 트랜잭션에 데이터소스 이름이 누락된 경우에는 해당 데이터소스가 트랜잭션으로 함께 수행되지 않으므로 주의해야 한다. 작성되는 데이터소스에 대한 설정은 데이터 오브젝트/데이터 오브젝트 팩토리 개발을 참고한다.

데이터소스가 설정되었다면 다음과 같이 'tibero6’와 'oracle’에 대한 데이터소스를 설정할 수 있다.

  • <ns17:datasource>
        <ns17:datasource>tibero6</ns17:datasource>
        <ns17:datasource>oracle</ns17:datasource>
    </ns17:datasource>

이때 특정 데이터소스는 트랜잭션을 분리해 자동 반영(autocommit) 기능을 사용하려면 데이터소스에 autocommit 속성을 설정한다. autocommit으로 설정된 데이터소스는 같은 트랜잭션에 묶이지 않으며, 서비스의 성공/실패 여부와는 상관없이 항상 데이터가 반영된다.

  • <ns17:datasource>
        <ns17:datasource autocommit="true">tibero6_auto</ns17:datasource>
        <ns17:datasource>tibero6</ns17:datasource>
        <ns17:datasource>oracle</ns17:datasource>
    </ns17:datasource>

위와 같이 설정된 경우 tibero6_auto 데이터소스는 반드시 커밋되며, 서비스의 실패 여부와 상관없이 항상 데이터의 변경 내역이 반영된다. 반면 tibero6와 oracle은 서비스가 실패한 경우 롤백 처리되며, 성공하면 같이 커밋된다.

<type>

트랜잭션의 종류를 설정한다.

트랜잭션의 종류는 두 가지가 있으며, 필요한 경우에 한해서만 설정해서 사용한다.

  • GLOBAL : 서비스에서 호출한 서비스들과 트랜잭션을 함께 적용하는 동작 방식이다. 현재 서비스가 실패한 경우 호출한 서비스들이 변경한 내역 또한 함께 롤백된다. 반면, 현재 서비스가 성공한 경우에는 호출한 서비스들의 실패 여부는 상관없이 모두 커밋된다. 다만 acall_noreply의 경우에는 GLOBAL로 설정해도 같은 트랜잭션으로 적용되지 않으므로 주의해야 한다.

  • LOCAL : 자신이 호출한 서비스들과는 별개로 트랜잭션을 적용하는 동작 방식이다. 호출한 서비스는 자신의 성공/여부에 따라 독자적으로 롤백과 커밋을 처리하며, 자신의 서비스도 다른 서비스의 트랜잭션에 영향을 주지 않는다.

7.2. 런타임 설정

서비스별 설정 정보는 다음 디렉터리의 파일에 등록한다. 디렉터리 구조에 대한 자세한 설명은 애플리케이션 디렉터리 구조를 참고한다.

  • 기본 런타임 설정 파일 위치

    ${SVG_HOME}/config/servers/{SERVER_NAME}/servicegroup.properties
  • 통합 런타임 설정 파일 위치

    ${SVG_HOME}/config/servicegroup.properties

{SERVER_NAME} 항목에는 proobject.xml 혹은 환경변수로 등록한 서버의 이름을 사용한다. config 디렉터리 하위에 servicegroup.properties이 존재하는 경우에는 해당 파일만 사용하며, 해당 파일이 없는 경우에는 자신의 서버 이름에 해당되는 디렉터리에서 servicegroup.properties를 찾아 사용한다.

설정할 내용이 없을 경우에는 해당 파일을 작성하지 않아도 되며 파일을 작성하지 않는 경우에는 ProObject가 새로 생성한다. 파일이 비어있다면 런타임 엔진은 기본값으로 해당 내용이 설정된 것으로 처리된다.

설정한 옵션은 서비스 그룹별 설정이나 애플리케이션 설정보다 우선시되므로 주의해서 사용해야 한다.

SERVICE_{SERVICE_NAME}_TIMEOUT = timeout_value
SERVICE_{SERVICE_NAME}_LOG_LEVEL = [SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST]
SERVICE_{SERVICE_NAME}_BEFORE_IMAGE_ENABLE = [true|false]
항목 설명

SERVICE_{SERVICE_NAME}_TIMEOUT

서비스의 타임아웃 시간을 설정한다.

설정한 시간 내로 서비스가 끝나지 않은 경우에는 ServiceTimeoutException이 발생한다. (단위: ms)

SERVICE_{SERVICE_NAME}_LOG_LEVEL

서비스의 로그 레벨을 설정한다.

특정 서비스에 대해서만 로그 레벨을 변경하는 경우 설정한다.

아래로 갈수록 더 상세한 로그가 출력된다.

  • SEVERE

  • WARNING

  • INFO (기본값)

  • CONFIG

  • FINE

  • FINER

  • FINEST

SERVICE_{SERVICE_NAME}_BEFORE_IMAGE_ENABLE

서비스의 이미지 로그 기능의 사용 여부를 설정한다.

8. HTTP를 이용한 서비스 호출

본 절에서는 HTTP를 이용해서 서비스를 호출하는 방법을 설명한다.

8.1. 서비스 호출

ProObject는 기본적으로 HTTP를 이용한 서비스 호출이 가능하다.

ProObject는 HTTP를 이용하여 서비스를 요청할 때에 다음의 2가지 방식으로 서비스를 요청한다.

  • RESTful API를 이용한 서비스 호출 방식

    URI와 HTTP 메소드를 조합해서 서비스를 인식하는 방식이다.

  • HTTP Payload를 이용한 서비스 호출 방식

    HTTP Payload 내에 ProObject의 프로토콜에 맞추어 서비스의 이름을 설정하는 방식이다.

URI를 이용해 서비스를 제어하는 경우에는 RESTful API를 이용하며, 보안을 위해 URI 자체를 숨기는 경우에는 HTTP Payload를 이용하여 서비스를 호출한다.

8.1.1. RESTful API를 이용한 서비스 호출

기본적으로 ProObject의 서비스를 HTTP로 호출할 때에는 HTTP 메소드가 원하는 리소스에 대한 액션으로 정의된다. 따라서 HTTP를 통해 서비스를 호출할 때에는 처리할 동작에 따라 HTTP 메소드를 신중하게 선택해야 한다. 메소드가 맞지 않는 경우에는 동작이 다른 것으로 처리되어 올바른 서비스로 인식되지 못할 수도 있다.

ProObject에서는 URI의 위치에 따라 애플리케이션 이름과 서비스 그룹, 서비스의 리소스 이름을 각각 인식하게 된다. 애플리케이션 이름은 URI의 첫 번째 위치를 서비스 그룹의 이름은 URI의 두 번째에 위치한 값으로 인식된다. 서비스 이름은 제어할 리소스와 처리할 행동이 조합되어 인식되며, URI의 마지막 RESTful API Key와 HTTP의 메소드를 조합하여 인식된다.

다음은 test 애플리케이션의 example 서비스 그룹에 속한 EmployeeGet 서비스를 호출하는 경우 사용하는 양식이다.

figure service restapi
RESTful API 규칙 예제
구분 설명

서버에 요청을 보내기 위한 방식(서비스 이름 꼬리말)이다.

메소드는 다음의 항목을 사용한다.

  • GET : 리소스들의 목록이나 리소스 하나의 내용을 표시(read)

  • POST : 리소스를 생성(create)

  • PUT : 리소스를 수정(update/replace)

  • DELETE : 리소스를 삭제(delete)

  • PATCH : 리소스를 수정(update/modify)

애플리케이션 이름을 설정한다.

서비스 그룹 이름을 설정한다.

REST API Key/Value 쌍을 설정한다. (생략 가능)

리소스 이름을 설정한다. (서비스 이름 머리말)

REST API Value를 설정한다. (생략 가능)

ProObject에서는 리소스 이름을 서비스 이름의 "머릿말"로 표현하며 동작을 서비스 이름의 "꼬리말"로 표현한다.

위의 메소드만으로 표현하기 어려운 행동을 정의는 경우 'action’이라는 이름의 쿼리스트링(QueryString)을 이용해 행동을 별도로 정의할 수 있다.

다음은 test 애플리케이션의 example 서비스 그룹에 속한 'EmployeePromotion’을 호출하는 양식의 예이다.

GET   /test/example/…/…/Employee?action=Promotion

RESTful API를 이용해 서비스를 호출하는 경우에는 ProObject JSON Protocol이나 Multipart를 이용해 서비스의 입력값을 전달하는 것이 가능하다. 자세한 내용은 서비스 입력 전달을 참고한다.

8.1.2. HTTP Payload를 이용한 서비스 호출

HTTP의 Payload를 이용하기 위해서는 위의 RESTful API 형식을 사용하지 않는 것으로 ProObject JSON Protocol 또는 ProObject XML Protocol을 사용해야 한다. JSON은 ProObject JSON Protocol, XML은 ProObject XML Protocol의 설명을 참고한다.

다음은 각 형식별 HTTP Payload의 전송 양식에 대한 설명이다.

  • JSON 형식

    JSON 형식을 따르는 HTTP Payload의 경우에는 반드시 아래와 같은 제약 조건을 만족하는 JSON 양식을 전송해야 한다.

    • Content-Type에 applicaton/json을 표기해서 JSON으로 메시지를 보낼 것임을 명시해 주어야 한다.

    • Payload는 ProObject JSON Protocol 형식에 맞추어 요청을 전달해야 한다.

      {
          "header" : {
              "service" : "test.example.EmployeeGet"
          },
          " dto" : {
              // 서비스 입력
          }
      }
  • XML 형식

    XML 형식을 따르는 HTTP Payload의 경우에는 반드시 아래와 같은 제약 조건을 만족하는 XML 양식을 전송해야 한다.

    • Content-Type에 text/xml을 표기해서 XML으로 메시지를 보낼 것임을 명시해 주어야 한다.

    • Payload는 ProObject XML Protocol 형식에 맞추어 요청을 전달해야 한다.

      <ProObjectHttpProtocol>
        <header>
          <service>test.example.EployeeGet</service>
        </header>
        <dto>
          // 서비스 입력
        </dto>
      </ProObjectHttpProtocol>

8.2. 서비스 입력 전달

본 절에서는 호출할 서비스의 입력을 HTTP를 통해 전달하는 방법에 대해서 설명한다.

8.2.1. ProObject JSON Protocol

ProObject JSON Protocol을 사용하는 경우에는 일반적인 JSON 형식에 맞추어 데이터를 전달한다.

다음과 같이 "dto"라는 JSON 필드(Field) 내에 전달할 객체를 입력한다. 단, ProObject JSON Protocol의 경우 서비스 입력으로 배열을 전달하는 것은 허용하지 않는다.

{
    " dto" : {
        // 서비스 입력
        "abcd" : "ABCD"
    }
}

8.2.2. Multipart

ProObject JSON Protocol은 일반적으로 간단한 서비스의 입력을 전달하기에는 유용하나 웹의 Form을 이용한 요청을 처리하거나 파일 업로드의 작업에는 부적합하다. ProObject는 form-data와 mixed 두 가지 타입의 멀티 파트만을 지원한다.

  • form-data

    Form을 이용하여 요청을 할 경우 파일을 제외한 모든 데이터는 서비스 입력 데이터 오브젝트의 필드로 간주되어 처리된다. 따라서 Form을 이용할 경우에는 모든 HTML의 Input 태그가 데이터 오브젝트의 필드에 대응되어 처리된다고 가정한다. 단, 파일을 함께 전송할 경우에는 일반적인 파일 업로드와 동일하게 간주되어 처리되며 서비스 내에서 요청 정보(RequestContext)에서 업로드된 파일에 접근해 처리할 수 있다.

  • mixed

    mixed를 이용한 요청의 경우에는 여러 가지의 요청 정보가 모두 섞인 것으로 간주된다. 이때 mixed는 "dto"라는 이름을 지니는 Part를 서비스 입력의 데이터 오브젝트로 간주하며, 그 외의 모든 Part 들은 무시된다. 단, 파일을 함께 전송할 경우에는 일반적인 파일 업로드와 동일하게 간주되며 서비스 내에서 요청 정보(RequestContext)에서 업로드된 파일에 접근해 처리할 수 있다.

8.2.3. ProObject XML Protocol

ProObject XML Protocol을 사용하는 경우에는 일반적인 XML 태그 형식에 맞추어 데이터를 전달한다. XML Document Element를 생략할 수 있다.

루트 엘리먼트로 <ProObjectHttpProtocol> 을 명시한 뒤 <dto> 엘리먼트의 하위 엘리먼트로 각 dto 필드에 해당하는 값을 입력한다. 단, 배열 형태의 값을 전달하는 것은 허용하지 않는다.

<ProObjectHttpProtocol>
  <dto>
    // 서비스 입력
    <key>value</key>
  </dto>
<ProObejctHttpProtocol>

8.2.4. TEXT/PLAIN

대부분의 서비스들은 ProObject JSON Protocol을 통해 호출하는 것이 가능하다. 하지만 경우에 따라서 text/plain type의 요청을 보내거나 응답을 받아야 하는 경우가 있다.

ProObject에서는 Content-Type을 text/plain으로 표기한 다음 body에 평문을 작성해서 서비스를 호출할 수 있도록 지원한다. 단, 호출할 서비스의 커스텀 바디 파서(custom-body-parser)가 필수적으로 설정되어 있어야 한다. 왜냐하면 text/plain인 경우에는 어떠한 형태로 요청이 들어오고 응답이 나가야하는지 엔진에서 알 수 없기 때문이다.

먼저 아래의 예제를 참고해서 BodyParser를 구현해야 한다.

package com.tmax.proobject.test.runtime.application.testOther.servicegroup.main.lib;

import java.nio.charset.Charset;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.tmax.promapper.engine.base.Message;
import com.tmax.proobject.core.service.ServiceName;
import com.tmax.proobject.model.promapper.ProMapperMessageType;
import com.tmax.proobject.network.protocol.proobject.request.ProObjectHttpProtocol;
import com.tmax.proobject.network.protocol.prooobject.parser.BodyParser;

/**
 * APP, SG 단위로 CustomParser를 달 수 있다.
 * 이 때 특정 서비스에만 parser를 적용하거나 특정 서비스를 제외하고 싶은 경우가 있다면, serviceName을 가지고 제어하면 된다.
 * parseResponseBody 메소드같은 경우는 ProObjectHttpProtocol의 값을  messageType에 맞게 커스터마이징 한 다음에
 * message Object로 marshal을 해야 한다.
 */
public class CustomParser implements BodyParser {
    @Override
    public ProObjectHttpProtocol parseRequestBody(byte[] inputStream, ServiceName serviceName, ProMapperMessageType messageType, String charset) {
        ProObjectHttpProtocol protocol = null;

        switch(messageType) {
        case NONE :
            //* text/plain인 경우에는 dto를 default message type인 json 형태로 만들어줘야 한다.
            protocol            = new ProObjectHttpProtocol();
            String text         = new String(inputStream, Charset.forName(charset));
            String[] value      = text.split(",");
            if(!value[0].equals("TEST")) {
                return protocol;
            }
            String jsonString   = "{\"value\":\"" + value[0] + "\", \"desc\":\"" + value[1] + "\"}";
            protocol.setDto(jsonString);
            break;
        default :
            break;
        }
        return protocol;
    }

    @Override
    public byte[] parseResponseBody(ProObjectHttpProtocol protocol, ServiceName serviceName, ProMapperMessageType messageType, String charset) {
        String      dtoJson     = protocol.getDto();
        JsonObject  jsonObject  = null;
        Message     message     = null;

        switch (messageType) {
        case NONE:
            try {
                jsonObject = new JsonParser().parse(dtoJson).getAsJsonObject();
            } catch(Exception e) {
                message = new ProObjectHttpProtocolMsgJson();
                message.setEncoding(charset);
                try {
                    return message.marshal(protocol);
                } catch (Exception e1) {
                    return null;
                }
            }
            StringBuilder result = new StringBuilder().append("CustomParser, ");
            if(serviceName.getFullName().equals("testOther.main.BodyParserTestCall")) {
                result.append("PARSED, ")
                .append(jsonObject.get("value").getAsString())
                .append(", ")
                .append(jsonObject.get("desc").getAsString());
            } else {
                result.append("NOTPARSED");
            }
            return result.toString().getBytes(Charset.forName(charset));
        default:
            break;
        }
    }
}

기본적으로 서비스의 요청은 엔진 내부에서 알아서 ProObjectHttpProtocol 이라는 객체를 만들어서 요청을 처리한다. 하지만 text/plain type은 위에서 언급한 이유 때문에 적절하게 ProObjectHttpProtocol 객체를 만들 수 없다.

따라서 커스텀 바디 파서 내에서 적절하게 파싱을 처리해야 한다. parseRequestBody 내에서는 요청으로 들어온 평문을 가지고 ProObjectHttpProtocol 객체를 파싱해서 반환하는 로직을 작성해야 하며, 응답을 내보낼 때는 반대로 엔진으로 전달 받은 ProObjectHttpProtocol 객체로부터 평문을 만들어서 반환하는 로직을 작성해야 한다. 커스텀 바디 파서의 설정은 애플리케이션 설정 또는 서비스 그룹 설정을 참고한다.

9. 서비스 편의 기능 사용

ProObject 런타임에서는 각종 기능들을 편리하게 이용할 수 있도록 다양한 기능들을 제공하고 있다. 본 절에서는 서비스 개발에 용이하게 사용할 수 있는 각종 유틸리티 기능에 대해서 설명한다.

9.1. 서비스 로거

ProObject에서는 Complex 로거에 한해서만 로그를 제공하고 있다. 서비스 수행 도중 로거는 다음과 같이 코드를 작성하면 Complex 로거를 얻어올 수 있다.

ProObjectLogger logger = ServiceLogger.getLogger();

로거를 얻어온 이후에는 JDK에서 기본으로 제공하는 Logger와 동일한 방법으로 사용한다. 제공하는 로그 레벨은 SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST로 필요한 레벨에 맞춰 로그를 작성한다. 기본 로그 레벨은 INFO이다.

9.2. 다국어 로거

다국어로 메시지를 출력할 때에는 언어마다 어순이 다르기 때문에 StringBuilder를 이용하거나, 더하기 연산자를 이용하여 처리하기가 어렵다. 이를 해결하기 위해 ProObject 로거는 로그의 내용 안에 매개변수의 순번을 통해 파라미터를 바인딩할 수 있는 기능이 제공된다.

아래와 같이 코드를 작성했다고 가정해보자.

ProObjectLogger logger = ServiceLogger.getLogger();
logger.info("Binding {1}, {0}, {2}", "1", 2, 3.0);

위와 같이 작성한 경우 {NUMBER}는 매개변수의 순번을 나타낸다. 따라서 {1}은 두 번째 매개변수인 2와 바인딩되고, {0}은 첫 번째 매개변수인 "1"과, {2}는 세 번째 인자인 3.0과 바인딩된다. 그러므로 최종적으로는 아래와 같이 출력되므로 참고하여 사용해야 한다.

Binding 2, 1, 3.0