레이블이 Pattern인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Pattern인 게시물을 표시합니다. 모든 게시물 표시

2014년 11월 20일 목요일

Apache Camel을 이용한 대출 모집인 구현


이 글은 팁코 액티브엔터프라이즈(TIBCO ActiveEnterpise) 기반 통합 솔루션을 Apache Camel 기반 통합 솔루션으로 대체하는 방법을 설명합니다.

1. 들어가며

기업 통합 패턴(Enterprise Integration Patterns)의 "9장 사잇장: 복합 메시징"에서는 대출 모집인을 세가지 통합 도구를 각각 이용해 실질적인 통합 솔루션을 구현하는 예들을 설명한다. 첫째 예는 SOAP 프레임워크인 Apache Axis를 이용하고, 둘째 예는 마이크로소프트 MSMQ를 이용하고, 셋째 예는 상용 EAI 제품인 팁코 액티브엔터프라이즈(TIBCO ActiveEnterprise)를 이용한다. 이 책 9장에 등장하는 예들의 특징을 표로 보이면 다음과 같다.

구현
실행 방식
주소 지정
수집 전략
채널 형식
기술
1
동기
수신자 목록
채널
웹 서비스/SOAP
자바/아파치 액시스
2
비동기
수신자 목록
상관관계 식별자
메시지 큐
C#/마이크로소프트 MSMQ
3
동기,비동기
게시 구독 채널
상관관계 식별자
게시 구독
팁코 액티브엔터프라이즈

위 구현 예들은 중 "팁코 액티브엔터프라이즈를 이용한 구현"은 앞의 다른 예들과 달리 동기 요청과 비동기 메시징이 결합됐다는 점에서 차이가 있다. 즉 첫째 예는 SOAP을 이용한 동기 호출 기반의 솔루션이고, 둘째 예는 마이크로소프트 MSMQ를 이용한 비동기 메시징 솔루션인데, 셋째 예는 팁코 액티브엔터프라이즈를 이용해 요청은 동기로 수신하고 후속 처리는 비동기 메시징을 이용한다. 셋째 예의 메커니즘을 좀더 구체적으로 설명하면, "팁코 액티브엔터프라이즈를 이용한 구현" 예의 대출 모집인은 대출 클라이언트로부터 대출 견적을 동기로 요청 받아, 동기 호출로 신용 평가 기관에서 대출 클라이언트의 신용 점수를 얻고, 은행들에게 비동기 게시 구독 채널로 견적 요청을 브로드캐스트하고, 각 은행들로부터 대출 견적 응답을 비동기 메시지 큐(서브젝트)로 수집해, 최상 대출 견적을 대출 클라이언트에게 동기 응답으로 반환한다. 여기서 중요한 점은 상용 EAI 제품을 이용했다는 점이다.

그런데 기업 통합 패턴에서는 이런 메커니즘에 대한 해결책이 의도적이든 의도적이지 않든 부실하다. ("팁코 액티브엔터프라이즈를 이용한 구현"에서만 동기, 비동기의 결합에 대한 전체적인 예를 살펴볼 수 있다.) 기업 통합 패턴은, 비동기 메시징을 전제로 해결책들을 제시하는 책이므로, 비동기 인프라에서 동기 인프라에 접근하기 위한 패턴으로 서비스 액티베이터 패턴[EIP]을 설명하고, 애플리케이션이 비동기 인프라에 접근하기 위한 패턴으로는 메시징 게이트웨이 패턴[EIP]을 설명한다. 이와 같이 "팁코 액티브엔터프라이즈를 이용한 구현" 예를 벗어난 곳에서는 동기 인프라와 비동기 인프라가 결합된 종합적인 해결책을 비중 있게 다루지는 않는다. 상관관계 식별자 패턴[EIP]을 활용할 수도 있지만, 상관관계 식별자 패턴은 주로 비동기 요청의 응답을 상관시키기 위한 패턴이다.

이에 반해 팁코 액티브엔터프라이즈 같은 상용 EAI 제품들은 동기 호출과 비동기 응답을 상관시키는 종합적인 메커니즘을 내장한다. 이 기술은 간단한 것처럼 보이지만 실상은 그렇지 않다. 동기 호출은 응답을 반환할 때까지 동기 프로세스(스레드)가 (중간) 처리 결과와 처리 상태를 유지하는 반면, 비동기 메시징에서는 (중간) 처리 결과는 메시지와 필터 프로세스(스레드)들에 유지되고 처리 상태는 메시지가 위치한 채널로 관리한다. 즉 동기 호출과 비동기 메시징의 처리 결과와 상태 관리는 완전히 다른 방식이다.

그렇다면 동기 호출과 비동기 메시징을 결합하기 위해서는 상용 EAI 제품만 사용해야 하는 것일까? 그렇지 않다. 통합 프레임워크를 이용한다면 얼마든지 상용 EAI 제품처럼 동기 호출과 비동기 메시징을 결합할 수 있다. 그러므로 이 기능 때문에 상용 EAI 제품을 선택해야 한다는 기준은 전제하지 않아도 될 것이다.


2. 대출 모집인 솔루션 아키텍처 비교

기업 통합 패턴 9장의 "팁코 액티브엔터프라이즈를 이용한 구현" 예의 팁코 대출 모집인 솔루션 아키텍처는 다음과 같다.

위 아키텍처 그림에서 테스트 대출 클라이언트와 대출 모집인 사이와 대출 모집인과 신용 평가 기관 사이는 팁코의 동기 랑데부 전송을 사용하고, 대출 모집인과 은행들 사이는 비동기 랑데부 전송을 사용한다. 중간의 프로세스 관리자는 팁코의 시각적 도구와 스크립트와 메타 저장소 등을 이용하여 메시지 송수신 흐름과 처리 상태를 관리한다. 보다시피 동기 메커니즘과 비동기 메커니즘이 결합되어 있다. (이 솔루션 아키텍처는 기업 통합 패턴의 복합 메시징의 예를 보여주기 위해 모든 통신 인프라를 팁코 랑데부 통신으로 구성한 것이다. 실무적을 보면 대출 클라이언트는 원격지에서 인터넷으로 대출 모집인에 접속할 것이고, 신용 평가 기관이나 은행들도 원격지에 위치해 전용선이나 인터넷으로 대출 모집인을 연결할 것이다.)

위 솔루션 아키텍처에 대응되는 "Apache Camel을 이용한 대출 모집인 솔루션 아키텍처"는 다음과 같다.

"팁코 액티브엔터프라이즈를 이용한 구현"에서 달라진 점은 Apache Camel을 이용한 대출 모집인 솔루션은 팁코 동기 랑데부 전송을 좀더 범용적인 SOAP/HTTP 전송으로 대체하고, 팁코 비동기 랑데부 전송을 내부 메시징과 Apache ActiveMQ의 JMS Topic으로 대체하고, 프로세스 관리자를 프로세스 관리자(웹 서비스 빈) 라우팅과 응답 관리자 라우팅으로 대체한다는 점이다.


3. 대출 클라이언트 인터페이스


3.1. 대출 모집인

앞서도 간단히 언급했듯이 "Apache Camel를 이용한 구현"은 "팁코 액티브엔터프라이즈를 이용한 구현"의 팁코 동기 랑데부 전송을 SOAP/HTTP 동기 웹 서비스로 대체한다. 이를 위해 대출 모집인은 대출 견적 요청 서비스를 스프링 설정 파일에 SOAP 웹 서비스 인터페이스 빈으로 다음과 같이 노출한다.

brokerQouteServiceEndpoint 빈은 웹 서비스 인터페이스 객체고, brokerQouteServiceImp 빈은 웹 서비스 객체다. BrokerQouteServiceImp 빈 클래스가 웹 서비스를 실제 수행한다. 그러므로 BrokerQouteServiceImp 클래스가 실제 프로세스 관리자[EIP]다. 여기서는 서비스 우선 개발(service first development) 방법에 따라 웹 서비스 인터페이스를 먼저 정의했다. 이 설정으로 대출 클라이언트에 대한 대출 모집인의 SOAP 웹 서비스 노출을 완성된다. 서비스 우선 개발 방법의 자세한 설명은 "Apache Camel을 이용한 마이크로 웹 서비스 개발" 글을 참고하기 바란다.

BrokerQouteService 인터페이스는 다음과 같다.

BrokerQouteService 는 평범한 자바 인터페이스다. 이 인터페이스를 상속한 BrokerQouteServiceImp 빈 클래스는 프로세스 관리자 절에서 설명한다.


3.2. 대출 클라이언트

대출 클라이언트는 대출 모집인이 노출한 웹 서비스를 호출한다. 그런데 이 예는 대출 모집인과 대출 클라이언트가 하나의 스프링 설정 파일을 공유하므로, 대출 클라이언트는 위한 (호출 측) 웹 서비스 빈은 따로 설정하지 않는다. (대출 클라이언트는 (호출 측) 웹 서비스 빈으로 대출 모집인의 (서버 측) 웹 서비스 빈을 사용한다.)

웹 서비스를 호출하는 대출 클라이언트의 Camel 라우팅 설정은 다음과 같다.

대출 클라이언트 라우팅은 5초마다 bean:client 엔드포인트를 실행한다. 그러면 대출 클라이언트 빈은 대출 모집인의 웹 서비스를 호출한다.

LoanQouteClient 빈 클래스는 다음과 같다.

LoanQouteClient는 client 생산자 템플릿 객체를 이용해, 대출 모집인 웹 서비스로 동기 호출을 수행하고 반환된 결과를 화면에 출력한다.


4. 신용 평가 기관 인터페이스


4.1. 신용 평가 기관

신용 평가 기관도 동기 웹 서비스를 노출하므로, 신용 평가 기관은 신용 평가 서비스를 스프링 설정 파일에 SOAP 웹 서비스 인터페이스 빈으로 다음과 같이 노출한다.

creditScoreServiceEndpoint 빈은 웹 서비스 인터페이스 객체고, creditScoreServiceImp 빈은 웹 서비스 객체다. CreditScoreServiceImp 빈 클래스가 웹 서비스를 실제 수행한다. 여기서도 웹 서비스 개발에 서비스 우선 개발(service first development) 방법을 사용한다.

CreditScoreService 인터페이스는 다음과 같다.

CreditScoreService 는 평범한 자바 인터페이스다.

CreditScoreServiceImp 빈 클래스는 다음과 같다.

신용 평가 기관의 신용 점수 계산을 시뮬레이션 하기 위해 랜덤 메소드를 사용한다.


4.2. 대출 모집인

대출 모집인은 위에서 설명한 creditScoreServiceEndpoint 빈 설정을 그대로 사용해 신용 평가 기관에게 웹 서비스를 요청한다. 대출 모집인은 BrokerQouteServiceImp 빈에서 다음과 같이 creditBurea 생산자 템플릿을 이용해 신용 평가 기관에게 (동기) 웹 서비스를 호출한다. (BrokerQouteServiceImp의 전체 소스는 프로세스 관리자 절에 있다.)


5. 은행 인터페이스


5.1. 은행

이 예에서 각 은행들은 게시 구독 채널인 ActiveMQ의 JMS Topic에 가입한다. 그러므로 각 은행들은 스프링 설정 파일에 다음과 같은 JMS 수신자(가입자) Camel 라우팅 설정을 갖는다.

각 은행은 bank.loan.request 게시 구독 채널로부터 견적 요청을 수신해, bank? 빈 엔드포인트를 수행하고, 수행 결과 메시지를 bank.loan.relpy 채널로 응답한다.

BankQouteService 인터페이스는 다음과 같다.

BankQouteService 는 평범한 자바 인터페이스다.

BankQouteServiceImp 빈 클래스는 다음과 같다.

BankQouteServiceImp는 기업 통합 패턴 9장의 "MSMQ를 이용한 비동기 구현"을 참조했다. 요청마다 다른 견적을 응답하기 위해 랜덤 메소드를 사용한다.


5.2. 대출 모집인

대출 모집인은 BrokerQouteServiceImp 빈에서 다음과 같이 auction 생산자 템플릿을 이용해 bank.loan.request 게시 구독 채널로 견적 요청 메시지를 게시한다. (BrokerQouteServiceImp의 전체 소스는 프로세스 관리자 절에 있다.)


6. 대출 모집인 프로세스 개요

팁코 예의 대출 모집인 프로세스 정의는 다음과 같다.

Apache Camel 예는 위 프로세스 워크플로우를 프로세스 관리자 라우팅 설정인 borkerProcessManager와 응답 관리자 라우팅 설정인 brokerReplyManager로 대체한다. borkerProcessManager는 대출 클라이언트의 동기 요청을 처리하고, brokerReplyManager는 비동기로 은행들로부터 응답을 수신해 최상의 결과를 선택한다.

프로세스 관리자 borkerProcessManager의 라우팅 설정은 다음과 같다.

borkerProcessManager는 팁코 프로세스 워크플로우의 첫 번째부터 마지막까지 직선상에 위치한 액티비티들을 대체한다. borkerProcessManager는 은행들에게 대출 견적을 게시한 후 견적 결과가 완성될 때까지 실행 스레드를 중지한다. (자세한 설명은 뒤에 나온다.)

응답 관리자 brokerReplyManager의 라우팅 설정은 다음과 같다.

brokerReplyManager는 팁코 프로세스 워크플로우에서 오른쪽에 위치한 "Process Received Quote From Bank" 액티비티를 대체한다. JMS 큐로부터 은행들의 견적 응답을 수집해 최상 견적의 선택하고 borkerProcessManager를 재시작시킨다.


7. 대출 모집인 프로세스 상세


7.1. 프로세스 관리자

프로세스 관리자는 웹 서비스를 통해 대출 클라이언트로부터 대출 견적 요청을 수신하고, 신용 평가 기관 웹 서비스를 호출하고, 은행들에게 견적 요청을 게시하고, 최상의 대출 견적을 대출 클라이언트에게 반환한다.

다음은 프로세스 관리자를 위한 빈들과 borkerProcessManager 라우팅 설정이다.

BrokerQouteServiceImp 빈 클래스의 전체 소스는 다음과 같다.

BrokerQouteServiceImp은 프로세스 관리자로서 신용 평가 기관 인터페이스와 은행 견적 요청을 위한 게시 구독 채널을 멤버 변수로 갖고, 팁코 액티브엔터프라이즈의 대출 모집인 프로세스의 대부분의 액티비티들을 구현한다. BrokerQouteServiceImp은 게시 구독 채널로 은행 견적 요청을 브로드캐스트한 후 잡 저장소와 자바의 동기 메커니즘을 이용해 스레드를 중지하고 brokerReplyManager 라우팅이 job 객체를 깨울 때까지 대기한다.


7.2. 응답 관리자

응답 관리자는 은행들로부터 견적 응답을 수신하고, 견적 응답을 수집하고, 최상의 견적을 선택하고, 프로세스 관리자를 재시작시킨다.

다음은 응답 관리자를 위한 응답 수신 빈들과 brokerReplyManager 라우팅 설정이다.

수집기는 요청마다 10초동안 은행들로부터 견적들을 수집한다. bankReplyAggregator 빈은 은행들의 견적 응답을 bids 객체에 추가한다.

BankReplyAggregator 클래스는 다음과 같다.

이 수집기는 은행으로부터 수신한 메시지로부터 상관관계 식별자를 추출하여 상관 관계 식별자로 잡 저장소에서 잡을 꺼내와 bid 객체에 은행 응답을 추가하고, 다시 잡 저장소에 잡을 저장한다. AggregationStrategy을 상속해 Apache Camel의 수집 로직 전략 메커니즘을 이용했다.

ProcessReceivedQoute 클래스는 다음과 같다.

ProcessReceivedQoute 는 잡 저장소로부터 잡을 꺼내고, 은행 응답 객체를 생성하고, 완성된 bid 목록으로부터 최상의 견적을 찾아, 응답을 채운 후, 잡 객체의 자바 동기화 메커니즘을 이용해 BrokerQouteServiceImp 빈에서 중지된 스레드를 깨운다.


7.3. 잡 저장소

동기 프로세스와 비동기 메시징을 결합하려면 세션 객체 저장소가 필요하다. 이 저장소는 스프링 설정 파일에 빈과 Camel 라우팅을 이용해 설정한다.

잡 저장소의 스프링 빈과 Camel 라우팅 설정은 다음과 같다.

JobRepository 인터페이스는 다음과 같다.

JobRepository 인터페이스를 상속한 EhCacheRepository 클래스는 다음과 같다.

JobRepository 객체는 BrokerQouteServiceImp 클래스 등에서 멤버 변수로 참조된다.


8. 실행

대출 클라이언트는 5초마다 대출 모집인 웹 서비스에게 대출 견적을 요청하고, 대출 모집인은 대출 견적 요청을 은행들에게 게시하면, 5개 은행들은 각각 자신의 대출 견적을 응답하고, 대출 모집인은 5개의 대출 견적 응답 중 최상의 대출 견적을 선택해, 대출 클라이언트에게 반환한다. 아래는 이 솔루션을 실행한 결과다.

이 시뮬레이션 솔루션은 Camel 라우팅 설정으로 실행되며, 대출 클라이언트, 대출 모집인, 신용 평가 기관, 은행들을 모두 포함한다. Apache CXF 프레임워크가 포함되어 있으며, 테스트를 위해 Apache ActiveMQ가 함께 실행된다. 실행 결과의 아래를 보면 대출 클라이언트는 각 대출 견적 요청마다 다른 은행의 견적을 최상의 견적으로 반환 받을 것을 볼 수 있다.


9. 맺음말

필자는 이 글에서 Apache Camel로도 팁코 액티브엔터프라이즈로 구축한 통합 솔루션을 충분히 대체할 수 있음을 보였다. 게다가 팁코 액티브엔터프라이즈로 구축한 솔루션 예보다 Apache Camel로 구축한 솔루션 예가 설정이나 코딩을 더 적게 사용했다. 이렇게 작업량을 줄일 수 있었던 이유는 Apache Camel의 DSL(Domain Specific Language, 도메인 특화 언어)와 이미 완성된 컴포넌트들을 이용했기 때문이다. 웹 서비스를 위해 단지 몇 십 줄의 설정과 코딩이 필요했으며, JMS 서비스를 위해서도 몇 십 줄의 설정과 코딩을 추가했다. 이점이 Apache Camel이 갖는 강력한 생산성이다.

이와 같이 오픈 소스 통합 프레임워크인 Apache Camel을 이용하면 팁코와 같은 상용 EAI 제품으로 구축한 통합 솔루션을 대체하거나 통합 솔루션을 새롭게 구축할 수도 있으나, 여기에는 전제가 따른다. 기업 통합 패턴으로 아키텍처를 분석할 수 있어야 하며, 기존 솔루션의 아키텍처에 대한 이해도 충실해야 하고, Apache Camel과 Spring 프레임워크와 통합에 포함되는 다양한 기술에 대해서도 깊은 이해가 있어야 한다. 이런 준비가 없다면 기업 애플리케이션들을 제대로 통합할 수 없을 것이다. 물론 이런 전제는 상용 EAI 제품을 도입하더라도 동일하게 적용된다. 상용 EAI 제품도 어떤 기능을 제공한다고 해서 단순히 몇 줄의 설정이나 코딩으로 기능이 동작하는 것은 아니다. 상용 EAI 제품을 제대로 활용하려고 해도 통합을 위한 각종 어댑터들을 이해해야 한다. (실제로 어떤 EAI 상용 제품은 기업 통합 패턴의 번호표 패턴[EIP]를 제공하기 위해 수십 페이지 정도의 설정 절차를 숙지해야 하는 경우도 있다.) 그럼에도 상용 EAI 제품만이 해결할 수 있는 통합은 없으며, 오픈 소스를 잘 이용한다면 그에 못지 않는 통합 품질뿐만 아니라 솔루션 구조도 단순화시킬 수 있다는 점은 고무적인 것이다.

필자는 기업 통합 패턴과 통합 프레임워크에서 많은 가능성을 전망하고 있다. 기업 통합 패턴과 Apache Camel 통합 프레임워크는 이 글의 예처럼 상용 EAI인프라를 대체해 기업을 통합시킬 뿐만 아니라, ETL, 배치, 동기화 등 통합의 각론 분야에서도 충분히 가치를 발휘한다. 또한 새롭게 등장하는 개념의 마이크로 서비스(Microservice)를 개발하는 데 있어서도 Apache Camel은 가장 적합한 도구가 되어 가고 있다. 그러므로 아키텍트와 개발자들도 기업 통합 패턴과 Apache Camel에 관심을 갖기 바란다.


참고 사이트

2014년 7월 4일 금요일

실시간 트위터 이미지 사이트 개발기 3


웹소켓(WebSocket)

웹 브라우저에서 오랫동안 사용한 통신 프로토콜은 HTTP다. HTTP는 동기 방식의 요청-응답 프로토콜로 반드시 클라이언트인 웹 브라우저가 요청해야만 서버인 웹 서버가 응답하는 프로토콜이다. 그러므로 HTTP 접속은 요청으로 시작해서 요청-응답을 반복하다가 반드시 응답으로 끝난다. 이 방식은 최초에 웹 혁명을 이끄는 데 있어서 중요한 단순성을 제공했다. 단순한 요청-응답 방식은 극단적인 수준까지 요청-응답을 처리하도록 서버를 개선하도록 했다. 범용 웹 사이트를 가능하게 하기 위해 TCP/IP 스택은 비약적으로 개선됐고, 아파치 웹 서버 같은 견고하고 성능도 좋은 서버들도 개발됐다. 사실 웹 기술이 보급되기 전까지 전통적인 클라이언트 서버 방식의 서버는 수십 클라이언트들의 접속을 처리할 수 있다면 할 일을 충분히 한다고 인정받던 시절도 있었다. 그러나 웹의 보급과 발전 덕분에 서버는 동시에 수천 접속을 유지하고 수만 요청도 처리할 수 있도록 비약적으로 진화했다. C10k 문제(일만 동시 접속 문제)라는 용어가 등장할 정도로 다수 동시 접속 처리 문제는 개발자들을 골치 아프게 했지만, 현재는 다양한 아키텍처 패턴을 이용해 이 문제를 극복하고 있다. 그러나 시대가 발전하면서 요청-응답으로는 처리하기 어려운 시나리오들도 웹 브라우저를 이용해 처리하고자 하는 요구가 끊임없이 확대됐다. 예를 들어 웹에서 실시간으로 변하는 증권 시세를 표시하려면 요청-응답 방식만으로는 해결할 수 없었다. 해결했다손 치더라도 속도나 성능 효용성이 충분하지 못했다. 그래서 자바 애플릿이나 액티브X 컨트롤을 이용해 소켓 통신으로 문제를 우회하려고도 했다. 그러나 이런 플러그인 기술들도 만족스럽지 못했고 보안 취약성이 또 다른 문제가 되기도 했다. 또 다른 예로 웹 채팅 애플리케이션이 있다. 웹 채팅을 구현하려면 Ajax 같은 복잡한 기술을 사용해야 그런대로 가능했다. 이런 방법들은 온전한 비동기 통신 기술이 아니다. 채팅은 상대방이 입력한 글이 웹 브라우저의 요청 없이도 즉시 본인의 웹 브라우저에 도착해야 하는 전형적인 비동기 통신 모델이다. 그런데 Ajax도 반드시 요청해야만 응답을 (비동기로) 수신할 수 있으므로 온전한 비동기 통신 방식은 아니다. 결국 요청-응답 방식의 HTTP 프로토콜 덕에 가질 수 있었던 단순성이 웹의 성공을 이끌었던 반면, 이 단순성은 웹 기반 비즈니스 시나리오가 확대됨에 따라 점점 부족함으로 작용하게 됐다. 완전한 비동기를 웹 브라우저에서 사용하려고 할 때, 단순함이 도리어 복잡한 Ajax 방식을 사용하게 강요한 면도 있었다. 이와 같이 확장되는 웹 기반 비즈니스 시나리오를 충족시키기 위해서는 좀더 편리한 통신 방법이 필요했다. 그리고 그 산물로 완전한 양방향 통신을 지원하는 웹소켓 프로토콜이 등장했다.

웹소켓 프로토콜은 상당히 최근에 등장했다. 2011년에서야 IETF는 웹소켓 프로토콜을 정의한 RFC 6455 문서를 표준으로 제정했다. 이에 따라 웹 브라우저들은 2011년부터 상호 호환성이 보장되는 RFC 6455 웹소켓 표준으로 웹소켓 지원을 통일했다 크롬 16, 사파리 6, 파이어폭스 11, 인터넷 익스플로러 10 이후 웹 브라우저들은 RFC 6455 웹소켓 표준을 준수한다. 웹소켓은 글자 그대로 소켓과 거의 같은 기능을 제공한다. 즉 일반적인 소켓이 제공하는 양방향 통신을 제공한다. (그러나 TCP/IP 계층보다 상위 계층 프로토콜로 독자적인 패킷 구조를 갖는다.) 그러므로 HTTP 프로토콜과 달리 접속이 완성된 후 TCP 소켓 접속처럼 서버의 우선 전송도 가능해 진다. 즉 요청-응답 순서가 클라이언트에서 서버뿐만 아니라 서버에서 클라이언트로도 가능하다. 그러므로 웹소켓은 클라이언트 풀(pull) 뿐만 아니라 서버의 푸시(push)도 어렵지 않게 제공한다. 그 결과 웹소켓은 동기 방식뿐만 아니라 비동기 방식 통신 프로토콜로 완벽하게 구현하게 하는 기반을 제공한다.

웹소켓은 HTTP 프로토콜에서 프로토콜 갱신 핸드쉐이크를 통해 웹소켓 프로토콜로 전환이 되는 구조를 가지고 있어, HTTP 표준 포트인 TCP 80 포트를 그대로 활용할 수 있다는 장점을 가진다고 한다. 그러나 이 부분에 있어서 필자는 다른 견해를 갖는데, 그 이유는 TCP 80 포트를 바인드하는 웹 서버가 HTTP 프로토콜과 웹소켓 프로토콜을 동시에 처리하려면 내부 아키텍처가 상당히 다른 두 프로토콜 스택을 서버 안에 함께 포함해야 한다. 웹소켓은 Stomp 같은 상위 프로토콜의 하부 프로토콜로 사용되기도 하는데 이 경우 또 다른 Stomp 프로토콜 스택도 웹 서버에 포함시켜야 하는 문제가 발생한다. 프로토콜 스택 개발은 웹 서버 개발과는 다른 문제일 수 있다. 그리고 아직 웹 서버들이 충분이 웹소켓을 다루는 기술이 성숙되지 않은 문제도 있다. 그러므로 웹소켓을 사용해야 한다면 웹 서버와 별도로 전용 웹소켓 서버를 운영하면서 TCP 포트를 별도로 개방하거나 방화벽 등의 정책으로 인해 접속 문제가 있을 수 있다면 별도 서버에 또 다른 TCP 80 포트를 개방해 웹소켓 서버를 운영하는 방법이 합리적이지 않을까 생각된다. 아직까지는 웹소켓의 사용 방식에 대해 뚜렷한 일관적인 방식이 등장하고 있지는 않은 것 같다.

웹소켓의 또 다른 문제는 웹소켓 위의 애플리케이션 프로토콜이 충분히 성숙돼지 않았다는 점이다. 웹소켓은 TCP 계층에 아주 얇은 층을 추가한다. 그러므로 웹소켓을 직접 사용하는 것은 마치 TCP 소켓을 직접 사용하는 것과 같다. 일반적으로 개발자들은 TCP 소켓을 직접 사용하기보다 상위 애플리케이션 프로토콜을 이용한다. 이런 예로 Telnet, Ftp, Ssh, HTTP, AMQP, OpenWire, Stomp, MQTT 등등이 애플리케이션 프로토콜이다. 그러므로 웹소켓을 충분히 잘 활용하려면 웹소켓 위의 애플리케이션 프로토콜이 많아지고 또 쓰임새도 정비돼야 한다.


실시간 트위터 이미지 사이트의 웹소켓 사용

웹소켓을 먼저 설명한 이유는 실시간 트위터 이미지 사이트도 웹소켓을 사용하기 때문이다. 실시간 트위터 이미지 사이트는 웹소켓을 아주 쓸모 있게 사용한다. 실시간 트위터 이미지 사이트의 전체적인 정보 흐름과 통신 프로토콜들은 다음과 같다.

1) 미국에 위치한 실시간 트위터 이미지 사이트의 백그라운드 Camel 서비스는 트위터에서 고양이로 트윗을 검색해 결과들을 수신한다. 2) 그 중 이미지를 포함한 트윗을 추출해 한국에 위치한 ActiveMQ 서버에 OpenWire 프로토콜을 사용해 메시지로 전달한다. 3) 메시지를 수신한 ActiveMQ 서버는 메시지를 게시 구독 방식의 토픽에 게시한다. 실시간 트위터 이미지 사이트에서 페이지를 조회한 웹 브라우저는 이미 웹소켓 위의 Stomp 프로토콜로 ActiveMQ 서버에 접속한 토픽 구독자다. 4) 토픽에 게시된 고양이 이미지 트윗 메시지 사본은 웹소켓 위의 Stomp 프로토콜을 통해 ActiveMQ 서버로부터 웹 브라우저로 비동기적으로 전송된다. 웹 브라우저는 비동기적으로 수신된 메시지를 웹 브라우저 화면에 고양이 쎔네일 이미지로 출력한다.

미국에 위치한 실시간 트위터 이미지 사이트 웹 서버의 Camel 서비스와 대한민국에 위치한 ActiveMQ 서버는 메시징 프로토콜로 OpenWire 프로토콜을 사용했다. OpenWire 프로토콜은 다양한 언어나 플랫폼에서 ActiveMQ에 접속할 수 있게 하는 메시징 프로토콜이다. ActiveMQ는 OpenWire, AMQP, XMPP, MQTT 등 다양한 메시징 프로토콜을 지원한다. 웹 브라우저와 ActiveMQ 서버는 웹소켓 위의 Stomp 프로토콜을 사용한다. Stomp 프로토콜을 사용한 이유는 WebSocket 위의 Stomp 프로토콜을 지원하는 STOMP.js라는 JavaScript 라이브러리가 있고, ActiveMQ 서버도 웹소켓 위의 Stomp 프로토콜을 잘 지원하기 때문이다. 참고로 TCP/IP 위의 Stomp 프로토콜과 웹소켓 위의 Stomp 프로토콜은 호환되지 않는다. TCP/IP 위의 Stomp 프로토콜은 Stomp 프로토콜이 TCP 계층 바로 위에 구현되는 반면, 웹소켓 위의 Stomp 프로토콜은 TCP 계층 위에 웹소켓 계층이 있고 그 위에 Stomp 프로토콜이 구현된다. 그러므로 Stomp 프로토콜을 지원한다고 해도 직접적으로 웹소켓 위의 Stomp 프로토콜로 통신할 수 없다. 문자 기반 프로토콜이어서 인지, 웹소켓 위에서 가장 구현하기 쉬워서 인지 상당히 이른 시기에 웹소켓 위의 Stomp 프로토콜은 등장했다. 한편 RabbitMQ의 기본 프로토콜인 AMQP 프로토콜이 웹소켓 위의 JavaScript 라이브러리로 개발이 된다면 웹 브라우저에서도 AMQP 프로토콜을 사용할 수 있을 것이다. 그러나 현재 RabbitMQ 쪽에서는 Node.js와 연결에 주력하고 있는 것 같다. RabbitMQ는 Node.js에서 동작하는 AMQP JavaScript 라이브러리까지만 지원하고 있다. 아마도 이 JavaScript 라이브러리는 TCP/IP 위에서 구현된 라이브러리일 것이다. 즉 AMQP 프로토콜의 하부 통신 계층이 TCP일 것이다. 그러므로 현재 웹소켓 위에서 사용할 수 있는 메시징 프로토콜 라이브러리는 STOMP.js이 유일하다. (글을 쓰면서 좀더 조사해 보니 최근 들어 웹소켓 위의 MQTT 프로토콜도 사용 가능한 것으로 보인다. 이에 대한 검토는 독자들에게 맡긴다!) 여기서 주목할 점은 메시징 프로토콜과 메시지 채널(큐, 토픽)은 서로 다른 종류의 개체라는 점이다. 즉 같은 메시지 채널이라도 여러 프로토콜로 발신, 수신, 게시, 구독이 가능하다. 실시간 트위터 이미지 사이트도 동일한 토픽에 대해 메시지 전송은 OpenWire 프로토콜을 메시지 수신은 웹소켓 위의 Stomp 프로토콜을 사용했다.

웹소켓을 사용하면 웹 브라우저를 기업 통합 솔루션의 비동기 메시징 컴포넌트의 일원으로 포함시킬 수 있게 된다. 실시간 트위터 이미지 사이트는 트위터 이미지 메시지를 비동기로 전송하는 용도로 간단하게 웹소켓을 사용했지만, 앞서 언급했듯이 웹소켓은 웹 브라우저의 요청-응답 또는 폴링 방법으로는 해결하기 어려운 실시간 비동기 정보를 손쉽게 처리할 수 있게 해준다. 예를 들어 실시간 뉴스, 실시간 대시보드, 실시간 제어, 실시간 게임, 실시간 그래프 애플리케이션 같은 것들도 웹 브라우저로 구현할 수 있는 가능성을 열어준다. 이를 위해서는 SVG(SVG(Scalable Vector Graphics)나 Canvas 같은 HTML5의 다른 기능들도 활용해야 할 것이다. 또한 필자도 웹소켓이 요청-응답 형식의 기존 HTTP 프로토콜을 완전히 대체할 수 있다고는 생각지 않는다. 기존 HTTP 프로토콜은 화면 조회, 웹 서비스, Restful, Ajax RPC 등 다양한 요청-응답 패턴을 여전히 잘 수용한다. 그러므로 웹소켓의 역할은 요청-응답 방식의 HTTP 프로토콜로는 쉽게 해결할 수 없었던 실시간 비동기 통신을 웹 브라우저에 제공함으로 웹 브라우저의 사용 범위를 넓혀주는 것으로 봐야 한다. 즉 실시간 비즈니스 시나리오가 포함된 멋진 웹 애플리케이션을 만들려면, HTTP 프로토콜과 웹소켓 프로토콜 중 어느 하나만을 고집하는 것이 아니라 서로의 장점을 함께 활용해야 한다. 편식은 언제나 몸을 해롭게 한다.

웹소켓은 표준이 2011년에 완성됐고 표준을 지원하는 브라우저도 2011년 이후 버전들이고 특히 대한민국에서 많이 사용하는 인터넷 익스플로러는 버전 10부터 지원되므로 아직은 웹소켓을 사용한 사례가 많지 않은 것도 사실이다. 그러나 웹소켓을 범용 서비스가 아닌 기업 통합 등 기업의 특화된 서비스에 이용한다면 실시간 정보의 사용에 있어서 전용 클라이언트 애플리케이션 못지 않게 훌륭한 웹 브라우저 애플리케이션을 개발할 수 있을 것이라 기대한다. 이 글을 통해 독자들도 웹소켓의 가능성에 주목할 수 있는 계기가 됐으면 한다.



참고 사이트


2014년 4월 4일 금요일

실시간 트위터 이미지 사이트 개발기 2


시스템 구성

실시간 트위터 이미지 사이트의 웹 서버는 미 동부에 위치한 레드햇 오픈시프트 클라우드에서 운영된다. 그러나 이 서비스에 참여하는 메시징 시스템은 대한민국에 위치한다. 즉 실시간 트위터 이미지 사이트는 범세계적인 분산 시스템이다. 이 실시간 트위터 이미지 사이트의 시스템 구성은 다음과 같다.

위 구성도는 트위터 이미지 사이트의 서버 배치와 실행 순서를 설명한다. 미국 동부에 위치한 오픈시프트에서 운영되는 웹 서버는 트위터 서버로부터 실시간으로 고양이 이미지를 포함한 트윗을 추출하고 메시지로 변환하여, 이 트윗 메시지를 대한민국에 위치한 ActiveMQ 서버의 토픽으로 실시간 전송한다. 웹 브라우저는 미국 동부에 위치한 웹 서버의 트위터 이미지 웹 페이지를 조회하면서 동시에 대한민국에 위치한 ActiveMQ 서버에 웹소켓으로 접속하여 ActiveMQ의 토픽을 구독한다. ActiveMQ 서버에서 트윗 메시지는 실시간으로 토픽에 게시 중이므로 토픽을 구독하는 웹 브라우저는 ActiveMQ 서버의 토픽으로부터 트윗 메시지를 실시간으로 수신한다. 이렇게 실시간 트위터 이미지 사이트에서 “고양이를 보여줘” 웹 페이지에 접근한 웹 브라우저는 두 대륙에 걸쳐 분산되어 있는 서버들을 이용한다.

실시간 트위터 이미지 사이트의 아키텍처가 일반적인 웹 서비스의 아키텍처와는 다른 점은 분산 아키텍처를 위해 ActiveMQ라는 메시징 시스템과 웹소켓 기술을 추가로 사용했다는 점이다.

이 글을 읽는 독자는 왜 일반적인 웹 서비스 아키텍처를 사용하지 않고, 메시징 기반의 아키텍처를 추가로 이용한 것일까? 라는 의문을 가질 수 있을 것이다. 일반적으로 웹 서비스는 요구-응답 방식에 적합한 아키텍처이다. 실시간 트위터 이미지 사이트처럼 지속적으로 데이터 스트림을 처리해야 하는 아키텍처 모델로는 적합하지 않다. 적합하지 않다라는 뜻은 웹 서비스의 요구-응답 통신은 동기 통신 기술로 지속적인 메시지 흐름을 갖는 비동기 통신을 구현하기 위해서는 기술적인 어려움과 복잡성을 부가해야 한다라는 말이다. 또한 요구-응답 방식의 서비스로는 게시-구독 방식의 서비스를 구현하는 것이 쉽지 않다. 더 나아가 메시지의 흐름 제어를 처리하는 것도 쉽지 않다.

반면 메시징 시스템을 이용하는 경우, 참여 시스템들은 느슨한 결합 아키텍처(loosely coupled architecture) 구조를 갖게 된다. 메시징 시스템을 통해 비즈니스 로직들이 서로 분리되어 시스템들 사이 상호 의존성이 감소한다는 말이다. 실시간 트위터 이미지 사이트는 ActiveMQ 메시징 시스템을 이용하여 웹 브라우저와 웹 서버 사이 단단하게 결합될 수 있었던 트위터 비즈니스 로직을 독립적으로 동작 가능하게 분리했다. 그 결과 실시간 트위터 이미지 사이트는 웹 서버의 중단이나 이전 없이 비즈니스 로직을 다른 서버로 이전하거나 심지어 다른 대륙에 위치시킬 수도 있게 됐다. 필자는 실시간 트위터 이미지 사이트를 디버깅하는 과정에서 필자의 PC로 비즈니스 로직을 이동시켜 디버깅을 진행하기도 했다. 디버깅을 위해 비즈니스 로직을 이동시킨 가장 큰 이유는 PAAS 환경이 디버깅이 용이하지 않은 점이 있었기 때문이었다. 단단히 결합된 아키텍처(tightly coupled architecture)를 가진 웹 서비스 방식이었다면, 디버깅을 위해 서비스를 옮기는 경우, 좀더 많은 설정이나 프로그램의 변경이 수반됐을 것이다. 또한 메시징 시스템을 이용하면 실시간 데이터의 비동기적 처리가 가능하다. 즉 데이터 생산자와 데이터 소비자가 메시징 시스템을 매개로 분리됨으로 메시지 수신과 발신이 독립적으로 발생할 수 있게 된다. 그 결과 발신자는 수신자의 대기 없이도 지속적으로 생성한 메시지를 메시징 시스템으로 전송할 수 있고, 수신자는 발신자의 메시지 발신 속도와 무관하게 자신의 처리 속도에 따라 메시징 시스템으로부터 비동기적으로 메시지를 수신하고 처리할 수게 된다. 메시징 시스템을 이용하면 게시-구독 서비스도 구현하기가 쉽다. 가장 대표적인 게시-구독 패턴은 그룹 채팅이다. 그리고 이전 글에서도 언급했듯이 실시간 복수 소비자들이 주가 변화를 지속적으로 수신하는 것과 같은 비즈니스를 쉽게 구현할 수 있게 해 준다. 그리고 ActiveMQ 메시징 시스템의 경우 새롭게 등장한 웹소켓과의 결합도 쉽게 해준다. 실시간 트위터 이미지 사이트에 접속한 웹 브라우저는 웹소켓의 상위 프로토콜로 메시지 프로토콜인 STOMP 프로토콜을 이용하여 대한민국에 위치한 ActiveMQ 메시징 시스템에 직접 접속한다. 웹 브라우저는 이 접속 채널을 통해 비동기적으로 트윗 메시지를 ActiveMQ 메시징 시스템으로부터 지속적으로 수신하여 웹 페이지에 이미지를 표시한다.

실시간 트위터 이미지 사이트는 어떤 패턴의 비즈니스로 발전할 수 있을 지를 생각해 보자. 첫째, 실시간 트위터 이미지 사이트는 트위터에서 실시간으로 트윗을 추출 방법을 보여 준다. 그러므로 이 사이트는 트위터에서 발생하는 유행들을 실시간으로 분석하거나 추출, 저장하는 용도로 쉽게 변경될 수 있다. 필자는 개발 과정에서 사용자가 입력한 검색어를 이용하여 실시간으로 트위터에서 이미지들의 추출하여 표시하는 웹 페이지도 개발했다. 그러나 이 웹 페이지는 사용자들의 과도한 검색으로 트위터의 채널 할당량 정책을 초과할 수 있어 공개하지는 않았다. 검색어를 이용해 트윗 결과를 실시간으로 획득할 수 있는 기능을 이용하면 트위터로부터 필요한 유행 정보를 좀더 쉽게 획득할 수 있을 것이다. 둘째, 실시간 트위터 이미지 사이트는 ActiveMQ 메시징 시스템이 대륙 간의 메시지 통신에도 잘 사용될 수 있다는 것을 보여준다. 그러므로 여러 지역에 분산된 본점과 지점들 사이 메시지 통신이 필요한 비즈니스 모델에 ActiveMQ 메시징 시스템을 이용할 수 있다. 셋째, 실시간 트위터 이미지 사이트는 웹소켓을 이용하여 웹 페이지에 실시간으로 이미지를 보여줄 수 있다는 것을 보여준다. 그러므로 웹 페이지에 실시간 정보를 표시하는 비즈니스는 웹소켓 기술을 이용할 수 있을 것이다. 예를 들어 웹 페이지에 채팅 메시지 서비스를 제공한다든지, 웹 페이지에 주가를 실시간으로 표시하는 서비스를 제공한다든지, 웹 페이지에 비즈니스 이벤트를 모니터링하는 서비스를 제공한다지 등등 웹 페이지에 실시간으로 데이터 전달이 필요한 어떤 비즈니스에도 응용될 수 있을 것이다.

이 글을 통해 독자들은 실시간 트위터 이미지 사이트가 보기와 달리 범 대륙적인 분산 시스템으로 메시징 시스템과 웹소켓을 이용해 이미지가 포함된 트위터를 실시간으로 추출 및 전송하고 웹 화면에 이미지를 실시간으로 표시하는 시스템이라는 것을 알았을 것이다. 이상으로 실시간 트위터 이미지 웹 사이트의 시스템 구성에 대한 설명을 마친다.



참고 사이트


2013년 7월 5일 금요일

기업 통합 패턴과 메일러 애플리케이션


1. 메일 서버

메일 서버는 메일 전송 에이전트(MTA, Message Transfer Agent)로 기능하는 서버를 말하는데, 발신자인 메일러로부터 SMTP 프로토콜로 전송을 요청 받은 이메일을 여러 단계의 내부 큐들을 거치면서 수신자에게 다시 SMTP 프로토콜로 전송한다. 요청자로부터 이메일 전송을 요청 받은 메일 서버 즉시 요청 수신 확인을 요청자에게 응답하고, 수신자 시스템이 수신할 때까지 비동기적으로 메일의 전송을 최대한 보장한다.

그러므로 메일 서버 메일 전송을 위해 특화된 메시징 시스템이라고 볼 수 있다. 다시 말해 메일 서버 내부적으로 큐를 사용하고, 메일을 비동기적으로 전송하고, 메일은 메시지 구조의 데이터이므로 메시징 시스템이다. 그러나 독자적인 메일 전송 프로토콜(SMTP)을 사용하여 메일 전송 업무에 특화된다.

기업은 메일 전송에 다양한 요청 방법들을 필요로 하므로, 전송 환경도 다양하게 구축된다. 예를 들어 메일의 전송 요청은 데이터베이스로부터 일수도 있고, 파일로부터 일수도 있고, RPC(Remote Procedure Call)) 또는 전문 통신으로부터 일수도 있고, 메시지 큐로부터 일수도 있고, 웹 페이지로부터 일수도 있고, 아웃룩과 같은 메일 클라이언트로부터 일수 있다. 실시간으로 메일의 전송이 요청될 수도 있고, 특정 시간에 배치 형식으로 대량의 메일들이 요청될 수도 있다. 어떤 경우는 메일의 전송을 대행시키고, 어떤 경우는 메일 서버로 직접 전송을 요청한다.


2. 기업 통합 패턴

기업은 전형적인 기업 통합의 문제 중 하나인 메일러의 문제를 해결하기 위해 어떤 접근 방법을 사용할 수 있을까? 각 요청 방식에 대해 별도의 개발자나 개발팀에서 서로 각각의 방식으로 메일러를 개발할 수 있다. 이 경우 최악으로는 네 종류의 메일러 애플리케이션들이 만들어 질 수 있다. 또 실시간 처리인가 배치 처리인가를 고려한다면 다시 두 종류의 애플리케이션들로 또 나뉘어 지게 된다. 이와 같은 개발 접근 방법은 개발 비용도 많이 소요될 뿐만 아니라, 개발 이후 유지보수 비용도 상당히 많이 소요될 것이다. 그러므로 전체적으로 기업 통합의 관점에서 메일러 애플리케이션을 바라볼 수 있어야 한다.

"기업 통합 패턴(Enterprise Integration Patterns)"은 기업 통합 시 등장하는 상황을 65개의 패턴으로 정리하고 각 패턴에 대한 적용 방법과 장단점, 그리고 다른 패턴들과의 연관성들을 설명하는 패턴 언어이다. 기업 통합 패턴은 필자의 번역으로 곧 번역서로 출판될 것이다.

기업 통합 패턴에서 제시하는 패턴 중에 "정규 데이터 모델" 패턴이 있다. 이 패턴을 적용하는 경우, 메일러는 수용하는 데이터를 정규 데이터 형식으로 표준화하고, 각 애플리케이션들은 이 정규 데이터 모델의 구조를 따르게 메일러에게 요청할 데이터의 형식을 수정한다. 그런데 이런 접근 방법은 문제가 있다. 왜냐면 메일러를 이용하려는 애플리케이션은 메일러보다 먼저 개발됐을 수도 있고, 메일러가 요구하는 정규 데이터 모델로 애플리케이션을 수정하기가 어려울 수 있기 때문이다. 예를 들어 패키지로 도입된 애플리케이션인 경우 메일러의 정규 데이터 모델을 수용하기가 현실적으로 불가능 할 수도 있다.

그러므로 필요할 때마다 개발되는 애플리케이션도 문제이고, 메일러가 정규 데이터 모델을 강요하는 것도 문제가 된다. 그러나 정규 데이터 모델은 필요하다. 그렇지만 정규 데이터 모델 하나만으로는 기업에서 메일러의 통합을 완수하지 못한다. 그러므로 메일러는 수정될 수 없거나, 수정되기 어려운 애플리케이션들을 위해 "채널 어댑터"로서도 역할 해야 한다. 즉 애플리케이션의 접속 형식에 맞추어 접근할 수 있어야 한다. 이 경우 메일러는 필요한 "메시지 변환"을 자체적으로 수행해야 하고, 요청 메일을 폴링하는 "폴링 소비자"이거나, 병렬 처리가 필요한 경우 "경쟁 소비자"이어야 한다. 다시 말해 메일러 개발에 기업 통합의 접근 방법을 적용함에 있어, 이상적인 메일러의 구조인 정규 데이터 모델을 수용하기 어렵다면, 기존 애플리케이션들의 특성을 수용하는 통합 방법을 선택해야 한다.

기업 통합 패턴은 메일러를 개발 또는 도입하기 위해, 애플리케이션, 메일러, 메일 서버의 기능을 구별하게 하고, 메시지의 흐름과 변환을 전체적으로 고려하는 일관된 방법을 제공한다. 기업 통합 패턴을 일관되게 적용하면, 애플리케이션들을 통합의 관점에서 좀더 유연하게 만들 수 있게 되고, 개발 기간의 단축은 물론 유지보수성도 좋아지게 된다. 그리고 이런 유연성은 느슨한 결합(loosely coupling)의 철학을 기반으로 하고 있다.

기업 통합 패턴을 적용하여 메일러를 개발한다고 해서 개발의 범위가 결코 주는 것은 아니다. 다만 일관된 접근 방법에서 얻을 수 있는 개발 생산성과 유지보수성이 좋아지는 것이다.


3. 통합 프레임워크

기업 통합 패턴을 적용하여 메일러 애플리케이션을 개발한다고 하면, 우선 무엇을 해야 할까? 기업 통합 패턴은 기업 통합을 위한 일관된 분석, 설계, 개발 등의 접근 방법을 규정할 뿐 개발에 별도의 생산성을 제공하지는 않는다. 그러므로 개발의 생산성을 도모하려면 기업 통합 패턴을 손쉽게 사용하게 해주는 "통합 프레임워크(Integration Framework)"가 필요하다.

기업 통합 패턴이 출판된 후, 몇몇 통합 프레임워크들이 등장했다. 통합 프레임워크는 기업 통합 패턴에 기반한 프레임워크로, 처음에는 ESB(Enterprise Service Bus)나 메시지 큐가 기존 애플리케이션들과 연결할 때 부딪치는 문제들을 해결하기 위해 필요한 작은 컴포넌트 프로젝트로서 출발하였다. 그러다가 그 유용성이 점점 인식되면서 독립적인 프레임워크로 성장하였다.

통합 프레임워크는 기업 통합 패턴을 구현한 구현체이다. 통합 프레임워크가 아직 없었을 때, 기업 통합 패턴은 말 그대로 기업 통합을 위한 일관된 접근 방법을 뿐이므로, 애플리케이션을 개발할 때 이 기업 통합 패턴의 접근 방법들 하나하나를 개발하거나, 애플리케이션들의 통합을 위해 고가의 EAI(Enterprise Application Integration) 제품들을 구매하고 EAI가 제시하는 방법대로 애플리케이션들을 개발해야 했다. 그러나 통합 프레임워크가 등장하면서 개발의 수고를 많이 줄일 수 있게 되었고, 굳이 고가인 EAI를 도입하지 않을 수 있게 되었고, 더 나아가 EAI로서도 해결하기 어려웠던 기업 통합의 네 가지 패턴(파일 전송, 데이터베이스 공유, 원격 프로시저 호출, 메시징)들을 자유롭게 중재(Mediation)할 수 있게 되었다.

대표적인 통합 프레임워크로 Apache Camel이 있다. Camel은 기업 통합 패턴에 등장하는 패턴을 컴포넌트화 하고, 메시지 변환을 위한 형식 변환기(Type Converter)들을 제공하고, 컴포넌트와 관련 기술들을 연결하고 중재하는 도메인 특화 언어(DSL, Domain Specific Language)를 제공한다. 제공되는 통합 컴포넌트의 개수도 백여 개가 넘고 데이터 형식들을 자동 변환해 주는 형식 변환기(Type Converter)들도 다양하게 지원된다. Camel 프레임워크가 제공하는 컴포넌트와 형식 변환기, DSL를 이용하면 기업 통합 패턴들을 레고 블록처럼 조립할 수 있다. 다시 말해 Camel 프레임워크는 기업 통합 패턴의 개발을 조립으로 가능하게 함으로 기업 통합에 극적인 생산성을 제공한다.

그러나 Camel과 같은 통합 프레임워크는 양날의 검이 될 수 있다. 기업 통합 패턴은 애플리케이션의 아키텍처 패턴과 애플리케이션들 사이의 메시징 패턴에 대한 개념을 모두 포함하는 패턴이다. 그러다 보니 통합 프레임워크의 동작 메커니즘을 이해하는 데, 그동안 접근해 보지 못한 방식을 접하게 되고, 또 강력한 컴포넌트들을 조율하는 수많은 동작 옵션들은 컴포넌트의 적절한 활용을 어렵게 하기도 한다. 즉 통합 프레임워크에서 사용하는 메시지의 변환이나 컴포넌트의 동작 옵션에 따라 전혀 엉뚱한 또는 걷잡을 수 없는 부작용이나 연쇄 효과들도 볼 수 있게 된다. 예를 들면 기대한 메시지의 소실, 메시지의 폭주, 메시지의 중복 또는 메시지 순서의 뒤바뀜 등 메시징 시스템의 문제들과 우발적 성능 저하, 기대 밖의 메시지의 변환, 동기화 비동기 사이의 조율, 자원 해제의 시기와 절차 등 프레임워크나 컴포넌트, 메시지 처리의 관례(convention)들을 제대로 이해하지 못한다면 다양한 예기치 못한 문제들을 만날 수 있다.

그러나 예리한 검처럼 Camel을 잘 활용하는 경우, 통합 프레임워크는 극적인 생산성과 유지보수성을 제공해 준다. 필자가 바른모 주식회사 홈페이지의 Wiki에 올렸던 Apache Camel의 예인 "기상청 사이트 서울 날씨 주간 예보 조회"처럼, 심지어 100줄 이내에 모든 중요 처리를 수행하는 애플리케이션을 개발할 수 있을 정도로 놀라운 개발 생산성을 제공해 준다. 그러므로 메시징의 동기, 비동기 메커니즘, 통합 프레임워크의 구조 및 컴포넌트 구현 기술을 잘 이해하는 경우, 통합 프레임워크를 활용하는 개발자들은 이제까지 경험해 보지 못한 놀라운 개발 성과를 얻을 수 있을 것이다.


4. 통합 프레임워크를 이용한 메일러 개발

이제 다시 메일러 개발의 문제로 돌아와서 통합 프레임워크인 Apache Camel을 이용하여 얼마나 간단하게 메일러를 개발할 수 있는지를 보일 것이다. 우리는 메일러 솔루션을 개발하는 것이 아니므로, 이곳에서는 "채널 어댑터" 패턴의 간단한 메일러를 개발하려고 한다. 그리고 네 가지 통합 방법 중 공유 데이터베이스 패턴인 데이터베이스로 테이블부터 메일 전송 요청을 로드하여 SMTP 프로토콜로 메일 서버에게 메일의 전송을 요청하는 메일러를 개발할 것이다. 즉 메일러는 "채널 어댑터" 애플리케이션이면서 "데이터베이스 공유" 패턴을 이용한다. 이 과정을 통해 어떻게 기업 통합 패턴을 따르는 설계가 실제 실무에서 활용되는 지를 보일 것이고, 통합 프레임워크가 제공하는 개발 생산성도 보일 것이다. 또한 기업 통합 패턴이 어떤 이상만을 추구하는 패턴이 아니며, 기존 애플리케이션들과도 잘 융합하는 패턴임도 보일 것이다.


5. 요구

우리가 개발하는 메일러의 핵심 요구를 다음과 같이 나열해 보자

  • 1) 메일러는 데이터베이스로부터 메일의 발신 요청들을 로드한다.
  • 2) 메일러는 메일 서버에게 메일의 발신 요청을 전송한다.
  • 3) 메일러는 데이터베이스 접속 정보와 메일 서버의 접속 정보를 설정으로 관리한다.
  • 4) 메일러는 배치 형식의 애플리케이션이다.

이 요구들을 만족하는 메일러 애플리케이션을 그림으로 도식화 하면 아래와 같을 것이다.

위 요건은 기업의 실제적인 요구를 포함한다고 볼 수 있다. 실제로 기업에서는 위와 같이 데이터베이스에 저장된 메일 전송 요청을 특정 시간에 읽어 메일로 전송하는 배치 프로그램을 운영하는 경우가 많다.


6. 분석

메일러의 요구를 정의 했으므로, 기업 통합 패턴의 접근 방법에 따라, 메일러를 분석해 보자. 메일러는 별도의 애플리케이션으로 데이터베이스 서버와 메일 서버에 모두에 접근할 수 있어야 한다. 기업 통합 패턴의 관점에서 보면 이 메일러는 "채널 어댑터"로 볼 수 있다. 그리고 이 채널 어댑터는 내부에 데이터베이스 컴포넌트와 메일 서버 컴포넌트를 가진다. 그리고 컴포넌트들 사이의 메시지 변환도 필요하다.

이런 분석을 토대로 메일러를 좀더 상세화하면, 메일러 애플리케이션의 구조는 다음과 같을 것이다.


7. 시스템 구성

메일러는 자바 애플리케이션으로 개발되며 Java 7을 사용한다. 그리고 메일러와 통합되는 데이터베이스 서버는 MySQL 서버를 사용하고 메일 서버는 Postfix 서버를 사용한다. 사용되는 시스템들을 정리하면 다음과 같다.

  • 1) 자바 가상 머신(JVM) 버전: 7
  • 2) 데이터베이스 서버: MySQL 서버
  • 3) 메일 서버: Postfix 서버


8. 개발 도구와 프로그램 소스

메일러를 개발하는 통합 프레임워크로는 Apache Camel 프레임워크를 사용한다. 데이터베이스를 액세스하는 ORM 프레임워크는 MyBatis 프레임워크를 사용하고, 사용 라이브러리들을 쉽게 포함시키기 위해 Maven을 사용하고, 통합 개발 환경으로 Eclipse, 그리고 프로그램 소스는 GitHub를 이용한다.

  • 1) 통합 프레임워크: Apache Camel
  • 2) ORM 프레임워크: MyBatis
  • 3) 통합 개발 환경: Eclipse
  • 4) 라이브러리 관리: Maven
  • 5) 프로그램 소스: GitHub


9. Camel 컴포넌트

메일러 애플리케이션은 데이터베이스 컴포넌트와 메일 서버를 액세스하는 컴포넌트들이 필요하다. 이 통합 컴포넌트들을 처음부터 개발한다면 이것만으로도 하나의 프로젝트가 될 수 있었을 것이다. 그러나 우리에게는 통합 프레임워크인 Apace Camel이 있다. Apache Camel은 이미 이 두 기술에 대한 컴포넌트들을 제공한다. 메일러는 ORM 프레임워크로 MyBatis 프레임워크를 이용하므로, Camel의 "MyBatis 컴포넌트"를 사용한다. Camel MyBatis 컴포넌트는 MyBatis 프레임워크를 이용하게 해주는 컴포넌트이다. 그리고 메일 전송 컴포넌트로는 Camel"메일 컴포넌트"를 이용한다. 각 컴포넌트에 대한 설명은 다음 URL에서 확인할 수 있다.


10. 메시지 흐름

메일러의 데이터베이스 컴포넌트는 데이터베이스로부터 메일의 전송 요청을 로드한다. 발신자는 발신하고자 하는 메일들을 데이터베이스 테이블에 축적하고, 메일러는 메일 전송 요청 레코드들을 읽는다. 즉 데이터베이스로부터 복수 개의 메일 요청 레코드들이 한번에 읽는다. 그런데 메일러는 메일 전송 레코드를 하나씩 메일 서버로 전송해야 하므로, 메일러는 읽은 메일 요청들을 개별 메일 요청으로 분할하는 메시지 분할기(Message Splitter)가 필요하다. Apache Camel을 이용하면 메시지 라우팅을 다양한 기술로 기술할 수 있다. 우리는 간단한 메일러 애플리케이션을 개발하고 있으므로 그 중에서 Camel의 Java DSL(Domain Specific Language, 도메인 특화 언어)를 사용하여 이 분할기를 포함하는 메시지 라우팅을 기술할 것이다. 우리의 분할기는 한 덩어리의 메일 발신 요청 레코드들을 레코드 개수만큼 분할할 것이다. 그리고 나서 분할기는 분할된 각 레코드들은 메일 컴포넌트로 전송할 것이다. 그런데 Camel의 메일 컴포넌트는 메시지의 헤더에 발신과 수신에 관련된 정보를, 메시지 본문에 메일 본문을 요구한다. 그러므로 분할된 메일 발신 요청 레코드는 다시 메일 컴포넌트가 요구하는 형식의 메시지로 변환돼야 한다. 이 변환에는 Bean 객체를 사용한다. 이 변환을 수행하는 Bean 객체도 일반 컴포넌트처럼 Camel의 DSL을 통해 호출한다. 그리고 발신한 메일은 데이터베이스에 해당 메일 레코드의 전송 상태를 전송 완료 상태로 갱신해야 한다. 이를 위해 위해 필요한 정보는 메일 전송 요청 레코드의 MailID 필드 정보인데, 이 정보는 데이터베이스로부터 읽은 메일 전송 요청 레코드에 포함되어 있으므로, 테이블에서 메일 발신 레코드 정보를 읽을 때, Camel의 exchange 메시지의 속성에 MailID를 저장하여 MyBatis 컴포넌트가 메일 전송상태를 갱신하는 데 이용하게 한다. 메시지 분할기와 메시지 변환기의 Camel 설명은 다음 사이트를 참조한다.

기업 통합 패턴은 각 패턴마다 고유한 패턴 다이어그램을 제공하므로, 지금까지의 설계를 EIP 다이어그램(Enterprise Integration Patterns Diagram)으로 표현하면 다음과 같다. 아래 그림에는 메시지 엔드포인트, 메시지 분할기, 메시지 변환기 패턴의 다이어그램들이 보인다.


11. 설정 관리

Apache Camel 프레임워크는 설정(Configuration)에 사용할 속성들(properties)을 관리하는 컴포넌트인 Camel Properties 컴포넌트를 제공한다. 메일러 애플리케이션은 이 속성 컴포넌트를 이용하여 메일 전송 서버의 접속 정보를 mailer.properties 속성 파일로 관리한다. 또 이 속성 파일에는 데이터베이스 접속 정보도 포함되는데, 이 정보를 MyBatis가 읽을 수 있도록 MyBatis의 설정 파일인 SqlMapConfig.xml의 properties 엘리먼트의 resources 애트리뷰트에도 mailer.properties 속성 파일을 지정한다.

메일러 애플리케이션의 설정들과 관련된 위치 관례들은 다음과 같이 정한다.

  • 1) mailer.properties 속성 파일은 classpath에서 찾는다.
  • 2) MyBatis 설정 파일은 classpath:camel/example/mailer/data/SqlMapConfig.xml 에서 찾는다.

이제 설정과 관련된 파일들의 내부를 살펴보자. 우선 mailer.properties 속성 파일은 다음과 같다. 이 속성 파일에는 메일 서버의 접속 정보와 데이터베이스 접속 정보가 포함된다.

MyBatis의 설정 파일인 SqlMapConfig.xml 파일은 다음과 같다. 이 설정 파일에는 mailer.properties 속성 파일과 매퍼 파일의 설정이 포함된다. 이곳에서 속성 파일의 정보는 ${}로 참조한다.

MailerMapper.xml 매퍼 파일은 전형적인 MyBatis 매퍼 파일이다.

위 소스를 보면 "limit 1000" 질의 구문을 통해 한번에 최대 1,000개의 레코드를 읽게 선택 질의를 구성했음을 볼 수 있다. 이 구분은 대량 전송 테스트를 위해 메일 발신 요청 레코드의 읽는 수를 제한하기 위해 사용되었다.

log4j.properties 설정 파일은 별도로 설명하지 않는다. 프로그램 소스를 참조한다.


12. Configurer

메일러 애플리케이션은 설정자(Configurer) 패턴을 이용하여 Camel의 컴포넌트들을 등록한다. 설정자 패턴은 필자가 정의한 패턴으로 설정을 기본 로직에서 분리하여 별도로 관리하게 해주는 패턴이다. 설정자 구현 클래스에서 property 컴포넌트, MyBatis 컴포넌트, 메일 컴포넌트를 등록한다. 이 세 컴포넌트는 클래스의 설정자(setter)를 통해서도 입력 받을 수 있게 했다. 이 설정자(setter)들은 Spring 프레임워크를 통해 설정자(Configurer)가 Bean으로 활용될 때, 각 컴포넌트를 주입할 수 있게 추가한 것이다 다음은 설정자(Configurer) 인터페이스를 구현한 MailerConfigurer.java의 소스이다.


13. 메시지 라우팅

기업 통합 패턴에서는 컴포넌트와 메시지 라우팅을 이용하여 애플리케이션의 로직을 구현한다. 통합 프레임워크인 Camel은 도메인 특화 언어(DSL, Domain Specific Language)로 메시지 라우팅을 정의할 수 있게 한다. 메일러 애플리케이션은 자바 소스 형태의 DSL을 이용하여 메시지 라우팅을 정의한다. 다음은 메일러의 메시지 라우팅을 정의한 SimpleMailerBuilder.java 이다.

위 메시지 라우팅 설계를 EIP 다이어그램으로 그리면 다음과 같다.

Camel의 메시지 라우팅 설계는 순수한 기업 통합 패턴 메시지 라우팅 설계와 조금 다른데, Camel 프레임워크에서는 메시지 라우팅의 시작을 위한 동기 소비자인 direct 컴포넌트와 데이터베이스에서 읽은 레코드가 있는 경우만 후속 라우팅을 진행하는 메시지 필터가 추가되었다. 이와 같이 통합 프레임워크를 이용한 기업 통합 패턴의 설계는 순수한 기업 통합 패턴의 메시지 라우팅의 뼈대에 통합 프레임워크의 특성이 추가되는 방식으로 설계가 이루어진다.


14. 메시지 변환기

메시지 변환기는 출발 기술의 데이터 형식을 목적 기술의 데이터 형식으로 변환해 주는 컴포넌트이다. 이 글의 메일러도 데이터베이스로부터 읽은 레코드를 메일 컴포넌트가 해석할 수 있는 메시지로 변환해 주는 메시지 변환기가 필요하다. 이 변환을 수행하는 ToMailTranslator.java는 다음과 같다.

위 소스는 입력 메시지를 메일 컴포넌트가 해석할 수 있는 메시지 포맷으로 변환한다. 메일 컴포넌트는 메일 전송에 필요한 정보를 메시지 헤더에서 참조하고, 메일 본문은 메시지 본문에서 참조한다. 위 소스에서 발신 결과를 다시 테이블에 기록하기 위해 필요한 MailID를 exchange 메시지의 속성에 저장하고 있는 점에 주목하자. 위 소스는 Camel의 Processor 인터페이스를 구현한다. Camel의 Processor 인터페이스는 Camel이 내부에서 사용하는 exchange 메시지를 애플리케이션이 참조할 수 있게 해주는 process 메소드를 정의한다. 일반적으로 Camel을 이용하는 애플리케이션들은 이 메소드를 이용하여 메시지 변환기를 구현하거나, 필요한 컴포넌트 로직을 Camel의 메시지 라우팅의 중간에 삽입한다. 기업 통합 패턴의 관점에서 Processor 인터페이스의 구현체들은 "파이프 필터", "메시지 필터", "메시지 변환기", "내용 보탬이" 등의 역할을 한다.

메일러 애플리케이션은 메일을 발신에 성공한 후 발신 상태를 MyBatis 컴포넌트를 사용하여 갱신한다. 이때 MyBatis 컴포넌트는 데이터베이스의 테이블을 갱신하기 위해 메시지의 본문으로 입력 파라미터를 요구한다. 그런데 메일 컴포넌트가 이미 메시지 본문을 메일의 본문 용도로 사용했으므로, 메일을 발신한 후에도 입력 메시지의 본문은 여전히 메일 본문이다. 그러므로 MyBatis 컴포넌트가 입력 메시지의 본문을 입력 파라미터로 해석할 수 있게 메시지 변환기를 통해 입력 메시지의 본문을 변환해 주어야 한다. 또 MyBatis 컴포넌트가 참조하는 MailerMapper.xml 매퍼의 질의들은 입력 파라미터로 형식 정의에 따라 MyBatis 컴포넌트의 입력 메시지 본문의 형식은 맵 객체이어야 한다. 그러므로 맵 객체를 생성하고, exchange 메시지 속성에 보관해 놓았던 MailID를 이 맵 객체에 저장한 후, 이 맵 객체를 입력 메시지의 본문으로 지정하는 메시지 변환기가 필요하다. 다음은 이 변환을 수행하는 ToMapTranslator.java의 소스이다.

위 소스도 마찬가지로 Camel의 Processor 인터페이스를 구현한다.


15. 메일러

지금까지 메일러 애플리케이션을 위해, 설정자(Configurer), 메시지 라우팅, 메시지 변환기들을 구현했다. 이제 컴포넌트들을 사용하게 해주는 Camel 컨텍스트와 이 컨텍스트를 기동하는 로직이 필요하다. 다음은 이 과정을 수행하는 메일러 클래스인 Mailer.java이다.

위 소스는 상당히 간단해 보인다. 위 소스의 run 메소드는 Camel 컨텍스트 생성하고, 메일러 설정자(Configurer)를 호출하고, 메시지 라우팅을 등록하고, Camel 컨텍스트를 시작하고, direct:start 엔드포인트를 이용하여 메일러의 메시지 라우팅을 기동하고, Camel 컨텍스트를 닫는다. Camel Producer Template의 requestBody 메소드는 동기 호출을 수행한다. 즉 requestBody 메소드를 호출하면 direct:start 엔드포인트로 시작하는 라우팅이 완료될 때까지 호출 측은 메소드의 반환을 기다리게 된다. 참고로 Camel Producer Template의 send로 시작하는 메소드들은 비동기 호출용 메소드들이다. 즉 send로 시작하는 메소드를 호출하면 메시지 라우팅의 완료와 상관없이 메소드의 반환과 동시에 실행 흐름은 계속된다. 이 메일러 애플리케이션은, 요구에 따라 배치 스타일로 동작되게, 의도적으로 동기 메소드를 호출하여 메시지 라우팅이 시작되고 완료될 때까지 메인 스레드의 실행 흐름이 중지시켰다. Mailer 클래스도 Spring 프레임워크에서 Bean으로 사용될 경우를 고려하여, Configuerer와 RouteBuilder를 주입할 수 있는 설정자(setter) 메소드를 정의했다.


16. 실행

이제 구현을 완료했으므로, 메일러 애플리케이션을 실행할 수 있다. 메일러 애플리케이션이 실행되기 위해 MySQL 서버에 mailer 계정, EMAIL 테이블, 테스트 레코드들을 준비해야 한다. 테이블 스키마 생성과 레코드 입력은 프로그램 소스의 EMAIL.sql을 참조한다. 그리고 Postfix 서버도 준비해야 한다. 참고로 Postfix 서버를 설치하지 않더라도 메시지 라우팅을 정의하는 자바 소스에서 Camel의 smtp 엔드포인트의 URL을 수신자의 이메일 URL로 지정하면 수신자에게 직접 메일을 전송할 수도 있다. 메일 컴포넌트는 메일을 대상 메일 서버로 직접 전송할 수도 있기 때문이다. 참고로 Java 7 부터는 듀얼 소켓 드라이버를 사용하는 경우 IPv6 스택이 우선 선택된다. 메일러 애플리케이션은 아직 IPv4로 설정되었으므로, 메일러 애플리케이션을 실행할 때 JVM 명령행 옵션으로 -Djava.net.preferIPv4Stack=true 를 입력하여 JVM의 실행 시스템 프로퍼티를 IPv4 우선으로 지정해야 한다. (Java 5 버전부터도 그렇다고 하는데, 테스트 결과 Java 6까지는 IPv4가 우선 선택되었다. 즉 Java 6까지는 -Djava.net.preferIPv4Stack=true를 사용하지 않아도 IPv4가 우선 선택되었다.)


17. 실행 결과

우리의 메일러 애플리케이션은 잘 동작했다. 테스트로 1024 bytes 크기의 메일을 1,000개 요청하게 했다.

위 결과를 보면 1,000개의 메일을 전송하는 데 약 164초 정도 걸렸음을 알 수 있다. 그러므로 이 메일러는 메일 서버는 초당 약 6건 정도 메일을 전송했다. 이번엔 다음 결과를 보자. 다음 결과는 여기에 소개한 단순 메일러 애플리케이션을 병렬 처리 메일러 애플리케이션으로 수정하여 실행한 결과이다.

위 결과로 1,000개의 메일을 전송하는 데 약 33초 정도 걸렸음을 알 수 있다. 단순 메일러보다 병렬 처리 메일러가 약 5배 정도 빨리 전송되었다. 즉 초당 약 30건의 메일을 전송했다. 이 차이는 순차 처리(sequential processing)와 병렬 처리(parallel processing)의 차이에서 비롯된다. 메일러의 경우 Camel에서 순차처리를 병렬 처리로 바꾸는 일이 어렵지 않다. 그러나 어떻게 순차 처리를 병렬 처리로 바꾸었는지는 설명하지 않는다. 조금 귀뜀해 준다면 단지 프로그램 소스에 메소드 호출을 하나 더 추가한 것뿐이다. 이렇게 Camel을 잘 활용한다면 평범한 속도의 애플리케이션을 순식간에 대용량 애플리케이션으로 전환할 수 있게 해 줄 수도 있다. 그러나 세상에 공짜 점심은 없고, 현실은 마법 세상이 아니다. 이런 기술을 적용하기 위해 각각의 기술에 대한 순차 및 병렬 처리에 대한 충분한 이해, 메시지 흐름, 응답 시간, 처리량 등에 대한 충분한 기본기가 없다면 Camel로 개발하는 프로그램은 양날의 검이 되어 도리어 성능 저하, 불안정, 메시지 소실, 알 수 없는 동작 등 다양한 양상의 버그들이 등장할 수 있다. 명검을 사용하려면 먼저 명검을 다룰만한 고수가 되어야 한다.

참고로 다음은 필자가 개발한 또 다른 Camel 기반의 메일러의 결과이다.

#
항목
결과
설명
1 메일 크기
1024 bytes
각 메일의 본문 크기
2 요청 메일 건수
10,000
요청 메일 건수
3 평균 전송 시간
약 330초
메일러가 1만건의 메일을 요청하는 데 걸린 평균 시간
4 초당 전송 건 수
약 120건
5 시간당 전송 건수
약 40만건
한 시간에 약 40만건의 이메일을 전송할 수 있다.
6 백만 건 전송 시간
약 2시간 30분
백만 건의 이메일을 약 두 시간 반만에 처리할 수 있다.

18. 맺음말

일반적으로 기업 환경에 애플리케이션을 개발하거나 도입할 때, 애플리케이션들 사이에 단단한 결합(tight coupling)의 아키텍처를 구성하는 경우가 종종 있다. 여기서 단단한 결합이란 참여한 애플리케이션이나 시스템들 사이에 가정을 많이 포함하는 아키텍처를 말한다. 그 결과 애플리케이션은 독자적으로 유지보수하기 어려워지고 관련된 애플리케이션들이 미치는 영향들까지 모두 고려해야 하는 상황에 이르게 된다.

여기에 소개한 메일러 애플리케이션도 단순하게 개발만 고려하거나 도입만 고려한다면, 일반적으로 단단히 결합된 특화된 용도의 애플리케이션이 되기 쉬워진다. 그러므로 메일러 애플리케이션을 새로 도입하는 경우 "정규 데이터 모델"의 관점에서 도입할 수 있도록 노력해야 애플리케이션들 사이의 의존성과 데이터 변환 등, 단단히 결합된 구조를 탈피하고 느슨한 결합 구조를 갖게 되어 향후 발생할 수 있는 운영과 유지보수에 비용을 절감할 수 있게 된다. 그러나 이미 도입된 애플리케이션들의 변경이 어려운 경우, 기존 애플리케이션들의 아키텍처 구조를 최대한 보장하고 새로운 시스템 때문에 발생하는 침입적 상황을 제거하는 것도 기업 통합에서 중요하다.

이 글에서는 기업 통합의 초기 단계에 손쉽게 접근할 수 있는 "공유 데이터베이스" 패턴에 기반한 메일러 애플리케이션을 개발하는 과정을 보여 주었다. 통합 프레임워크인 Apache Camel 프레임워크를 이용하여 일반적인 애플리케이션 개발에 필요한 코딩 노력보다 상당히 적은 코딩 노력으로 메일러 애플리케이션을 구현할 수 있음을 보여 주었다. 아마도 어떤 프레임워크나 라이브러리보다 적은 량의 코딩으로 애플리케이션을 개발했을 것이다.

이렇게 통합 프레임워크는 기업 통합을 위한 애플리케이션 개발에 놀라운 생산성을 달성하게 해줄 수 있다. 그러나 이것이 끝이 아니다. 기업 통합 패턴의 장점 및 통합 프레임워크의 장점은 이렇게 개발된 애플리케이션의 확장, 수정, 유지보수 등에 따르는 비용도 놀랍도록 줄여 줄 수 있다는 것이다. 예를 들어 일반적인 메일러 애플리케이션의 경우, 메일 요청 레코드가 추가로 파일로부터도 제공돼야 한다고 요구 조건이 확장되면, 개발된 소스에 파일 처리, 레코드 추출, 데이터 변환, 메일 전송 등 전반적인 소스의 수정을 필요로 할 것이다. 그러나 기업 통합 패턴 기반 즉 통합 프레임워크 기반 애플리케이션은 이 추가된 요구에 대해 이미 준비된 파일 컴포넌트, 분할기와 메시지 라우팅의 수정 등으로 개발에서 보았던 생산성을 수정된 요구의 확장에서도 제공한다.

그러나 통합 프레임워크를 잘 사용하기 위해서는 컴포넌트에 활용된 기술이나 프레임워크를 잘 이해하는 것이 무엇보다도 중요하다. 그리고 기업 통합 패턴도 잘 이해하고 있어야 하고, 통합 프레임워크가 제공하는 편의 기능들도 잘 이해하고 있어야 한다. 이런 기본기가 없이 함부로 통합을 시도하면 결과적으로 기존 기업 애플리케이션들의 통합에 상존하는 문제들이 해결되지 않을 뿐만 아니라 새로운 양상의 문제들이 등장할 수도 있다.

기업 통합(EI, Enterprise Integration) 패턴은 그동안 우리가 시스템 통합(SI, System Integration)을 중심으로 개발하던 관행을 시스템들 사이의 통합에 대해서도 고려하게 해주고, 어떻게 시스템 통합이 개발돼야 다른 시스템들과도 잘 통합될 수 있는지에 대한 방법론을 패턴 언어로 제시한다.

코드로는 수백 줄에 불과한 메일러 애플리케이션을 개발하는 과정을 보이면서, 기업 통합에 많은 패턴들을 고려해야 했다. 즉 결과는 보잘것없을 수도 있지만 그 속에 수많은 생각들을 담고 있는 것이다. 애플리케이션들 사이의 느슨한 결합, 개발 생산성, 확장성, 유지보수성 등. 이런 것들이 고려되지 않고 우선 당장 언 발에 오줌 누기 식으로 급히 개발할 수도 있지만 그럴 경우 안정성, 속도, 유지보수성의 문제로 인해 지속되는 추가 비용을 감당해야 할 것이다. 소프트웨어는 코드의 양이 아닌 알고리즘, 아키텍처, 패턴들을 어떻게 활용하느냐가 결과적으로 성능, 생산성, 유지보수성을 높인다는 것을 우리는 너무 쉽게 잊는다.

"기업 통합 패턴"과 "Apache Camel" 프레임워크는 기업 통합 아키텍처나 생산적인 애플리케이션 개발에 관심이 있는 개발자나 아키텍트들이라면 알아야 할 패턴이고 프레임워크이다. 그리고 기업 통합 패턴은 곧 필자가 번역한 번역서로 출판될 것이므로 영문을 읽어야 하는 불편함도 해소될 것이다.


참고 사이트

2012년 8월 20일 월요일

동기 호출(Synchronous Call)에 대한 고찰


필자가 올린 “동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)” 블로그에서는 동기 호출, 비동기 호출의 정의에 대해 설명했었다. 여기에 그 정의를 다시 옮겨 보면 다음과 같다.
애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다.
애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.

지금까지 컴퓨터는 빠른 계산 능력을 사용하여 사용자에게 최대한 빠른 응답을 제공하는 방향으로 발전해 왔다. 그 결과 사용자는 컴퓨터에 어떤 요구를 하던지 거의 실시간으로 컴퓨터로부터 응답을 받게 되었다. 만약 컴퓨터에 요구한 응답이 몇 초 이상 걸리게 되는 경우 사용자는 몇 초의 기다림에 불안감까지 느낄 정도로 컴퓨터는 사용자의 요구에 대한 신속한 처리를 보장해 주었다.

이런 빠른 응답을 제공하기 위하여 컴퓨터 하드웨어는 무어의 법칙에 따라 18개월마다 성능을 두 배씩 성능을 향상해 왔다. 그러나 그 하드웨어에서 동작하는 프로그램은 상당히 정체된 발전 단계를 거쳐 왔다. 한 언어가 주류 언어가 되면 적어도 10년 이상 그 언어로 프로그램은 작성되고 그 뒤로도 몇 십 년을 사용하는 방식이었다. 예를 들어 현재 주류 언어 중 하나인 C (대표적 구조적 프로그램 언어) 언어는 태어난 지 이미 40년이 지난 언어이고 Java (객체 지향 언어) 언어도 1995년에 때어나 벌써 17년이나 지나고 있다. 이렇게 하드웨어 발전 속도와 프로그램 언어 사이에 발전의 차이는 프로그램에서 발전된 하드웨어를 최대한 활용하는 점점 어려운 상황에 접어 들게 되었다.

예를 들어 하드웨어 CPU가 늘어났지만 프로그램은 한 CPU에서 밖에 동작하지 못한다던 지, 하드웨어 레지스터 크기가 확장되어 더 많은 정보를 표현할 수 있지만 프로그램 언어의 자료형이 지원을 하지 못한다던 지 등 하드웨어에 도입된 중요한 기술을 프로그램은 좀더 오랜 기간을 거쳐야 비로소 그 기술에 적응하고 활용할 수 있게 된다. 그 중에서도 작업 처리 측면에서 보면 현재 주류 프로그램 언어들이 가진 순차적 명령 처리를 패러다임은 이런 하드웨어 성능 활용에 최대 걸림돌이 되고 있다. 즉 모든 실행은 순서를 가지고 하나씩 실행해 나가는 방식으로 어떤 일이던지 시작에서 끝까지 실행 흐름이 줄을 서서 실행된다. 이 방식은 객체 지향 언어에서도 마찬가지인데 여기서는 액터와 오브젝트 사이의 역할을 나누지만 실행 흐름에 대해서는 순차적 명령 처리 패러다임의 범주에서 벗어나지 못하고 있다. 즉 하부작업 요청 시 함수(메서드) 호출과 결과 리턴이라는 기본 골격은 구조적 프로그래밍 언어와 같다.

일반적으로 프로그램은 실행 흐름작성, 하부작업 실행, 하부작업 실행 결과 활용 등으로 작성된다. 이 과정에서 하부작업 실행과 그 결과를 구성하는 방식은 일반적으로 동기 호출 방식을 사용하게 된다. 즉 하부작업 요청 시 호출 쪽 본작업은 실행을 멈추고 하부작업이 리턴 될 때까지 대기하는 방식이다.

동기 호출은 하부작업의 결과물 획득에 순차적 처리가 가능한 리턴이라는 직관적이고 간결한 메커니즘을 제공한다. 즉 동기 호출은 하부작업이 진행되는 동안 호출 프로세스(스레드)는 실행이 중지되고 하부작업의 리턴 시 자동으로 실행이 계속되어 실행의 순차적 논리흐름을 잘 표현할 수 있다. 이에 비하여 비동기 호출은 하부작업의 완료까지 프로세스(스레드)가 대기하지 못하므로 실행의 순차적 논리흐름을 표현하는데 어려움이 있다. 좀더 구체적으로 비동기 호출은 하부작업의 결과물을 획득하는 메커니즘으로 Call Back 방식을 통한 결과 획득 또는 약속된 장소에 하부작업 결과를 확인하는 Poll 방식이 있는데 Call Back 방식은 호출 프로세스(스레드)의 작업에 대하여 순차적 처리 흐름을 보장하지 못하고 Poll 방식은 호출 프로세스(스레드)의 순차적 작업 흐름 처리는 가능하지만 동기 방식에 비하여 특별한 장점도 없으면서 처리 과정만 복잡한 문제를 일으킨다. 결론적으로 동기 호출은 실행의 순차 처리가 쉽고 비동기 호출은 실행의 순차 처리가 어렵다. 이런 측면에서 애플리케이션 개발 시 동기 호출은 비동기 호출에 비해 훨씬 지배적인 호출 방식으로 활용되고 이다.

그럼 이제부터 동기 호출 방식에 몇 가지 질문을 해보자 우선 처리 시간의 관점에서 동기 호출을 살펴보자. 일반적으로 동기 호출은 즉시 답을 요구하는 실시간 애플리케이션에 적합한 호출 방식이다. 즉 동기 호출의 처리는 아주 짧은 순간에 처리를 마쳐야 하는 경우에 주로 사용된다. 그런데 동기 호출을 미시적으로 살펴보면 (여기에서는 단일 애플리케이션에서 동기 호출을 살펴본다.) 호출 명령에서 호출이 일어난 실행 주소를 보관하고 하부작업 주소로 실행 주소를 옮겨 하부작업을 실행하고 하부작업 실행 결과를 저장하고 다시 호출 실행 주소로 작업을 옮겨 실행을 계속하게 된다. 이 과정에서 하부작업의 실행 시간이 반드시 일정 시간 필요하게 되는데 결과적으로 하부작업 요청시각(t0)와 하부작업 리턴 시각(t1) 사이 Δt = t1 – t0 이 진행된 후 본작업은 다음 작업을 할 수 있게 된다. 다시 말해 동기 호출도 엄밀히 말하면 동시성을 보장하지 못하고 반드시 비 동시성을 전제하는 호출인 것이다. 이렇게 동기 호출에 대하여 하부작업 처리를 위한 처리 시간이 필요하다는 것을 이해 한다면 결국 동기 호출도 실행 흐름은 순차적이지만 호출에 따른 실행 시간은 비동시적인 것이다.

동기 호출의 가장 큰 특징은 실행 흐름의 제어가 손쉽다는 것이다. 즉 앞에서 말한 하부작업 호출과 그 결과 획득의 프로그램의 간결성이다. 동기 호출을 사용하면 애플리케이션은 하부작업 처리 결과를 획득하기 위하여 별다른 노력 없이 단순히 함수(메서드)만 호출하면 된다.
int in, out;
in = 1;
out = subprocess(in);
System.out.println(in + " => " + out);
위 Java 소스 세 번째 줄을 보면 본처리 프로그램에서 하부처리 메서드 호출과 그 처리 결과 획득과정이 단 한 줄로 표현되는 것을 볼 수 있다. 이와 같은 동기 호출의 사용은 단순한 구조를 가지고 있고 단일 애플리케이션에서 분산 애플리케이션까지 일관되게 적용된다. 즉 분산 애플리케이션에서도 단일 애플리케이션의 호출 방식과 같은 방식으로 동기 호출을 사용한다. 그러나 이런 편리한 동기 호출을 지원하기 위하여 분산 애플리케이션은 통신 프로토콜, 하부 라이브러리, 프레임워크 부분에서는 상당히 복잡한 동기화 작업들을 구현하고 있다.

그럼 분산 애플리케이션에서 동기 호출 시 하부작업의 처리에 문제가 발생하면 어떻게 될까? 만약 하부작업에서 흐름제어를 관리할 수 있는 정도의 문제가 발생하는 경우에는 하부작업은 오류를 리턴하면 된다. 그러나 만약 하부작업의 문제가 흐름제어를 관리할 수 없을 정도의 심각한 문제가 발생하여 하부작업이 리턴을 할 수 없게 되면 전체 실행 흐름은 정지하고 애플리케이션은 정지(Hang-up)상태가 된다. 그리고 요구된 거래는 성공도 실패도 알 수 없는 상태에 이르게 될 것이다. 이렇게 하부작업에 흐름제어를 잃는 장애가 발생하면 애플리케이션은 중대한 문제 상황에 빠지게 된다. 그렇지만 일반적인 동기 호출에서는 하부작업 호출 시 타임아웃을 설정할 수 없다. 만약 하부작업 호출 시 타임아웃을 설정하고 실행 흐름 제어를 회복하기 위해서는 호출 프로세스(스레드)에 하부작업 완료를 확인하는 별도 스레드 등을 추가해서 하부작업을 모니터링 하는 기능을 추가 해야 한다. 그러나 일반적인 동기호출 사용 패턴에서 이런 문제까지는 고려는 되지 않고 있다. 예를 들어 J2EE 동기 호출 규격에도 흐름제어 회복을 위한 타임아웃에 대한 고려는 없다. 그러나 애플리케이션 운영측면에서 동기 호출 방식의 하부작업 호출에서 흐름제어 회복을 위한 타임아웃을 고려하지 않는다면 정지된 본작업 프로세스(스레드)는 아무런 일을 하지 않는 좀비가 되고 시스템 자원만 점유하게 되어 시스템 자원 가용성 및 성능에 영향을 줄 수 있다. 그리고 하부작업 흐름제어 회복 불능 장애 발생 시 더 심각한 애플리케이션 비즈니스 처리 상태의 불확실성을 만들어 낼 것이다.

동기 호출 패턴이 타임아웃 처리에 대한 관심이 적은 이유는 과거 단일 시스템에서 단일 프로세스로 동작하는 애플리케이션에서 사용하던 함수 호출에 대한 경험 때문일 것이다. 단일 프로세스 환경에서는 프로세스 내 실행 흐름이 하나 밖에 없기 때문에 본작업이던 하부작업이던 모두 같은 실행 흐름 내에 존재한다. 그러므로 하부작업이 흐름제어를 회복할 수 없는 문제가 발생하면 이미 전체 애플리케이션의 문제이기 때문에 애플리케이션은 동일한 정지(Hang-up) 현상을 보이게 된다. 그러나 분산 애플리케이션에서는 본작업의 실행흐름을 가진 프로세스(스레드)와 하부작업의 흐름을 가진 프로세스(스레드)가 각각 독립적인 실행 흐름을 가질 수 있다. 그 결과 동기 호출 시 본작업에서 요청한 하부작업의 실행흐름이 불능인 경우에도 본작업 프로세스(스레드)의 실행 흐름은 영향을 받지 않고 다음 처리를 할 수 있다. 단 본작업이 하부작업의 실행흐름을 잃는 장애에 대하여 자신의 실행 흐름을 계속하기 위해서는 본작업은 하부작업의 처리시간을 한정하는 타임아웃 기능을 추가해야 한다. 그러나 앞서 말했듯이 동기 호출에 대한 타임아웃 고려는 아직 개별적 고려 사항으로 일부 라이브러리나 규격에서 설정 또는 패러미터 입력으로 등장하고 있는 정도이다.

분산 애플리케이션 환경에서 동기 호출의 안정성에 대한 문제를 살펴보자. 일반적으로 단일 하드웨어에서 단일 애플리케이션의 동기 호출은 충분히 신뢰할 수 있다고 볼 수 있다. 그러나 분산 애플리케이션 환경에서 동기 호출은 시스템 하드웨어, 네트워크 등 관련된 구성 요소들이 모두 충분히 신뢰할 수 있다는 전제하에 안정성을 기대할 수 있다. 그러나 분산 애플리케이션에서는 아래와 같은 경우 동기 호출에 대한 신뢰성 문제를 가질 수 있다.

  1. 복수 시스템에 각각 동작하는 애플리케이션들 사이 동기 호출이 발생할 경우 시스템과 시스템의 네트워크 안정성은 프로세스 내 단일 애플리케이션의 하부작업 호출에 사용되는 시스템 내부 버스의 안정성보다 상당히 낮다. 그리고 네트워크 장애 발생 시 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다. 
  2. 복수 시스템에서 각 시스템의 애플리케이션이 동기 호출을 사용하는 경우 상대 시스템의 안정성을 기대할 수 없는 경우 가 있다. 다시 말해 각 시스템에 소유 주체가 다름으로 각 시스템에 동작하는 애플리케이션의 동작 여부를 항상 보장 받지 못할 수 있다. 어느 쪽이던 시스템의 장애 또는 애플리케이션에 장애가 발생하면 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다.

동기 호출 메커니즘은 프로그램 언어가 등장한 이래 가장 일반적인 작업 메커니즘이었다. 그러나 하드웨어가 발전하고 시스템들은 네트워크를 통해 연결되고 단일 애플리케이션에서 분산 애플리케이션으로 작업 규모가 커지면서 동기 호출의 한계가 점점 등장하고 있다. 동시성 또는 아주 짧은 처리 시간을 요구하는 업무들을 처리하는 방식에 동기 호출은 적합하지만 동시성을 지원하기 위하여 시스템 안정성, 네트워크 안정성, 애플리케이션 안정성을 모두 보장해야 하는 어려움이 따르게 되었다. 이런 어려움 속에서도 동기 호출 방식의 패러다임은 여전히 가장 선호하는 호출 방식으로 세상을 지배하고 있다. 그러나 분산 애플리케이션 환경에서 동기 호출을 사용할 때 하부작업 처리 시간, 실행 제어 회복, 분산 구간의 안정성 등을 고려해야 하므로 동기 호출 패턴이 예전 단일 애플리케이션과 비교하여 그렇게 단순하지 않게 되었으며 이와 관련하여 대안적인 패턴으로 비동기 호출의 필요성이 재 발견되고 있다.

지금까지 기술적 측면에서 동기 호출에 대한 몇 가지 사항을 생각해 보았다. 그런데 우리의 일상적인 비즈니스 패턴은 어떤 패턴일까도 고민해 볼 필요가 있다. 우리들이 살아가는 세상은 동기 호출 방식이 지배하는 세상일까 비동기 호출 방식이 지배하는 세상일까? 우리는 개인, 조직, 국가, 세계와 어떻게 의사 소통하며 살아가는가에 대하여 좀더 고민해 볼 필요가 있다. 그래야 동기, 비동기 문제를 좀더 객관화시킬 수 있고, 우리 생활과 잘 접목되는 애플리케이션 사용 방향을 찾을 수 있을 것이다.

2012년 8월 14일 화요일

동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)


프로그램에서 동기, 비동기 호출은 멀티 프로세스, 멀티 스레드 환경에서 동작하는 애플리케이션을 작성할 때 등장한다. 멀티 프로세스(스레드) 환경 즉 복수 프로세스(스레드)가 병렬적으로 동시에 실행되는 환경에서 각 프로세스(스레드)는 독립적인 실행 흐름을 가지고 동작하는데, 한 프로세스(스레드)가 다른 프로세스(스레드)에게 하부작업(프로시저, 함수, 메서드)을 요청할 경우 호출한 프로세스(스레드)의 실행 흐름의 중지 여부에 따라 따라 동기, 비동기를 구분 짓게 된다. 이 두 개념을 좀더 상세하게 설명하면 다음과 같다.

애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다. 멈춘 호출 프로세스(스레드)의 실행 흐름은 하부작업이 리턴되면 다시 계속된다.



애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.



비동기 호출은 기업 통합 패턴(Enterprise Integration Patterns)을 이해하는 기본 개념이다. 기업 통합 패턴은 비동기 호출을 어떻게 기업 애플리케이션 아키텍처로 활용할 수 있는지 설명한 패턴으로 기업 시스템이 복잡해짐에 따라 애플리케이션은 애플리케이션 내 상호 작용, 애플리케이션 간 상호 작용을 위한 효과적인 아키텍처가 필요한대 이때 기업 통합 패턴이 유용하게 사용될 수 있다.

참고)
1) Hophe Gregor and Bobby Woolf. Enterprise Integration Patterns (Addison-Wesley, 2003)

2012년 8월 3일 금요일

설정자 패턴(Configurer pattern)


어떻게 오브젝트 생성 시 필요한 초기화 정보의 획득과 입력 작업을 오브젝트 생성 로직에서 분리할 수 있을까?

일반적으로 오브젝트 생성과 관련하여 개발자들의 관심사는 Constructor, Builder, Factory 패턴 등과 같은 것이다. 개발자들은 오브젝트 생성을 위해 상황에 맞는 적절한 생성 패턴을 사용하여 오브젝트 생성 알고리즘을 구현한다.

이와 같이 오브젝트 생성과 관련해서는 잘 정리된 생성 패턴들이 있지만, 오브젝트 생성 시 필요한 초기화 정보 획득과 등록 작업에 관련된 적절한 패턴에 대한 고민은 상대적으로 부족하다. 그러나 프로그램을 개발하다 보면 오브젝트 생성을 위한 생성 패턴뿐만 아니라 초기화 정보의 획득 및 입력 작업에 대해 일관되고 확장이 용이한 방법에 대해서도 자주 고민하게 된다.

프로그램에서 오브젝트를 생성하기 위해서는 오브젝트 초기화에 필요한 입력 정보를 오브젝트에 입력해야 한다. 생성되는 오브젝트가 수행하는 작업에 따라 초기화에 사용되는 입력 정보는 통신 관련 오브젝트라면 원격지 접속 주소, 데이터베이스 처리 관련 오브젝트라면 데이터베이스 접속 정보, 데이터 파일 관리 오브젝트라면 데이터 파일 디렉터리 위치와 파일명 등이 될 수 있다. 그리고 생성되는 오브젝트의 초기화 입력 정보 형식에 따라 문자형, 숫자형과 같은 단순 원시 자료형에서 복합 구조의 오브젝트 형식 등 입력되는 정보의 형식도 다양한 구조를 가질 수 있다.  또한 생성되는 오브젝트의 초기화 입력 정보의 추출 경로에 따라 초기화 입력 정보는 하드 코딩, 설정 파일, 설정 데이터베이스, 설정 리지스트리, 원격지 설정 서버 등 다양한 경로를 통해 획득할 수 있다.

이와 같이 오브젝트 생성을 위한 초기화 정보 획득 및 입력 작업은 오브젝트 작업 내용, 초기화 정보 입력 형식, 초기화 정보 추출 경로에 따라 다양한 조합이 만들어져 개발에 따른 복잡성이 증가하고 일관성 있는 구현을 방해하게 된다.  이렇게 오브젝트 생성과 관련된 초기화 정보 획득개발이 복잡하고 일관성 없는 개발로 빠져들기 쉬운 문제점을 해결하기 위해 오브젝트 생성 로직에서 설정자(Configurer) 패턴을 사용할 수 있다.

설정자(Configurer) 패턴은 전략 패턴을 응용한 것으로 오브젝트 생성 시 오브젝트 생성 로직과 초기화 정보 획득 및 등록 로직을 분리하여 초기화 정보 획득 및 등록 작업에 일관성을 부여하고 초기화 정보 획득 및 등록 작업 알고리즘을 필요에 따라 대체할 수 있는 구조를 제공한다.

아래는 Factory 패턴과 함께 사용된 설정자(Configurer) 패턴 클래스 다이어그램이다. Factory 패턴 구조는 Design Patterns 의 Factory 패턴 구조이고 Design Patterns에서 제시한 Factory 패턴에서 생성에 필요한 로직은 Factory에 구현하고 Factory는 초기화 정보 획득과 입력 작업을 Configurer에 위임한다. 즉 Factory에서 초기화 정보 획득 및 입력을 분리하여 Configurer가 처리하도록 Factory 패턴의 역할을 나누고 확장한 것이다.

설정자(Configurer) 패턴 구조


Product
  • Factory Method가 생성하는 오브젝트 인터페이스
ConcreteProduct
  • Product 인터페이스 구현 클래스
  • 생성 시 필요한 초기화 입력 Method 구현
Configurer
  • 오브젝트 생성 시 오브젝트 초기화 정보 입력 Method 인터페이스
CustomConfiguer
  • Configure 인터페이스 구현 클래스
  • 오브젝트 생성관련 초기화 정보 추출 및 입력 처리, Setter Method구현 등
  • configure Method에서 Product 구현 오브젝트에 초기화 정보 입력
Factory
  • 오브젝트 생성 Method인 getObject 정의를 가진 Factory 인터페이스
ConcreteFactory
  • Factory 구현 클래스
  • Product 오브젝트 생성 시 Configurer 오브젝트에 초기화 정보 추출 및 입력 처리 위임
  • 오브젝트 생성관련 준비 작업 및 생성 오브젝트 반환


설정자(Configurer) 패턴은 Factory 패턴뿐만 아니라 Builder 패턴에서도 적용할 수 있는데 Factory패턴에서와 마찬가지로 Builder에서 초기화 정보 입력을 Configurer 오브젝트에 위임하는 방식으로 구현하면 된다.

적용 사례

이제부터 설정자 패턴 사용을 실제 예를 통해 살펴보자. 설명을 위해 사용한 Factory 패턴은 Spring Framework의 FactoryBean이다. 즉 Spring Framework 내에서 FactoyBean 사용 시 설정자 패턴을 적용하는 예를 설명할 것이다.

먼저 Java 프로그램에서 오브젝트가 어떻게 생성되는지 ektorp 란 라이브러리에서 HttpClient 오브젝트를 생성하는 프로그램을 보자. 아래는 소스를 보면 ektorp 라이브러리는 HttpClient 오브젝트 생성을 위해 Builder 패턴을 사용하고 있다.

HttpClient 오브젝트 생성 예
HttpClient httpClient = new StdHttpClient.Builder()
    .host("localhost")
    .port(8080)
    .build();
ektorp 라이브러리는 Builder 패턴을 사용하여 오브젝트를 생성하므로 구조적으로는 잘 만들어진 라이브러리로 볼 수 있다. 그러나 Spring Framework에서는 Bean 형식을 고려하지 않고 개발된 Java 오브젝트를 Spring Framework에서 직접 생성하기가 쉽지 않다. 이런 경우 Spring Framework는 임의의 Java 오브젝트를 생성하는 Spring Bean Factory 메커니즘을 제공한다. 다시 말해서 Spring Framework가 제공하는 Factory 패턴 방식을 사용하면 Bean 형식의 생성 초기화 방식을 갖지 않는 Java 클래스를 Spring Framework에서 사용할 수 있는 오브젝트로 생성할 수 있다. 아래와 같이 Spring Framework가 제공하는 Factory 인터페이스인 FactoryBean 인터페이스를 상속받아 HttpClientFactoryBean을 작성하면 httpClient 오브젝트를 생성할 수 있다.

HttpClientFactoryNoConfigurerBean.java
package com.brm.pattern.configurer;

import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.springframework.beans.factory.FactoryBean;

public class HttpClientFactoryNoConfigurerBean implements FactoryBean<HttpClient> {

 private String host;
 private int port;

 public HttpClient getObject() throws Exception {
  return new StdHttpClient.Builder()
    .host(host)
    .port(port)
    .build();
 }

 public Class<? extends HttpClient> getObjectType() {
  return StdHttpClient.class;
 }

 public boolean isSingleton() {
  return true;
 }

 public void setHost(String host) {
  this.host = host;
 }

 public void setPort(int port) {
  this.port = port;
 }
}
Spring Framework에서 HttpClientFactoryNoConfigurerBean 클래스는 아래 Spring XML 설정처럼 사용된다. 아래 Bean myHttpClientOrg 정의에서 생성되는 오브젝트는 HttpClientFactoryNoConfigurerBean의 getObject 메서드를 통해 생성된 오브젝트다.

application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 <bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>
</beans>
위 Spring XML에서 Bean 정의에 오브젝트 생성을 위한 초기 입력값 host와 port 정보는 property로 정의에 속성값으로 주입하였다. 속성 주입은 Spring Framework의 가장 중요한 장점 중 하나로 속성 주입을 사용하면 속성값을 프로그램 변경 없이 외부에서 입력할 수 있게 된다. 이러한 속성 주입 기능을 사용하기 위해서 HttpClientFactoryBean 클래스에서 오브젝트 생성에 필요한 초기 입력 정보를 setter 속성으로 정의했다. 이렇게 setter 속성을 정의하면 Spring Framework는 Bean 정의 시 해당 setter 속성의 property에 값을 주입할 수 있게 된다. 여기에서 setter 속성이란 Bean 형식의 클래스에서 set 으로 시작하는 메서드를 말한다. 이 부분에 대한 자세한 내용은 이 글의 주제를 벗어나므로 이해가 되지 않는 독자는 Spring Framework의 Bean 정의 설명 문서를 참조하면 될 것이다.

Spring Framework의 속성값 주입 기능을 통해 오브젝트 생성에 필요한 초기화 입력 정보를 HttpClientFactoryNoConfigurerBean 클래스 소스의 수정 없이도 수정할 수 있는 장점도 함께 생기게 되었다.

그러나 만약 어떤 애플리케이션 개발 구조상 위 두 속성 정보인 host와 port 정보를 데이터베이스로부터 입력 받는다면 어떻게 될까? 이 경우 속성값을 데이터베이스에 값으로 바로 추출하는 기능은 Spring Framework가 제공하지 않으므로 처음 임의의 Java 오브젝트를 Spring 프레임워크에서 사용하기 위해 부딪쳤던 문제와 유사한 문제에 다시 부딪치게 된다. 즉 Spring Framework의 속성값 주입 기능만으로는 부족하고 다른 방안을 찾아 소스를 수정해야만 된다. Spring Framework의 속성 값 주입 기능을 버리고 HtttpClient 오브젝트를 생성하기 위해 새로운 Spring FactoryBean 클래스 작성하여 host와 port 정보를 데이터베이스에서 읽도록 재 작성해야 한다. 그러나 Factory 로직을 분석하고 해당 초기화 입력 추출 로직을 대체하는 작업을 하는 개발자는 Factory 로직 내의 오브젝트 생성 로직과 초기화 입력 정보 획득 로직이 결합된 로직을 모두 분석해야 할 것이다. 여기서 예를 든 소스는 이 과정이 그렇게 복잡하지 않지만 일반적인 경우 항상 이런 경우를 기대할 수 없을 것이고 생성 과정이 복잡한 경우도 대해서도 고려해야 할 것이다. 이 과정이 복잡한 소스를 개발자가 분석 수정하는 경우 소스에 대한 분석과 수정에 따른 오류 영향도가 커지게 되어 개발 생산성은 저하될 것이다. 그러므로 오브젝트 생성 로직과 초기화 입력 정보 획득 및 등록 로직을 분리하여 할 수 있다면 그리고 개발자는 오브젝트 생성을 위해 단지 초기화 입력 정보 추출과 등록 로직만 수정할 수 있다면 개발 생산성은 높아 지게 될 것이다. 그리고 구조적으로 보면 Factory 개발 측면에서는 오브젝트 생성과 관련된 로직의 은익성을 확보하고 사용 오브젝트 사용 측면에서는 초기화 입력의 유연성을 제공받는다.

그럼 지금까지 Factory 패턴만 적용된 소스와 설정에 설정자 패턴을 추가해 보자. 먼저 Configurer 인터페이스를 정의한다.

Configurer.java
package com.brm.pattern.configurer;

public interface Configurer<T> {

 public void configure(T client) throws Exception;
}
Configurer 인터페이스는 초기 입력 정보를 주입할 수 있는 오브젝트를 입력 파라미터로 가지는 configure Method를 정의한다. 이 메서드에서 추출된 초기 입력 정보를 오브젝트에 등록하는 기능을 한다

다음으로 Configurer 인터페이스 구현 클래스를 작성한다.

HttpClientConfiguer.java
package com.brm.pattern.configurer;

import org.ektorp.http.StdHttpClient.Builder;

public class HttpClientConfiguer implements Configurer<Builder> {

 private String host;
 private int port;

 public void configure(Builder builder) throws Exception {
  builder.host(host).port(port);
 }

 public void setHost(String host) {
  this.host = host;
 }

 public void setPort(int port) {
  this.port = port;
 }
}
HttpClientConfigurer는 설정자 패턴의 구조를 설명하기 위한 클래스이므로 복잡한 추출 로직을 제시하지 않고 HttpClientFactoryNoConfigurerBean Factory내에서 초기 입력 정보 획득 부분만을 옮겨 왔다. Host와 port를 속성(setter)으로 사용하고 configure Method에서 Builder 오브젝트에 host와 port값을 등록한다. 만약 추출 경로가 데이터베이스라면 HttpClientConfigurer 를 상속받아 새로운 클래스를 (예를 들어 DBConfigurer) 만들고 데이터베이스 추출 로직을 추가하여 데이터베이스에서 추출한 host와 port 속성 값을 등록하는 로직을 추가하면 된다. 여기에서는 HttpClientConfigurer 로만 설명을 진행한다.

이제 HttpClient Factory 에서 초기 입력 정보 추출 로직과 등록하는 로직을 제거하고 Configurer 오브젝트를 호출하는 부분을 추가하면 아래와 같이 좀더 간결한 Factory 클래스가 된다.

HttpClientFactoryBean.java
package com.brm.pattern.configurer;

import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.ektorp.http.StdHttpClient.Builder;
import org.springframework.beans.factory.FactoryBean;

public class HttpClientFactoryBean implements FactoryBean<HttpClient> {

 private Configurer<Builder> configurer;

 public HttpClient getObject() throws Exception {
  Builder builder = new StdHttpClient.Builder();
  configurer.configure(builder);
  return builder.build();
 }

 public Class<? extends HttpClient> getObjectType() {
  return StdHttpClient.class;
 }

 public boolean isSingleton() {
  return true;
 }

 public void setConfigurer(Configurer<Builder> configurer) {
  this.configurer = configurer;
 }
}
보는 바와 같이 host와 port 속성에 대한 로직이 제거되었고 대신 Configurer 주입 속성이 생겼으며 getObject Method에 host와 port 값 등록 로직 대신 configurer 오브젝트의 configure Method 호출 로직으로 대체 되었다. 변경된 Spring XML 파일은 다음과 같다.

application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

 <bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>

 <bean id="myHttpClient" class="com.brm.pattern.configurer.HttpClientFactoryBean">
  <property name="configurer" ref="configuerer" />
 </bean>
 <bean name="configurer" class="com.brm.pattern.configurer.HttpClientConfiguer">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>
</beans>

위에서 보면 새로운 Bean Configurer 정의가 추가 되었고 초기 입력 정보와 관련된 속성관리는 Configurer Bean 정의로 옮겨졌다.
이렇게 구조를 변경하여 오브젝트 생성 로직은 초기 입력 정보 추출 경로가 변경되더라도 Factory의 수정은 발생하지 않고 Configurer 구현 클래스만 상속 변경 또는 변경하여 추출 경로 변경에 대응할 수 있는 유연한 구조가 되었다. 이 예에서는 설명을 위해 간단한 구조만 언급하였지만 실제 오브젝트 생성에 필요한 초기 입력 정보는 생성 오브젝트가 복잡하고 다양한 환경에서 운영되기 위하여 적게는 하나의 입력에서 많게는 수 십개 이상의 초기 입력 정보를 가질 수 있다. 이런 경우 설정자 패턴이 더 빛나는 구조가 될 수 있을 것이다.

맺음말

필자가 설정자(Configurer) 패턴이라고 이름을 붙인 이 패턴은 Apache Camel Framework의 분석 과정에서 얻은 것이다. 한 예로 Camel의 Http Component 라이브러리에 설정자 패턴이 적용되어 있다. Camel Http Component는 설정자 패턴을 통해 Apache HttpClient 오브젝트의 수많은 초기 파라미터 입력을 생성 로직과 분리하여 입력할 수 있는 기능을 제공한다. 그리고 Spring Framework에서도 설정자 패턴과 유사한 로직을 볼 수 있는데, 필자의 분석으로는 설정자 패턴으로 명확한 개념으로 정리가 되지 않고 개발 소스 수준의 적용으로 보인다. 그리고 Camel Framework에서도 설정자 패턴을 단지 전략 패턴으로만 인식하고 있고 Camel Component에 일부는 적용되어 있지만 일관되게 적용되지 않고 있는 점으로 미루어 아직 패턴으로 인식하지 않는 것 같다. 그러나 Configurer를 하나의 패턴으로 인식하고 이런 측면에서 오브젝트 생성 프로그램을 개발 할 때 설정자 패턴을 적용하여 설정 사용에 일관성을 부여하면 개발자들에게 오브젝트 생성에 대한 초기 정보 등록의 생산성과 초기 정보 추출 확장성을 제공해 줄 수 있을 것이라 믿는다.

참고 사이트
1) ektorp : http://github.com/helun/Ektorp
2) Camel HTTP Component : http://camel.apache.org/http.html