2013년 11월 6일 수요일

기업 통합 패턴 목록


필자가 올리는 기업 통합 패턴(Enterprise Integration Patterns) 관련 글을 읽는 독자들이 기업 통합 패턴의 목록이라도 알 수 있도록, 기업 통합 패턴의 전체 패턴 목록을 정리해 보았다. 기업 통합 패턴은 기업 통합을 위한 메시징 기반의 패턴으로 다음과 같은 상황(context)에 대한 방법론을 패턴으로 제공한다. 이 패턴들에 대한 상세한 방법론에 대해 좀더 관심이 있는 독자라면 원서를 읽어보거나, 출판될 필자의 번역서를 참조할 수 있을 것이다.



기업 통합 패턴 개요




메시징 엔드포인트(Messaging Endpoints)


메시지 엔드포인트(Message Endpoint)
메시징 시스템을 통해 메시지를 수신하고 발신하려면 애플리케이션은 어떻게 해야 할까?
메시징 게이트웨이(Messaging Gateway)
애플리케이션의 나머지 부분으로부터 메시징 시스템에 대한 액세스를 캡슐화하려면 어떻게 해야 할까?
메시징 매퍼(Messaging Mapper)
도메인 객체와 메시징 인프라의 독립성은 유지하면서, 이들 사이에 데이터를 이동시키려면 어떻게 해야 할까?
트랜잭션 클라이언트(Transactional Client)
클라이언트는 메시징 시스템과 함께 어떻게 트랜잭션을 제어할 수 있을까?
폴링 소비자(Polling Consumer)
준비된 애플리케이션만이 메시지를 소비하게 하려면 어떻게 해야 할까?
이벤트 기반 소비자(Event-Driven Consumer)
애플리케이션은 어떻게 사용 가능한 메시지를 자동으로 소비할 수 있을까?
경쟁 소비자(Competing Consumer)
메시징 클라이언트가 복 수개의 메시지들을 동시에 처리하려면 어떻게 해야 할까?
메시지 디스패처(Message Dispatcher)
하나의 채널에 대한 복수 소비자들은 자신들의 메시지 처리를 어떻게 조정할 수 있을까?
선택 소비자(Selective Consumer)
수신하려는 메시지만 선택하려면, 메시지 소비자는 어떻게 해야 할까?
영속 구독자(Durable Subscriber)
구독자의 수신 중지 동안, 발생 가능한 메시지 누락은 어떻게 방지할 수 있을까?
멱등 수신자(Idempotent Receiver)
메시지 수신자는 중복 메시지를 어떻게 처리할 수 있을까?
서비스 액티베이터(Service Activator)
애플리케이션은 메시징 기술과 비 메시징 기술 모두를 통해 호출되는 서비스를 어떻게 설계할 수 있을까?


메시지 구축(Message Construction)


메시지(Message)
메시지 채널로 연결된 두 애플리케이션은 어떻게 정보를 교환할까?
명령 메시지(Command Message)
애플리케이션들은 프로시저 호출에 어떻게 메시징을 사용할 수 있을까?
문서 메시지(Document Message)
애플리케이션들은 데이터 전송에 어떻게 메시징을 사용할 수 있을까?
이벤트 메시지(Event Message)
애플리케이션들은 이벤트 전송에 어떻게 메시징을 사용할 수 있을까?
요청 응답(Request-Reply)
애플리케이션은 어떻게 요청 메시지를 발신하고 응답 메시지를 수신할 수 있을까?
반환 주소(Return Address)
응답자는 응답 메시지를 전송할 채널을 어떻게 알까?
상관관계 식별자(Correlation Identifier)
요청자는 수신한 응답으로 어떤 요청에 대한 응답인지를 어떻게 알 수 있을까?
메시지 순서(Message Sequence)
많은 양의 데이터를 메시징을 사용하여 어떻게 전송할 수 있을까?
메시지 만료(Message Expiration)
메시지가 오래되어 사용 중단이 필요한 때를 발신자는 어떻게 지정할 수 있을까?
포맷 표시자(Format Indicator)
변경에 잘 대응하려면 메시지의 데이터 포맷은 어떻게 설계돼야 할까?


메시지 라우팅(Message Routing)


파이프 필터(Pipes and Filters)
독립성과 유연성을 유지하면서 메시지에 대한 복잡한 처리도 수행할 수 있으려면 어떻게 해야 할까?
메시지 라우터(Message Router)
개별 처리 단계들의 결합을 제거하여 메시지를 조건에 따라 서로 다른 필터로 전달할 수 있게 하려면 어떻게 해야 할까?
내용 기반 라우터(Content-Based Router)
단일 로직 기능이 여러 시스템에 물리적으로 분산되어 있는 경우 어떻게 처리해야 할까?
메시지 필터(Message Filter)
불필요한 메시지를 컴포넌트는 어떻게 수신하지 않을 수 있을까?
동적 라우터(Dynamic Router)
효율성을 유지하면서도 목적지에 대한 라우터의 종속성을 없애려면 어떻게 해야 할까?
수신자 목록(Recipient List)
수신자들이 가변적인 경우 어떻게 메시지를 라우팅할까?
분할기(Splitter)
메시지에 포함된 요소들을 각각 처리하려면 어떻게 해야 할까?
수집기(Aggregator)
서로 관련성이 있는 개별 메시지들은 어떻게 묶어 처리할 수 있을까?
리시퀀서(Resequencer)
순서가 뒤바뀐 메시지들 어떻게 올바른 순서로 되돌릴 것인가?
복합 메시지 처리기(Composed Message Processor)
서로 다른 처리를 요구하는 복수 개의 요소들을 포함한 메시지를 처리하면서도, 전체 메시지 흐름을 유지하려면 어떻게 해야 할까?
분산기 집합기(Scatter-Gather)
수신자들 각각에게 메시지를 발신하고 수신해야 하는 경우, 전체 메시지의 흐름은 어떻게 관리할까?
회람표(Routing Slip)
결정되지 않은 일련의 처리 단계들로 메시지를 라우팅하려면 어떻게 해야 할까?
프로세스 관리자(Process Manager)
설계 당시에는 필요한 단계가 알려지지 않았고 순차적이지 않을 수 있는 복합 처리 단계로 메시지를 라우팅하려면 어떻게 해야 할까?
메시지 브로커(Message Broker)
메시지 흐름의 중앙 제어를 유지하면서, 어떻게 메시지와 목적지의 결합을 제거할 수 있을까?


메시지 변환(Message Transformation)


메시지 변환기(Message Translator)
다른 데이터 포맷을 사용하는 시스템들이 메시징을 사용하여 서로 통신하려면 어떻게 해야 할까?
봉투 래퍼(Envelope Wrapper)
메시지 헤더 필드, 암호화 같은 특별한 포맷을 가진 메시지 교환에 기존 시스템을 참여시키려면 어떻게 해야 할까?
내용 보탬이(Content Enricher)
수신한 메시지에 필요한 데이터 항목이 완전하지 않은 경우 어떻게 다른 시스템과 통신할 수 있을까?
내용 필터(Content Filter)
큰 메시지에서 일부 데이터만 필요한 경우, 메시지 처리를 어떻게 단순화할까?
번호표(Claim Check)
시스템을 가로질러 전송되는 메시지의 데이터 크기를 정보 손실 없이 줄이려면 어떻게 해야 할까?
노멀라이저(Normalizer)
의미는 같지만 다른 포맷으로 수신된 메시지는 어떻게 처리할까?
정규 데이터 모델(Canonical Data Model)
다른 데이터 포맷을 사용하는 애플리케이션들을 통합할 때, 어떻게 하면 의존성을 최소화할 수 있을까?


시스템 관리(System Management)


제어 버스(Control Bus)
여러 플랫폼에 걸쳐 분산되어 있는 메시징 시스템을 효과적으로 관리하려면 어떻게 해야 할까?
우회기(Detour)
검증, 테스트, 디버깅 등을 수행하는 단계로 메시지를 통과시키려면 어떻게 라우팅해야 할까?
와이어 탭(Wire Tap)
포인트 투 포인트 채널을 지나가는 메시지는 어떻게 검사할 수 있을까?
메시지 이력(Message History)
느슨하게 결합된 시스템에서 메시지의 흐름을 어떻게 효과적으로 분석하고 디버깅할 수 있을까?
메시지 저장소(Message Store)
메시징 시스템의 느슨한 결합과 임시 보관적 특성을 방해하지 않으면서 어떻게 메시지 정보를 보고할 수 있을까?
스마트 프록시(Smart Proxy)
요청자가 지정한 반환 주소로 응답 메시지를 게시하는 서비스의 메시지는 어떻게 추적할 수 있을까?
테스트 메시지(Test Message)
컴포넌트가 메시지를 처리하면서 내부 오류로 인해 잘못된 메시지를 내보낸다면 어떤 일이 생길까?
채널 제거기(Channel Purger)
테스트 또는 운영 시스템이 교란되지 않게 채널 위에 남겨진 메시지들을 관리하려면 어떻게 해야 할까?


참고 사이트

2013년 10월 10일 목요일

Apache Camel, Hello, world!


1. 들어가며

Apache Camel은 기업 통합에 없어서는 안될 중요한 통합 프레임워크이다. Camel 프레임워크는 일반적인 애플리케이션에 내장 가능한 경량 프레임워크로, 프레임워크 내부에 라우터 엔진, 프로세서, 컴포넌트, 메시징 시스템을 포함하여, 애플리케이션의 내부를 외부 세계와 손쉽게 인터페이스할 수 있게 해준다. 즉 Camel 프레임워크는 애플리케이션, 시스템, 서비스들 사이에서 데이터(Data)와 기능(Function)을 통합(인터페이스)하는 중재자(Mediator)로서 역할한다. 이 글은 Camel 프레임워크가 어떻게 애플리케이션의 통합에 기여하는지를 간단한 "Hello, world!" 애플리케이션의 통합 과정을 통해 보여줄 것이다.

일반적으로 애플리케이션은 외부 세계와 인터페이스하기 위해 다양한 기술을 필요로 한다. 예를 들어 파일을 복사하기 위해서는 Java File Stream API를 사용해야 하고, 데이터베이스를 이용하기 위해서는 JDBC 드라이버를 사용해야 하고, 웹 서비스에 접속하기 위해서는 Apache HttpClient 라이브러리를 사용해야 하고, 이메일을 발신하기 위해서는 JavaMail API를 사용해야 한다. 게다가 새로운 Twitter 서비스를 이용하려 한다면 OAuth에 기반한 Twitter 서비스를 이용해야 한다. 즉 외부 애플리케이션이나, 서비스, 시스템들과 인터페이스 하려는 애플리케이션은 각 인터페이스에 맞는 기술을 애플리케이션 안에 모두 포함해야 한다. 따라서 애플리케이션을 개발하는 개발자가 외부와 인터페이스하는 각각의 기술에 대한 사용하는 방법을 알아야 한다. 그런데 일반적으로 애플리케이션 개발자는 비즈니스 로직 개발자들이다. 그러므로 외부 세계와 인터페이스에 많은 어려움을 호소하곤 한다. 실제로 인터페이스가 연결되지 않아 비즈니스 로직을 개발이 지연되는 경우가 상당히 많이 발생한다. 다음은 애플리케이션이 다양한 외부 시스템들과 인터페이스하는 방식을 그림으로 표현한 것이다.

그러나 애플리케이션이 Camel을 이용하는 경우, 애플리케이션은 Camel을 통해 외부 세계와 인터페이스할 수 있게 된다. 이 경우 Camel이 애플리케이션을 대신해 외부 세계와 인터페이스하게 된다. 이런 구조를 갖게 되면 애플리케이션은 Camel의 인터페이스 기술만으로, 어떤 외부 세계와도 인터페이스 할 수 있게 된다. 즉, 비즈니스 애플리케이션 개발자는 Camel 개발자하고만 의사소통하고, Camel 개발자는 인터페이스 하려는 외부 시스템의 개발자와 소통한다. 이 경우 비즈니스를 개발하는 애플리케이션 개발자의 외부 인터페이스에 대한 개발 부담은 현저하게 줄게 될 것이다. 물론 그 부담을 Camel 개발자가 떠안게 되지만, Camel은 외부 인터페이스 연동을 위해 이미 수백 가지 컴포넌트를 제공하고 있으므로, Camel 개발자는 외부와의 인터페이스에 새로운 프로그램을 작성하기 보다 Camel이 제공하는 컴포넌트를 활용할 수 있다. 이런 개발 과정이 얼마나 극적인 효과를 주는 지 곧 보게 될 것이다. 다음은 Camel을 사용한 애플리케이션의 인터페이스 방식을 그림으로 표현한 것이다.


2. Hello, world to Console

이제 Camel을 사용하는 간단한 애플리케이션을 작성해 보자. "The C Programming Language"에 처음 등장하는 "Hello, World!"를 콘솔에 출력하는 프로그램을 평범한 POJO 형식의 Java 프로그램과 Camel을 이용한 POJO 형식의 Java 프로그램으로 작성해 보자. 먼저 콘솔에 "Hello, World!"를 출력하는 Java 프로그램은 다음과 같다.

위 소스는 Java 개발자라면 모두 이해할 수 있는 간단한 프로그램이다. 이제 위 Java 프로그램과 동일한 결과를 출력하는 프로그램을 Camel를 이용해 작성하면 다음과 같다.

위 프로그램 소스를 살펴보자. 위 소스에서는 기본 Java 프로그램에서는 없었던, Spring Bean 정의 XML 파일인 ToConsole.xml을 Camel Main 객체에 지정한다. (Spring Bean 정의 XML을 사용한 이유는 느슨한 결합(loose coupling)이 가능하도록 Camel을 설정하기 위해서이다. 결합도(coupling)를 고려하지 않는다면 Java 프로그램 소스안이 이 XML을 프로그램적으로도 삽입 할 수 있다.) 프로그램의 main 메소드는 Camel Main 객체를 이용하여 Camel 컨텍스트를 시작한 후, Main 객체로부터 Camel과 통신할 수 있는 생산자(발신자) 객체인 ProducerTemplate를 얻어, "direct:start" URL과 "Hello World!" 문자열을 파라미터로 ProducerTemplate 객체의 requestBody 메소드를 호출한다. (생산자(producer) 또는 발신자(sender)는 기업 통합 패턴에서 사용하는 용어로 메시지를 발신하는 개체를 말한다.) 그리고 main 객체를 종료한다. Camel Context의 초기화와 종료를 제거하고 보면 처음의 Java 프로그램에 비해 그렇게 복잡하지 않다. 그렇다면 ToConsole.xml 파일이 혹시 복잡한 것은 아닐까? ToConsole.xml을 살펴 보자.

위 XML 설정을 보면 Camel Context를 정의하고 그 안에 라우팅 로직을 하나 지정했다. 이 설정은 "direct:start" 엔드포인트에서 출발하여 "stream:out" 엔드포인트로 도착하는 라우팅을 지정한다. ToConsole.xml을 기업 통합 패턴(EIP) 다이어그램으로 보면 다음과 같다.

여기에서 "direct:start" 엔드포인트는 Camel이 애플리케이션으로부터 동기 호출을 수신하는 출발 지점이다. "stream:out" 엔드포인트는 Camel이 표준 콘솔로 메시지를 발신하는 지점이다. 즉 위 라우팅은 Camel의 "direct:start" 엔드포인트로 수신된 메시지를 "stream:out" 엔드포인트로 전달하라는 정의를 담고 있다.

프로그램을 컴파일하고 실행하기 위해서는 다음의 Maven 의존이 필요하다.

하나는 Camel을 Spring 프레임워크와 함께 사용하기 위해 필요한 의존이고, 하나는 Camel의 Stream 컴포넌트 라이브러리에 필요한 의존이다.

Eclipse 환경에서 프로그램을 컴파일하고 실행할 수 있도록 프로그램 소스를 Eclipse 프로젝트로 GitHub에 올려 놓았다. Maven을 설치했다면, Eclipse 환경에서 프로그램 소스를 열지 않고도, 다음과 같이 명령창에서 Maven(mvn)을 이용하여 컴파일과 실행이 가능하다.


3. Hello, world to Log

지금까지 작성한 Console 출력 애플리케이션을 log4j를 통해 로그로 기록하는 프로그램으로 수정해 보자. 수정된 프로그램 소스는 다음과 같다.

위 소스에서 Console로 출력하는 프로그램과 달라진 점은 Spring Bean 정의 XML 파일이 ToConsole.xml에서 ToLog.xml로 달라진 것 밖에 없다. Spring Bean 정의 파일을 새로 지정한 이유는 단지 이곳의 예를 위해서 필요했기 때문이다. 실제 프로그램에서는 애플리케이션 소스를 수정하지 않고 Spring Bean 정의 XML 파일의 라우팅 정의를 수정함으로 애플리케이션의 출력을 즉시 Console에서 Log로 변경할 수 있다. ToLog.xml을 살펴 보자.

라우팅 정의에서 출발지는 "start:direct"로 ToConsole.xml에서와 같고 도착지의 "log:ToLog?level=WARN"로 ToConsole.xml의 "stream:out"와 다르다. Camel에서는 라우팅의 엔드포인트(도착지, 출발지)에 따라 인터페이스하는 외부 세계가 달라진다. 즉 ToLog.xml의 외부 세계는 이제 로그 라이브러리가 된 것이다. 상단의 logFormatter Bean 객체의 정의는 로그 기록 포맷을 지정하는 포맷터로 로그 기록 포맷을 자유롭게 커스터마이징할 수 있게 해 준다. 이곳에서 사용한 HelloFormatter는 메시지 본문의 문자열을 로그로 기록한다. HelloFormatter 소스는 프로그램 소스가 올라가 있는 GitHub를 참조한다. ToLog.xml의 EIP 다이어그램은 다음과 같다.

프로그램을 컴파일하고 실행하기 위해서는 다음의 Maven 의존이 필요하다.

Log 컴포넌트는 Camel Core 라이브러리에 포함되어 있으므로 stream 컴포넌트처럼 별도의 라이브러리가 필요하지 않다. Eclipse에서 프로젝트를 열지 않는 경우, 다음과 같이 명령창에서 Maven(mvn)을 이용하여 컴파일과 실행이 가능하다.


4. Hello, world to Mail

지금까지는 별로 특이한 사항은 없다. 이제 좀더 특별한 애플리케이션을 만들어 보자. 애플리케이션에서 메일을 발신하려고 한다. 애플리케이션은 어떻게 수정돼야 할까? 메일을 발신하는 ToMail.java는 다음과 같다.

위 소스도 ToCosole이나 ToLog 애플리케이션과 동일하다. 단지 Spring Bean 정의 XML 파일이 ToMail.xml로 달라진 것 밖에 없다. 즉 애플리케이션은 "Hello, world!"를 메일로 발신하기 위해서도 수정되지 않는다. ToMail.xml을 살펴 보자.

라우팅 정의에서 "start:direct" 엔드포인트는 이전 프로그램들 설정들과 동일하고 도착지 엔드포인트가 메일 엔드포인트인 "smtp:barunmo.com?username=testuser&password=testpassword"로 바뀌었다. 메일을 발신하기 위해서는 SMTP 서버와 메일 주제, 발신자, 수신자 등이 추가적으로 필요한데, 이런 정보들은 XML에 설정으로 지정했다. ToMail.xml의 EIP 다이어그램은 다음과 같다.

이전 라우팅 정의와 달리 중간에 setHeader 태그로 지정된 부분은 메시지 변환기(Message Translator)로 표시되었다. 이 메시지 변환기는 EIP 패턴 다이어그램 중 하나이다. 더 많은 패턴 다이어그램이 기업 통합 패턴(Enterprise Integration Patterns)에 정리되어 있다.

프로그램을 컴파일하고 실행하기 위해서는 다음의 Maven 의존이 필요하다.

Camel Mail 컴포넌트는 내부적으로 JavaMail API를 이용한다. Camel Mail 컴포넌트의 의존이 필요한 관련 라이브러리를 자동으로 추가해 줌으로 애플리케이션에서는 별도로 추가할 의존은 없다. Eclipse에서 프로젝트를 열지 않는 경우, 다음과 같이 명령창에서 Maven을 이용하여 컴파일 및 실행이 가능하다. 단 GitHub의 소스에서 내려 받은 소스에는 메일 정보들이 가상의 값으로 채워져 있으므로, 이 예를 실행 전에 메일 수신자와 발신자 정보 그리고 smtp 엔드포인트의 SMTP 서버 정보, 사용자, 패스워드를 테스트 하는 시스템과 개발자의 정보로 수정해야 한다.

다음은 ToMail 프로그램을 실행하여 필자가 수신한 메일이다.

ToMail 애플리케이션도 Spring XML 설정을 가리키는 부분 이외의 프로그램의 수정이 없으면서, 메일을 발신하는 기능을 갖게 되었다. 다음은 마지막으로 Twitter와 인터페이스하는 애플리케이션을 작성해 보자.


5. Hello, world to Twitter

요즈음 애플리케이션들은 소셜 네트워크와 뗄래야 뗄 수 없는 환경에 있다. 그러므로 이제 우리의 애플리케이션도 트위터로 트읫을 전달하게 만들어 보자.

트위터 애플리케이션을 만들기 위해서는 https://dev.twitter.com/apps/new 사이트에서 애플리케이션을 등록하고 OAuth 등의 정보를 획득해야 한다. 필자도 이 사이트에 접속하여 Barunmosoft 계정으로 접속하는 애플리케이션을 등록했다. 이 애플리케이션을 테스트하기 위해서는 독자들도 이 사이트에 접속하여 트위터 애플리케이션을 등록해야 한다. 다음은 Hello, world를 트윗하기 위한 ToTwitter.java의 소스이다.

위 소스도 ToCosole이나 ToLog나 ToMail 애플리케이션과 마찬가지로 소스상에 변화는 없다. 단지 Spring Bean 정의 XML 파일이 ToTwitter.xml로 달라진 것 밖에 없다. 즉 애플리케이션은 트윗을 사용하기 위해서도 수정이 필요 없다. ToTwitter.xml을 살펴 보자.

라우팅 정의는 이전 프로그램들처럼 도착지 엔드포인트가 트위터 엔드포인트인 "twitter://timeline/user"로 바뀌었다. 트윗하기 위해서는 트위터에 애플리케이션을 등록해야 하는데, 이 등록 과정을 진행하고 나면 트위터 사이트는 consumerKey, consumerSecret, accessToken, accessTokenSecret 값을 생성해 준다. 이 생성된 값을 twitter 컴포넌트의 Bean 정의에 속성으로 지정한다. 위 설정에서는 트위터 관련 토큰들이 엔드포인트 URI에 키-값으로 지정됨으로 엔드포인트 URL이 길어지는 것을 방지하기 위해, twitter 컴포넌트의 팩토리 Bean에 해당 값들을 속성으로 지정했다. 위 라우팅 정의의 EIP 다이어그램은 다음과 같다.

프로그램을 컴파일하고 실행하기 위해서는 다음의 Maven 의존이 필요하다.

Camel Twitter 컴포넌트는 내부에서 Twitter4J 를 이용한다. Camel Twitter 컴포넌트의 의존이 필요한 관련 라이브러리를 자동으로 추가해 줌으로 애플리케이션에서는 별도로 추가할 의존은 없다. Eclipse에서 프로젝트를 열지 않는 경우, 다음과 같이 명령창에서 Maven을 이용하여 컴파일 및 실행이 가능하다. 단 프로그램 실행 전에 트위터 사이트로부터 받은 consumerKey, consumerSecret, accessToken, accessTokenSecret 값을 Twitter.xml에 올바르게 지정해야 한다.

다음은 ToTwitter 프로그램을 실행하여 필자가 수신한 트윗의 타임라인이다..


6. Camel의 애플리케이션 적용 패턴

지금까지 프로그램은 JVM 환경의 Java 애플리케이션에 Spring 프레임워크와 결합된 Camel을 내장하는 Camel의 사용 패턴을 사용했다. 이것은 Camel이 애플리케이션에 얼마나 쉽게 내장할 수 있는지를 보여 주는 하나의 예에 불과하다. Camel Core는 약 2.5M bytes 정도의 작은 크기를 가지면서도 POJO 방식을 지원하여 애플리케이션에 쉽게 내장될 수 있다. 이 작은 프레임워크는 애플리케이션에 내장되든, 독립된 애플리케이션으로 동작하던 애플리케이션들을 손쉽게 통합할 수 있게 해 준다. 이점이 기존 애플리케이션 통합 제품인 EAI 제품들이 일반적으로 메시징 미들웨어를 기반으로 통합하는 방식과 다른 점이다. 다음은 애플리케이션에서 Camel을 적용하는 몇몇 패턴들을 보여준다.


7. 맺음말

이 글에서는 애플리케이션이 Apache Camel 프레임워크로 외부 세계와 소통하는 예로 "Hello, world!" 메시지를 콘솔에 출력하는 애플리케이션에서부터 "Hello, world!" 메시지를 트위터의 타임라인으로 트윗하는 애플리케이션까지 개발해 보았다. 이 과정에서 애플리케이션은 일관되게 Camel의 ProducerTemplate 객체를 사용하여 "Hello, world!" 메시지를 전송했으며, Camel 설정의 수정을 통해 이 "Hello, world!" 메시지는 점점 더 복잡한 프로토콜을 가진 외부 시스템으로 전송되었다. 이 글의 마지막에 보인 애플리케이션처럼 애플리케이션을 트위터 시스템과 연동하게 하는 방법에 있어서, Camel보다 더 간단하게 애플리케이션을 트위터 시스템과 연동하게 할 수 있는 솔루션이나 EAI 제품이 있을까?

이 글을 꼼꼼히 읽은 독자라면 Camel이라는 통합 프레임워크의 가능성을 잘 이해했을 것이다. 즉 Camel 프레임워크를 이용해 애플리케이션에게 일관된 통합 인터페이스를 제공했고, 애플리케이션으로부터 통합 로직을 분리했으며, 외부 시스템의 변경은 설정으로 대응하도록 했다. 그러나 이 글에 등장하는 패턴은 기업 통합 패턴 중 하나인 메시징 게이트웨이(Messaging Gateway) 패턴을 적용한 것으로 볼 수 있으며, 또한 Camel의 많은 기능들 중 극히 일부분 이곳에 사용되었다. 이 글에서는 Camel의 능력을 과시(?)하기 위해 외부 시스템에 대한 인터페이스에 집중해서 Camel의 사용을 보여 주었지만, 사실 중요한 것은 어떻게 인터페이스할 것인가 보다 어떻게 통합할 것인가 이다. 즉 나무보다는 숲을 보는 통합 아키텍처 관점이 더 중요하다. 그런 관점에서 이 곳에 설명한 프로그램 예는 단지 기업 통합이라는 신세계의 입구를 본 것뿐이다.

일반적으로 우리는 프로시저 호출(procedure call) 방식의 동기 패러다임의 아키텍처에 익숙하다. 그러나 기업 통합 패턴(Enterprise Integration Patterns)은 메시지 전달(Messaging) 방식의 비동기 패러다임으로 기업 아키텍처를 접근한다. 이런 관점의 전환이 Apache Camel이라는 통합 프레임워크를 탄생시켰고, 지금도 난제로 여겨지고 있는 애플리케이션 통합에 신선한 전환점을 만들어 주고 있다.

Apache Camel은 Red Hat의 JBoss Fuse Middleware에도 포함되어 있다. JBoss Fuse는 오픈 소스로 구성된 메시징 서버(ActiveMQ), ESB 엔진(ServiceMix), 통합 프레임워크(Camel), OSGi 컨테이너(Karaf)의 애플리케이션 통합 미들웨어 제품군이다. JBoss Fuse는 기존 기업 내 애플리케이션 통합에 사용되던 독점적 폐쇄적 EAI 제품을 대체할 수 있다. 그리고 Red Hat에서 제품으로 출시되고 있으므로 필요한 경우 상용 제품과 동일한 수준의 기술 및 제품을 지원 받을 수 있다. 그러므로 오픈 소스인 Apache Camel에 대해 관심을 가진 누구라도 오픈 소스를 직접 다운받아 활용해 보거나, Red Hat을 통해 기업 통합에 필요한 컨설팅을 받을 수 있을 것이다. 필자의 회사도 Red Hat과 Fuse 제품의 기술 지원 파트너이다.

Spring 프레임워크의 창시자인 Rod Johnson이 2012년에 출판된 "Spring Integration In Action" 책의 서문 첫 문장에 다음과 같이 썼다. "Integration is currently a hot topic" (통합은 현재 뜨거운 주제이다.) 즉 미국에서도 현재 애플리케이션들 사이의 통합이 뜨거운 주제인 것이다. 그리고 Spring Source에서도 Apache Camel 보다는 늦었지만 Spring Integration 프레임워크를 만들어 열심히 발전 중에 있다. (참고로 필자는 Spring Integration의 채널 중심의 통합 접근 방법을 별로 좋아하지는 않는다.) 그리고 Camel이나 Spring Integration이나 그 사상은 모두 기업 통합 패턴(Enterprise Integration Patters) 책에 기반한다. (이 책은 필자가 번역해 "기업 통합 패턴, 에이콘 출판"으로 출간됐다.) 우리나라 사정은 어떤가? 우리나라도 애플리케이션들 사이의 통합이 심각한 문제이나, 전체 아키텍처 측면에서는 어떻게 접근해야 하는지 별다른 방안을 갖지 않는 것 같다. (물론 독점적 EAI 제품을 도입한다거나 스파게티 구조의 인터페이스를 가지기도 한다.) 통합에 있어서, 인터페이스를 개별적으로 고민하지 말고 전체 아키텍처를 고민해야 한다. 그래야 인터페이스의 추가에 따르는 개발 비용과, 유지보수에 따르는 비용을 절감할 수 있게 된다. 이런 통합 문제를 해결하기 위한 중심에 기업 통합 패턴(Enterprise Integration Patterns)이 있고 Apache Camel이 있다.


참고 사이트

2013년 7월 16일 화요일

Camel, Spring 기반 이메일 전송 라이브러리


1. 들어가며

전에 블로그에 올린 "Apache Camel 기반 이메일 전송 라이브러리"에서는 Apache Camel 프레임워크와 메일, Velocoty 컴포넌트를 이용하여 이메일 전송 라이브러리를 개발해 보았다. 이번 글에서는 동일한 라이브러리 개발에 Apache Camel 프레임워크를 Spring 프레임워크와 결합하여 개발하는 방법을 보이고자 한다.


2. Spring XML

Apache Camel은 메시지 라우팅을 다양한 DSL(Domain Specific Language, 도메인 특화 언어)로 기술할 수 있게 해준다. Camel이 Spring 프레임워크와 결합되는 경우, Camel은 Spring의 XML Schema Extension을 이용하여 Spring의 Bean 정의 XML 파일에 XML DSL로 메시지 라우팅을 설정할 수 있게 해준다. 이 XML 기반 DSL은 Java DSL의 거의 모든 기능을 그대로 지원하므로, Spring XML DSL을 이용하면, 메시지 라우팅을 설정하는 Java 클래스를 컴파일이 필요 없는 Spring XML 설정으로 옮겨 놓을 수 있다.


3. 메시지 라우팅

"Apache Camel 기반 이메일 전송 라이브러리"에서는 메시지 라우팅을 RouteBuilder 인터페이스를 구현한 Java 클래스로 설정했었다. 이번에는 Spring XML을 이용하여 메시지 라우팅을 설정할 것이다. Spring Bean 정의 XML인 MailSender.xml에 다음과 같이 Camel Context와 메시지 라우팅을 정의한다. 이 설정 파일은 "Apache Camel 기반 이메일 전송 라이브러리"의 MailSenderBuilder 클래스의 메시지 라우팅을 설정을 대체한다.

메시지 엔드포인트인 MailSender 객체는 component-scan 태그를 사용하여 자동 주입했다. 참고로 MailSender 클래스는 이미 이전 글에서 @Service 어노테이션을 이용하여 Bean으로 정의했다. 그리고 MailSenderBuilder 클래스의 설정자(setter) 메소드들은 XML에서 속성으로 참조하게 구조를 변경했다. 이를 위해 Spring이 제공하는 PropertiesFactoryBean을 이용하여 Bean XML 정의 파일에 속성을 직접 정의했다. (이 기법은 Spring 프레임워크의 숨겨진 기능 중 하나이다.) 이 속성 Bean은 camelContext의 propertyPlaceholder에서 참조하게 한다. (Camel 프레임워크 웹 사이트 문서는 이 참조 방법을 잘 설명하지 않는다.) 일단 이렇게 Bean 정의와 참조를 설정하고 나면, Camel의 설정에서는 속성 정보들을 {{}}를 이용하여 참조할 수 있게 된다. MailSenderBuilder 클래스의 configure 메소드에 설정된 메시지 라우팅은 Spring XML의 route 태그에 XML로 정의한다. 이렇게 함으로 MailSenderBuilder 클래스는 모두 Spring XML로 대체된다. 이제 Camel Context는 MailSenderBuilder 클래스가 없더라도 메시지를 라우팅할 수 있게 된다.


4. 메시지 엔드포인트

메시지 엔드포인트는 "Apache Camel 기반 이메일 전송 라이브러리"에서 구현한 클래스인 MailSender.java를 재사용한다..

이전 글을 올릴 때, 이미 Spring 프레임워크와의 결합을 고려했었다. 이때 이미 Spring의 @Service 어노테이션을 포함했다. 또 Camel의 @Produce 어노테이션은 Camel의 ProducerTemplate 객체를 자동으로 주입한다.


5. 테스트

테스트 실행에 필요한 라이브러리는 Maven의 pom.xml로 포함시켰다. 다음은 Camel과 Spring 프레임워크를 결합시킬 때 필요한 Maven의 라이브러리 의존 설정이다. 의존 라이브러리 전체는 "프로그램 소스"의 pom.xml 파일을 참조한다.

Spring 프레임워크를 사용하므로 Spring Test 어노테이션을 이용하는 JUnit 테스트 클래스를 작성한다. 다음은 Spring Test 어노테이션을 사용한 MailSenderSpringTest.java 클래스이다.

Spring Bean 정의 XML 파일인 MailSender.xml은 Spring이 제공하는 @ContextConfiguration 어노테이션을 사용하여 소스에 바로 지정한다. MailSender 객체는 Spring의 @Autowired 어노테이션으로 자동 주입된다. Test 메소드는 이전에 Java와 Camel 기반 이메일전송 라이브러리의 메소드와 거의 동일하다. 단지 설정자(setter) 메소드들이 XML 설정에서 속성으로 변경되었으므로 설정자를 호출하는 부분이 테스트 소스에서 사라졌다. 주입된 MailSender 객체에 메일 발신에 필요한 정보와 메일 본문 템플릿 입력 개체를 설정하고 발신하면 테스트는 완료된다.


6. 이전 라이브러리와 비교

Spring 프레임워크를 사용함으로 "Apache Camel 기반 이메일 전송 라이브러리"에서 사용된 메시지 라우팅 클래스는 제거되었고, 대신 컴파일이 필요 없는 Spring Bean 정의 XML 파일에 메시지 라우팅이 설정되었다. 컴파일이라는 단단한 결합을 설정 파일이라는 느슨한 결합으로 전환한 것이다. 이메일 전송 엔드포인트 클래스는 재활용되었다.

동일하게 이메일 전송을 처리하는 라이브러리로 이번 버전은 Java 소스는 줄었고, XML 설정은 추가 되었다. DSL이 Java 소스에서 XML로 바뀐 것이다. 그러나 Spring 프레임워크의 장점을 Camel에서 활용할 수 있는 구조가 되었다. Camel은 Spring의 Context는 컴포넌트 레지스트리로, Bean 객체는 메시지 엔드포인트로 활용한다.

Spring 프레임워크는 J2EE 기반 솔루션을 대체할 수 있는 강력한 엔터프라이즈 프레임워크이다. 그리고 대부분의 프레임워크들은 Spring 프레임워크와의 인터페이스를 지원한다. (모든 프레임워크는 Spring 프레임워크로 통한다!) Camel도 역시 Spring 프레임워크와 잘 결합된다.


7. 맺음말

Camel과 Spring을 이용하여 메시지 라우팅을 XML 설정으로 변경했고, Spring 어노테이션과 자동 주입을 이용하여 객체들의 초기화 과정을 생략하여 애플리케이측 소스를 좀더 간결하게 만들었다. 이렇게 Camel과 Spring 프레임워크는 서로 잘 결합되고 각 프레임워크의 장점을 서로 융합한다. 그러므로 Camel과 Spring 프레임워크를 결합하는 방식으로 애플리케이션에서 사용할 것을 권장한다.

기업 통합 패턴이 처음 패턴 언어로서 정리됐을 때, 우리들은 아직 통합 프레임워크를 갖지 못했었다. 그 결과 패턴을 이용한 통합의 구현은 애플리케이션을 개발하는 개발자가 직접 개발하거나 EAI 솔루션이 제공하는 방법에 따라 개발해야 했다. 그러나 모듈과 모듈, 애플리케이션과 애플리케이션, 시스템과 시스템, 애플리케이션과 미들웨어 등 기능과 기능을 중재(mediation)하는 기업 통합 패턴의 사상이 녹아 든 Apache Camel이나 Spring Integration과 같은 통합 프레임워크(Integration Framework)가 등장하면서 애플리케이션의 통합에 위의 이메일 전송 라이브러리처럼 극적인 단순함과 생산성, 유연성을 제공할 수 있게 되었다. 그리고 또다시 이 통합 프레임워크는 애플리케이션 프레임워크인 Spring 프레임워크와 결합함으로 장점이 더 극대화된다.

Camel, Spring의 또 다른 예로 바른모 사이트의 "기상청 사이트 서울 날씨 주간 예보 조회 - Spring Framework"를 참조하기 바란다.

이 라이브러리를 실제 업무에 사용하려면 예외 처리나 특정 환경에 맞게 로직들을 추가해야 할 것이다. 이 라이브러리는 기본적인 기능만 제공하기 때문이다. 그럼에도 불구하고 이보다 더 간결하고 확장 및 수정이 가능한 이메일 전송 라이브러리를 만들 수 있는 다른 프레임워크가 있을 수 있을까? 이렇게 Camel과 Spring은 애플리케이션 라이브러리 개발에 아주 유용한 도구이다.


참고 사이트

2013년 7월 11일 목요일

Apache Camel 기반 이메일 전송 라이브러리


1. 들어가며

"기업 통합 패턴과 메일러 애플리케이션"이란 이전 글에서 메일 발신을 기업 통합 패턴의 관점에서 해석하고 간단한 메일러 배치 애플리케이션을 개발하는 과정을 보였다. 이 메일러 애플리케이션은 독립된 애플리케이션으로 데이터베이스와 메일 서버를 통합한다. 그럼 애플리케이션들 사이의 통합이 아닌 애플리케이션 내부의 기능들의 통합에도 기업 통합 패턴을 이용할 수 있을까? 이번 글은 이 문제를 다루어 보려고 한다.


2. 상황

어떤 기업에서 고객 관리 애플리케이션을 개발하고 있다고 가정해 보자. 이 애플리케이션의 주요 기능은 고객 관리 기능이다. 그런데 필요한 경우 고객에게 안내 이메일을 전송해야 한다. 그런데 이메일 전송에 필요한 고객의 정보들은 이미 애플리케이션에서 관리하고 있다. 그러므로 현재 개발 중인 고객 관리 애플리케이션의 입장에서는 애플리케이션으로부터 전달 받은 고객 정보를 이용하여 메일을 전송하는 라이브러리가 있으면 된다. 그런데 안내 이메일의 특성상 메일 본문이 자주 변경될 수 있다. 그리고 메일 발신 대상 고객마다 별도의 메일 본문을 생성하기에는 안내 내용이 너무 크다. 즉 이럴 경우 메일 전송 요청을 위해 불필요한 저장 공간이 사용된다. 그러므로 메일 전송 라이브러리는 고객 정보와 안내 메일의 본문을 분리할 수 있어야 한다.


3. 요구 분석

개발하고자 하는 고객 관리 애플리케이션의 메일 전송 라이브러리는 다음과 같은 요구를 충족해야 한다.

  • 1) 메일 발신 기능이 필요하다.
  • 2) 메일 발신에 필요한 정보는 애플리케이션에서 모두 전달한다.
  • 3) 메일 본문은 별도의 템플릿을 관리해야 한다.
  • 4) 애플리케이션에서 사용할 수 있는 라이브러리로 개발돼야 한다.

여기에 나열된 요구들은 고객을 관리하는 애플리케이션들에게서 흔하게 볼 수 있는 요구들이다.


4. 기업 통합 패턴 설계

기업 통합 패턴의 관점에서 메시지는 애플리케이션으로부터 메일 전송 라이브러리를 거쳐 메일 서버로 전달된다. 이를 위해 애플리케이션에서 메시징 시스템으로 데이터를 전달하기 위한 "메시지 엔드포인트"가 필요하다. 즉 애플리케이션에서 메시징 시스템을 액세스하는 얇은 API 계층이 필요하다. 그리고 이 라이브러리 내에는 메일 본문 템플릿에 고객 정보를 보태는 필터가 있어야 한다. 그리고 메일 전송 컴포넌트가 필요한데, 이 컴포넌트는 이전에 블로그에 올린 "에서도 사용된 컴포넌트이다. 이런 컴포넌트들을 포함한 아키텍처를 기업 통합 패턴의 EIP(Enterprise Integration Patterns) 다이어그램으로 그리면 다음과 같이 그릴 수 있을 것이다.

이 다이어그램은 애플리케이션과 메시징 시스템을 연결하는 "메시지 엔드포인트", 메일 전송과 관련된 고객 정보와 메일 본문을 결합하는 "내용 보탬이(Content Enricher), 메일 서버에게 메일을 전달하는 메일 컴포넌트의 메일 엔드포인트로 구성된다.


5. 사용 기술

메일 전송 애플리케이션을 개발하기 위해서 두 기술이 필요하다. 첫째, 메일 전송 기술이다. 이 기술은 Camel 메일 컴포넌트를 활용할 수 있다. 둘째, 템플릿 활용 기술이 필요하다. 이 기술 중 널리 알려진 기술은 Apache Velocity 템플릿 엔진이다. Camel은 Velocity 컴포넌트를 통해 이 기술을 이용할 수 있게 해준다. 각 Camel 컴포넌트에 대한 설명은 다음 웹사이트를 참조한다.

필자는 Eclipse 개발 환경에서 m2eclipse(maven plugin)을 사용하는 것을 좋아한다. 왜냐면 사용하는 라이브러리들을 자동으로 포함시켜 주기 때문이다. 위 두 Camel 컴포넌트 라이브러리와 관련 jar들은 다음 두 엘리먼트를 pom.xml 의 의존 엘리먼트 영역에 추가하면 maven repository에서 다운받을 수 있다.


6. 메시지 라우팅 구현

이제 간단하게 메시징 흐름 설계(?)를 마쳤으므로 본격적으로 구현(?)해 보자. 다음 클래스는 위 EIP 다이어그램에 따라 메시지 라우팅을 Camel 도메인 특화 언어(DSL, Domain Specific Language) 중 Java DSL로 정의한 MailSenderBuilder .java이다.

위 클래스는 Apache Camel의 RouteBuilder 인터페이스를 구현하여 메시지 라우팅을 정의한다. 이 클래스는 메일 서버의 접속 정보는 설정자(setter)를 통해 주입 받는다. Spring 프레임워크를 고려하여 설정자를 노출한 것이다. 위의 소스의 라우팅 정의에서 to("velocity 로 시작하는 메시지 소비자(내용 보탬이)의 정의를 보면, 메시지 라우팅은 Apache Velocity 엔진 컴포넌트를 이용하고 있음을 알 수 있다. 단 한 줄로 Velocity 기능을 추가했다! Velocity 템플릿인 letter.vm 파일의 위치는 클래스 패스 아래 camel/example/client/template/letter.vm 이다. 참고로 템플릿 파일의 위치는 메일 접속 정보를 설정자를 주입하는 것처럼, 설정자를 외부에서 주입되게 바꾸는 것은 어렵지 않다. 메일 전달 기능도 smtp 소비자 엔드포인트 한 줄로 추가한다! 발신되는 메일의 형식이 HTML 형식의 이메일임을 지정하기 위해 URI의 입력 파라미터로 "contentType=text/html"을 지정했다.


7. 메시지 엔드포인트 구현

Apache Camel Context 객체는 자체가 컨테이너이면서 메시징 시스템이다. 그러므로 메시지 엔드포인트가 애플리케이션이 Camel의 메시징 시스템으로 데이터를 전달할 수 있게 API를 노출해야 한다. 이를 위해 구현된 메시지 엔드포인트 클래스가 MailSender.java이다.

MailSender 클래스는 semd 메소드를 애플리케이션에게 노출한다. 이 메소드는 header 파라미터로 이메일 전송 정보를 입력 받고, model 파라미터로 메일 본문의 입력할 고객 정보를 입력 받는다.


8. 메일 템플릿

메시지 라우팅 정의에서 언급했듯이 메일의 본문은 HTML로 작성해야 한다. HTML 템플릿은 Velocity의 템플릿 파일인 "letter.vm"로 저장된다. 이 파일은 메일을 전송하기 전에 생성하거나 수정한다. 이 파일은 메시지 라우팅 정의에 따라 클래스 패스 아래 camel/example/client/template/letter.vm에 놓인다. 다음은 필자가 개콘의 황해를 흉내 낸 안내 메일 템플릿이다.

위 파일에서 ${body.name} 부분은 MailSender.send 메소드의 model 파라미터에 "name" 키로 put한 값으로 대체된다. 동일한 방법으로 대체하고자 하는 곳에 ${} 안에 "body." 을 접두사로 하는 키 값을 지정한다.


9. 테스트

벌써 개발이 완료되었다. 이제 테스트를 해보자. 테스트는 JUnit을 활용한다. 다음은 MailSender 클래스를 테스트하는 MailSenderTest.java이다.

필자가 개발한 메일 전송 라이브러리는 Camel 프레임워크를 사용하므로 setup 메소드에서 Camel Context를 생성하고, 메시지 라우팅 로직을 추가하고, 테스트하고자 하는 MailSender 객체를 생성하고, ProducerTemplate 객체를 생성하여 MailSender 객체에 전달한다. 그리고 test 메소드에서 메일 발신과 관련된 정보와 고객 정보를 추가하여 MailSender 객체의 send 메소드를 호출한다. 소스가 보이는 것처럼 복잡하지 않다.


10. 테스트 결과

필자는 테스트 결과로 다음과 같은 메일을 수신했다.




11. 타 구현과의 비교

인터넷을 검색해 보면 이메일 전송에 Apache Velocity 템플릿 엔진을 이용하는 방법에 대하여 설명한 사이트들이 꽤 나온다. 이 사이트들은 주로 Spring 프레임워크를 이용하여 구현하고 있다. 왜냐면 Java Mail API를 직접 사용하는 것보다 Spring API를 이용하는 것이 코드 량을 줄여 주기 때문이다. 이런 사이트들은 Spring 프레임워크를 활용함으로 Bean 정의와 Spring 프레임워크의 메일 전송 API, Velocity API를 호출하는 방법을 설명한다. 즉 프레임워크의 사용과 코딩 방법을 중심으로 설명한다. 이를 확인할 수 있도록 참고 사이트에 구현 사이트 중 한 곳의 링크를 걸어놓았다.

이에 반해 필자는 동일한 문제에 대해 기업 통합 패턴의 시각에서 데이터의 흐름 즉 메시지 라우팅의 관점으로 문제를 접근했다. 그리고 메시지 라우팅을 수립하고 나서 실제 코딩을 진행했다. 이 접근 방법의 장점은 문제에 대한 해결을 미시적인 구현에 집중하는 것이 아니라 전체적인 데이터의 흐름을 볼 수 있게 해준다는 점이다. 또 이렇게 파악된 메시지 흐름을 Camel DSL로 간결하게 표현함으로 코딩의 량을 극적으로 줄일 수 있었다. 메일 전송에 구현된 로직의 코딩은 실제 60줄도 채 안된다. 그리고 추가적인 장점으로 메시지 라우팅은 컴포넌트들 사이를 느슨한 결합(loose coupling)으로 정의한 것이므로, 이곳에서 구현된 메일 전송 라이브러리는 향후 추가 또는 변경되는 요구들에 대해서도 신속하게 대응할 수 있게 되었다.


12. 맺음말

이 글에서는 애플리케이션 내부에서의 기능 통합으로 볼 수 있는 메일 전송 라이브러리를 개발해 보았다. 기업 통합 패턴을 이용하여 문제를 접근하였고, 구현은 Apache Camel 프레임워크를 이용하여 구현하였다. 제시된 요구대로 Velocity 템플릿 엔진을 이용하여 메일의 본문을 별도 파일로 분리함으로 프로그램과 메일 본문 사이의 단단한 결합(tight coupling)을 제거하였다. 그러면서도 소스는 아주 간결하게 구현하였다. 이 과정을 통해 기업 통합 패턴이 애플리케이션이 활용하는 라이브러리에도 잘 활용될 수 있음을 보여주었다.

여기에 구현된 메일 전송 라이브러리는 기업 통합 패턴 중 "내용 보탬이(Content Enricher)" 패턴의 전형적인 예이다. 그리고 이 내용 보탬이 패턴을 Apache Camel 프레임워크가 제공하는 Velocity 컴포넌트를 이용하여 간결하게 구현하였다. 이렇게 일반적인 애플리케이션 내부의 통합 문제에 있어서도 Apache 프레임워크를 이용하면 다른 어떤 프레임워크를 사용하는 것보다 더 간결하게 프로그램 할 수 있다. 물론 Apache Camel 프레임워크를 잘 활용하기 위해서는 기업 통합 패턴을 잘 알아야 한다.

애플리케이션을 개발(통합)한다는 것은 각 기능들을 구현, 조립 또는 상속하여 상위의 서비스를 제공하는 것을 말한다. 이 기능들의 개발(통합)은 거시적으로는 기업 통합(Enterprise Integration)의 형태로 미시적으로는 애플리케이션 통합(Application Integration)의 형태로 진행된다. 그리고 기업 통합 패턴은 거시적이든 미시적이든 애플리케이션 개발(통합)의 모든 경우의 분석과 설계에 유용하고, Apache Camel 프레임워크는 이 모든 경우의 구현에 훌륭한 도구이다.


참고 사이트

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


참고 사이트

2013년 6월 21일 금요일

MyBatis 프레임워크의 트랜잭션 제어


 MyBatis 프레임워크는 관계형 데이터베이스의 레코드와 자바 도메인 객체 사이의 매핑을 자동화 해주는 ORM(Object Relation Mapping) 프레임워크로 데이터베이스의 SQL 문을 거의 그대로 사용할 수 있게 해준다. 특히 J2EE의 엔티티 빈(entity bean)이나 하이버네이트(Hibernate) 프레임워크에서는 (견해에 따라) 활용하기 어려운 집계나 조인도 데이터베이스의 SQL을 그대로 사용함으로 쉽게 활용할 수 있게 해준다.

 MyBatis 프레임워크는 오랫동안 사랑받아 왔던 ORM 프레임워크인 iBatis 프레임워크를 동일한 개발자들이 내부 구조를 재설계하여 새롭게 만든 ORM 프레임워크이다. (그래서 내부적으로는 iBatis라는 패키지 이름이 계속 사용되고 있다. 심지어 버전도 이어 받고 있다.) MyBatis는 iBatis의 기본 골격을 거의 그대로 유지하고 있으므로, iBatis에 익숙한 개발자라면 MyBatis로 적응하는 것이 그렇게 어렵지 않을 것이다. 필자도 MyBatis 사용자 가이드를 보면서 간단한 MyBatis 프로그램을 어렵지 않게 작성할 수 있었다. 동작하는 MyBatis 애플리케이션을 작성하는 데 하루 정도 걸린 것 같다.

트랜잭션 관리

 최근 프레임워크들은 선언적 트랜잭션 관리(DTM, declarative transaction management)라든지 컨테이너 관리 트랜잭션(CMT, Container Managed Transaction)들의 지원에 많은 노력을 기울이고 있다. 선언적 트랜잭션 관리는 어노테이션 등을 사용하여 메소드나 클래스 단위의 트랜잭션의 흐름을 정의하는 방법이고, 컨테이너 관리 트랜잭션은 컨테이너 서버의 설정으로 컨테이너 내 객체들의 트랜잭션을 관리하는 방법이다. 그런데 선언적 방법 또는 컨테이너 관리 방법 모두 데이터베이스를 연동하는 애플리케이션 개발자들에게 익숙하지 않다는 점이 문제가 된다. 그 결과 이 두 방법으로는 개발자가 트랜잭션이 잘 처리 되었는지 안되었는지 확신하기가 어려울 수 있다. 왜냐면 선언적 방법에서는 어떻게 선언하느냐에 따라 트랜잭션의 범위나 적용 방법이 달라지게 되는데 이것을 개발자가 쉽게 이해하지 못할 수 있고, 컨테이너 기반의 트랜잭션 관리는 J2EE의 엔티티 빈이나 CMT 기반 세션 빈의 실패로 이미 그 개발자들의 적응에 문제가 있음이 증명되었다고 볼 수 있다. 그래도 여전히 이 두 방법이 추구되는 이유는 세상의 모든 데이터를 객체로 다루고 싶은 사람들의 주장이나 이상의 입김이 계속 업계로 작용하는 것이 아닐지 생각해 보게 된다. 아무리 좋은 기술이더라도 개발자가 쉽게 이해하고 사용하고 확인할 수 없다면 좋은 기술이라고 볼 수 없는데, 뛰어나거나 위대한 그루(?)들이 개발자들을 이끌려는 방향이 너무 이상적인 것 같다. 어쨌든 DTM이든 CMT이든 개발자들에게는 너무 어렵다는 것이 필자가 가진 일관된 의견이다.

 그럼 트랜잭션을 프로그램으로 관리하는 것은 어떤가? 트랜잭션을 프로그램을 관리하는 방법은 수십 년 동안 개발자들이 사용해 오던 방법이다. 이 방법을 사용하면, 데이터베이스 처리 로직이 중첩 또는 내포 형식의 복잡한 구조를 갖지 않는 일상적인 구조인 경우, 개발자들은 누구든지 쉽게 트랜잭션의 범위를 명시적으로 프로그램으로 지정할 수 있고, 이렇게 명시적으로 지정된 트랜잭션의 범위는 일반적으로 쉽게 파악된다. 그리고 데이터베이스와 관련된 비즈니스 로직들은 단순한 구조를 갖는 경우가 대부분이다. 더욱이 긴 절차가 필요한 비즈니스에서 일정 절차까지의 진행 상황을 커밋해야 하는 경우엔, 프로그램으로 관리하는 데이터베이스 트랜잭션 방법이 더 적합하다. 선언적 방법으로 진행해야 하는 경우 각 메소드마다 트랜잭션의 전파를 고려해야 하는 어려움이 따르고, 컨테이너 관리 방법도 긴 절차의 트랜잭션을 관리하기 위해서는 더욱 복잡하게 각 단계마다 세션 빈과 같은 것들을 만들어야 한다. 그렇기 때문에 최신 프레임워크들이 DTM이나 CMT을 제시함에도 불구하고 개발자들에게는 프로그램으로 관리하는 데이터베이스 트랜잭션이 여전히 선호되고 있다. 그리고 iBatis 프레임워크는 이런 기본적인 기능에 충실한 프레임워크 중 하나였다.

iBatis의 장점

 iBatis 프레임워크는 데이터베이스 트랜잭션에 있어서 질의 메소드 단위의 자동 커밋과 필요한 경우 언제든지 프로그램으로 트랜잭션의 범위를 지정하게 해주는 유연성을 동시에 제공한다. 그리고 데이터베이스 처리와 관련된 자원도 자동으로 해제한다. 다시 말해 iBatis 프레임워크를 사용면 JDBC API를 다루는 경우 발생하는 connection 객체나 statement 객체들을 해제하기 위해 호출해야 하는 close() 메소드들을 호출하지 않아도 된다. 자원 해제 문제는 애플리케이션의 안정성과 가용성에 아주 중요한 문제를 야기할 수 있다. 제대로 해제 되지 않는 자원 객체들은 자원의 고갈이나 메모리 부족의 문제를 야기시키고 결과적으로 애플리케이션의 다운 문제로 확대될 수 있기 때문이다. iBatis를 사용하면 데이터베이스 연동에서 발생하는 이런 자원 샘 현상도 방지할 수 있다.

 iBatis 프레임워크를 사용하면 데이터베이스를 연동하는 코드를 다음과 같이 간단하게 작성할 수 있다.
…
try {
      sqlMapClient.startTransaction();
      sqlMapClient.update("insertAccountViaParameterMap", account);
      sqlMapClient.commitTransaction();
    } finally {
      sqlMapClient.endTransaction();
    }
…
 위 코드처럼 iBatis를 사용하면 자원 해제를 위해 close() 메소드들을 직접 호출하지 않아도 되며, 필요한 곳에서 언제든지 데이터베이스 트랜잭션을 시작하고 종료할 수 있다. 단 finally 블록 안에 트랜잭션을 종료하는 로직을 반드시 추가하여 트랜잭션이 닫히지 않는 것을 방지해야 한다. 이 코드에서 볼 수 있듯이 iBatis는 데이터베이스 연동에 필요한 코드를 상당히 줄여 줄 뿐만 아니라, 데이터베이스 트랜잭션도 자유롭게 지정할 수 있게 하고, 소스와 SQL 쿼리를 분리함으로 이기종 데이터베이스의 이식성도 좋게 한다. 결과적으로 데이터베이스 비즈니스 개발의 생산성을 향상시킨다. 그리고 이런 장점들이 MyBatis로 이어져 발전하고 있다. 또 위 코드에서 트랜잭션 관련 코드들을 제거하더라도 iBatis가 메소드 단위의 커밋을 지원하게 설정된 경우 메소드가 성공적으로 호출되면 자동으로 커밋까지 실행된다. 그러므로 이 경우 데이터베이스 처리 로직이 단 하나의 갱신이나 추가 메소드를 호출하는 경우 트랜잭션 과련 로직을 굳이 추가하지 않아도 된다. 즉 테이블에 입력할 정보가 준비된 경우, 아래와 같이 단 한 줄로 데이터베이스에 레코드를 추가할 수 있게 된다.
…
sqlMapClient.update("insertAccountViaParameterMap", account);
…

MyBatis의 변화

 그런데 MyBatis에 와서 이상한 변화가 생겼다. 첫째, 그동안 스레드 안전한 SqlMapClient 클래스가 사라졌다. SqlMapClient 객체 대신 스레드 안전하지 않는 SqlSesssion 객체를 사용하여 질의를 수행한다. (SqlSesssion 객체가 스레드 안전하지 않은 이유는 요청(request) 또는 메소드(method) 범위의 객체이기 때문이다.) 또 SqlSession 객체는 자원 해제를 위해 반드시 close() 메소드를 호출해야 한다. 나아진 점도 있다. 데이터베이스의 트랜잭션을 시작하는 메소드가 없어도 트랜잭션을 처리할 수 있다. (즉 openSession 메소드의 호출과 더불어 필요에 따라 자동으로 트랜잭션이 시작된다.) 그 결과 MyBatis API를 사용한 데이터베이스를 연동하는 코드가 다음과 같이 작성된다.
...
SqlSession session = sqlSessionFactory.openSession();
try {
// following 3 lines pseudocod for "doing some work"
    session.insert(…);
    session.update(…);
    session.delete(…);
    session.commit();
} finally {
    session.close();
}
...

 위 코드에서 볼 수 있듯이 MyBatis 프레임워크를 사용하는 경우 사용을 마친 SqlSession 객체는 반드시 닫아 주어야 한다. 즉 finally 블록 등에서 반드시 session.close()를 호출해야 한다. iBatis에서는 없던 호출을 MyBatis에서는 넣어 주어야 한다.  의아한 것은 왜 자원 해제의 누락으로 애플리케이션의 안정성을 해칠 수도 있는 로직을 개발자에게 떠 넘겼냐 하는 것이다. 또 close()를 호출하지 않는 방법을 전혀 제공하지 않는 것도 아니다. MyBatis 프레임워크를 Spring 프레임워크와 결합하여 SqlSessionTemplate 클래스 객체를 사용하면 자원 해제에 대한 문제가 사라진다.

 MyBatis 프레임워크는 Spring 프레임워크에서 MyBatis를 통합하여 사용할 수 있게 MyBatis-Spring를 제공한다. MyBatis-Spring를 통해 만들어진 SqlSessionTemplate 객체는 SqlSession 객체와 달리 내부적으로 인터셉터를 통해 자동으로 close()를 호출하여 자원 해제 문제를 해결한다. 개발자는 SqlSessionTemplate 객체를 사용하는 경우 더 이상 MyBatis의 자원 해제 문제를 신경 쓰지 않아도 된다. 그런데 대신 MyBatis-Spring은 다른 문제를 야기한다. 데이터베이스 트랜잭션의 문제이다. MyBatis-Spring을 사용하는 경우, MyBatis의 SqlSessionTemplate 객체는 commit(), rollback() 메소드를 사용할 수 없다. 즉 SqlSessionTemplate 객체를 이용하여 프로그램적으로는 트랜잭션을 관리할 수 없게 한다. 억지로 SqlSessionTemplate 객체의 commit() 메소드를 호출하면 다음과 같은 예외를 발생한다.
java.lang.UnsupportedOperationException: Manual commit is not allowed over a Spring managed SqlSession
 at org.mybatis.spring.SqlSessionTemplate.commit(SqlSessionTemplate.java:278)
 at com.brm.mybatis.MybatisSupportTest.testProgramacTraction(MybatisSupportTest.java:28)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:240)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:180)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


Spring 프레임워크의 트랜잭션 관리

 SqlSessionTemplate 객체를 이용하여 MyBatis의 자원 해제 문제를 해결하고 나니, 프로그램 방법의 데이터베이스 트랜잭션 관리가 불가능해지는 새로운 문제가 등장했다. 이 문제에 대해 MyBatis 문서는 Spring 프레임워크의 프로그램적 트랜잭션 관리 방법을 사용할 수 있다고 언급한다. 한 가지는 TransactionTemplate을 이용한 프로그램 방법이고 다른 가지는 PlatformTransactionManager를 사용한 프로그램 방법이 있다고 언급한다. 프로그램으로 데이터베이스 트랜잭션을 관리하려고 했더니, Spring 프레임워크를 이용하라고 한다. 이 부분이 참 의아한 부분이다. 이전에 iBatis에서 제공하는 SqlMapClient 객체는 이런 번거로운 작업 없이도 자원 해제와 트랜잭션을 모두 자체적으로 처리를 해 주었는데, 왜 MyBatis에서는 이렇게 바뀌었는지 굳이 Spring 프레임워크에 의존해야 했는지, 그런 경우라도 기본 구현을 내부에 포함하면 될 일인데, 굳이 MyBatis를 사용하는 개발자들에게 Spring 프레임워크를 사용하여 직접 구현하게 할 필요가 있었는지, 필요한 다른 이유가 있었던 것인지, MyBatis 커미터들이 게으른 것인지?. 어쨌든 현실은 적응해야 한다.

 필자는 Spring 프레임워크의 데이터베이스 관련 Template 시리즈 클래스 객체들을 아주 싫어한다. 그 이유는 콜백처리 때문이다. 질의 하나를 호출하려고 해도 콜백 방식을 사용해야 한다. (필자가 데이터베이스 처리에 있어서 Spring 프레임워크보다 iBatis 프레임워크를 더 선호했던 이유가 바로 iBatis에서는 일반적인 질의에 콜백을 사용하지 않기 때문이기도 했다.) Spring의 TransactionTemplate도 예외는 아니다. TransactionTemplate를 사용하려면 반드시 콜백 방식을 사용해야 한다. 다음은 TransactionTemplate를 사용하는 Spring 문서의 코드에 TransactionTemplate 객체를 사용하는 코드 부분을 추가한 예이다.
public class SimpleService implements Service {
    
    private final TransactionTemplate transactionTemplate;
    
    public SimpleService(PlatformTransactionManager transactionManager) {
        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        
        // the transaction settings can be set here explicitly if so desired
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
    }
    ...

    public int insertUser(final User user) {
        //use TransactionCallback handler if some result is returned
        return    transactionTemplate.execute(new TransactionCallback<Integer>() {
            public Integer doInTransaction(TransactionStatus paramTransactionStatus) {
                String inserQuery = "insert into users (username, password, enabled , id) values (?, ?, ?, ?) ";
                Object[] params = new Object[]{user.getUserName(), user.getPassword(),user.isEnabled(),user.getId()};
                int[] types = new int[]{Types.VARCHAR,Types.VARCHAR,Types.BIT,Types.INTEGER};
                return jdbcTemplate.update(inserQuery,params,types);
            }
        });
    }
}
 프로그램적으로 데이터베이스 트랜잭션을 처리하기 위해 이렇게 복잡하게 일을 해야 하다니, MyBatis 프레임워크는 프로그램 방법의 데이터베이스 트랜잭션을 Spring에 떠 넘기고 Spring 프레임워크는 여전히 자신들의 콜백 방식을 고집한다.

 콜백이 없는 두 번째 방법으로 PlatformTransactionManager를 사용하는 방법이 있다. 다음은 PlatformTransactionManager를 사용하는 Spring 문서의 코드 예이다.
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can only be done programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
  // execute your business logic here
}
catch (MyException ex) {
  txManager.rollback(status);
  throw ex;
}
txManager.commit(status);
 위 코드도 여전히 복잡하다. iBatis에서는 프로그램에서 데이터베이스 트랜잭션을 사용할 때 startTransaction(), commitTransaction(), rollbackTransaction(), endTransaction() 메소드들만 호출하면 됐었는데. 위 코드에서 보다시피 데이터베이스 트랜잭션이 필요한 경우 몇 개의 객체들을 추가로 생성하고 관련 메소드들을 호출해야 한다.

문제 해결

 어떻게 하면 MyBatis도 자원 해제에 대해 자유로워 지고, 편리하게 프로그램으로 데이터 트랜잭션을 관리할 수 있을까? 구글링을 해보고 관련 문서들을 다 찾아 보았지만 원론적인 설명 밖에는 찾을 수 없었다. 그래서 직접 이 문제를 해결하기로 했다.

 필자도 MyBatis 프레임워크와 Spring 프레임워크를 결합하여 사용하는 것을 권장한다. 이 경우 일차적으로 자원 해제 문제가 해결된다. 남은 문제는 프로그램으로 관리할 수 있는 편리한 데이터베이스 트랜잭션 방법을 찾아 내는 것이다. 필자는 소스 코드의 흐름의 방해하는 콜백 방식을 사용하지 않는 PlatformTransactionManager을 사용하여 이 문제를 해결했다.

 문제 해결 전략은 DefaultTransactionDefinition, PlatformTransactionManager, TransactionStatus 클래스를 묶어 내부적으로 처리하는 새로운 클래스를 정의하고, iBatis의 SqlMapClient 클래스 객체에서 호출하는 트랜잭션 절차 메소드들을 이 새로운 클래스에서 노출시키고, 이 과정에서 Spring 프레임워크의 빈 자동 주입, 프로토타입 빈 등을 사용하고, 위임(delegation)과 상속(inheritance)를 적절히 이용하여 추가되는 코드 량을 최소화 하는 것이었다. 이런 전략을 바탕으로 MyBatisTransactionManager 클래스와 지원 클래스인 MyBatisSupport 클래스를 다음과 개발했다.

MyBatisTransactionManager.java
package com.brm.mybatis.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
@Scope("prototype")
public class MyBatisTransactionManager extends DefaultTransactionDefinition {

 private static final long serialVersionUID = -1375151959664915520L;

 @Autowired
 PlatformTransactionManager transactionManager;

 TransactionStatus status;

 public void start() throws TransactionException {
  status = transactionManager.getTransaction(this);
 }

 public void commit() throws TransactionException {
  if (!status.isCompleted()) {
   transactionManager.commit(status);
  }
 }

 public void rollback() throws TransactionException {
  if (!status.isCompleted()) {
   transactionManager.rollback(status);
  }
 }

 public void end() throws TransactionException {
  rollback();
 }
}
 위 코드에서 MyBatisTransactionManager 클래스는 DefaultTransactionDefinition를 상속하여 DefaultTransactionDefinition를 위임할 경우 발생하는 수많은 위임 메소드들을 정의하지 않게 했고, MyBatisTransactionManager가 프로토타입 빈으로 정의되는 것을 감안하여 자유롭게 멤버 변수를 선언했고, transactionManager 멤버 변수는 Spring의 어노테이션 주입 기능을 이용하여 불필요한 설정자(setter) 메소드를 정의하지 않았다.

MyBatisSupport.java
package com.brm.mybatis;

import com.brm.mybatis.transaction.MyBatisTransactionManager;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

@Service("myBatisSupport")
public class MyBatisSupport {

 @Autowired(required = false)
 @Qualifier("sqlSession")
 protected SqlSessionTemplate sqlSession;

 @Autowired
 ApplicationContext applicationContext;

 public MyBatisTransactionManager getTransactionManager() {
  return applicationContext.getBean(MyBatisTransactionManager.class);
 }
}
 MyBatisSupport 클래스는 간단하지만 MyBatis를 이용하려는 애플리케이션 클래스들이 간단한 상속이나 주입만으로 언제든지 데이터베이스를 연동할 수 있는 SqlSessionTemplate 객체와 데이터베이스 트랜잭션을 프로그램적으로 지원하는 MyBatisTransactionManager 객체를 이용할 수 있게 해준다. 단 MyBatisSupport 클래스가 Spring 프레임워크의 어노테이션과 컨텍스트를 사용함으로 MyBatisSupport를 상속하는 애플리케이션 클래스들도 Spring 프레임워크 내에서 동작하게 해야 한다. 애플리케이션 클래스들을 Spring 프레임워크 내에서 동작하게 하는 방법은 Spring 프레임워크의 문서를 참조하기 바란다. 필자는 프로그램적 데이터베이스 트랜잭션을 데트스하는 JUnit MybatisSupportTest 클래스에 Spring-Test의 @RunWith와 @ContextConfiguration 어노테이션을 사용하여 Spring 프레임워크가 MybatisSupportTest 클래스의 Spring 어노테이션들을 자동으로 조사하여 객체를 주입하게 했다.

 다음은 MyBatisTransactionManager를 테스트하는 MybatisSupportTest 클래스 소스이다. MybatisSupportTest 클래스는 MyBatisSupport 클래스를 상속한다.

MybatisSupportTest.java
package com.brm.mybatis;

import com.brm.mybatis.transaction.MyBatisTransactionManager;

import java.sql.SQLException;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/mybatis.xml" })
public class MybatisSupportTest extends MyBatisSupport {

 @Test
 public void testProgramacTraction() throws SQLException {

  MyBatisTransactionManager transaction = getTransactionManager();
  try {
   
   transaction.start();

   List results = sqlSession.selectList("test.select");
   System.out.println("selected = " + results);
   int cnt = sqlSession.update("test.insert", results.get(0));
   System.out.println("inserted = " + cnt);

   transaction.commit();

  } finally {
   transaction.end();
  }
 }
}

 위 코드를 보면 SqlSessionTemplate 객체를 주입하는 부분도, 자원 해제를 위한 close() 메소드의 호출도 없다. 스레드에 안전한 SqlSessionTemplate 객체는 MyBatisSupport 지원 클래스에서 자동으로 주입되고, 자원 해제는 MyBatis-Spring 내부에서 인터셉터를 통해 자동으로 처리된다. 트랜잭션 매니저 객체인 MyBatisTransactionManager 객체는 MyBatisSupport 클래스의 getTransactionManager() 메소드 호출을 통해 Spring 프레임워크의 애플리케이션 컨테이너로부터 반환 받는다. MyBatisTransactionManager 객체는 iBatis SqlMapClient 객체의 데이터베이스 트랜잭션 처리 메소드들과 거의 유사한 메소드들을 애플리케이션에게 노출한다. 차이점은 iBatis SqlMapClient 객체는 내부적으로 트랜잭션 관련 인스턴스들을 생성하고 관리하는 반면 MyBatisTransactionManager 객체는 getTransactionManager() 메소드를 통해 획득된다는 점이다.

 다음은 MyBatis 프레임워크를 Spring 프레임워크와 함께 사용하기 위한 Spring 프레임워크의 빈 정의 XML인 mybatis.xml이다. 이곳에 MyBatisTransactionManager 클래스가 빈으로 등록된다. (어노테이션 스캔을 활용하는 경우, 이런 정의조차 없앨 수 있다.) 나머지 부분은 데이터 소스와 트랜잭션 관리자 SqlSessionTemplate과 관련된 빈들의 등록이다.

mybatis.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"
 xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
 xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

 <context:property-placeholder location="classpath:app.properties" />

 <!-- Data Source -->
 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="${datasource.driver}" />
  <property name="url" value="${datasource.url}" />
  <property name="username" value="${datasource.username}" />
  <property name="password" value="${datasource.password}" />

 </bean>

 <!-- Transaction Manager -->
 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
 </bean>

 <bean class="com.brm.mybatis.transaction.MyBatisTransactionManager" scope="prototype" />

 <!-- MyBatis Sql Session Template -->
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations">
   <list>
    <value>classpath*:${mybatis.query.locations}</value>
   </list>
  </property>
 </bean>

 <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
 </bean>
</beans>
 위 XML에서 MyBatisTransactionManager 빈은 스코프가 prototype이다. 그러므로 애플리케이션이 데이터베이스의 트랜잭션을 위해 MyBatisSupport 클래스의 getTransactionManager 메소드를 호출하면 Spring 프레임워크는 새로운 MyBatisTransactionManager 객체를 생성하여 반환한다.

 mybatis.xml에서 참조하는 설정 속성 파일인 app.properties는 다음과 같다. 테스트 데이터베이스는 오라클 데이터베이스를 사용했다.

app.properties
datasource.driver = oracle.jdbc.driver.OracleDriver
datasource.url = jdbc:oracle:thin:@192.168.1.50:1521:ORACLE
datasource.username = ******
datasource.password = ******
mybatis.query.locations =  com/brm/**/sql/*.xml

 MyBatis 프레임워크를 사용하여 데이터베이스 연동 로직을 개발 할 때, 위에 설명한 MyBatisTransactionManager 클래스와 MyBatisSupport 클래스를 추가하면, iBatis를 사용할 때와 유사한 방법으로 자원 해제와 데이터베이스 트랜잭션을 처리할 수 있게 된다. 또한 위 테스트 코드에서 MyBatisTransactionManager의 데이터베이스 트랜잭션 호출들을 제거하는 경우, MyBatis의 SqlSessionTemplate 객체는 내부에서 인터셉터를 통해 세션을 닫을 때 자동으로 커밋을 호출해 준다. 즉 데이터베이스 트랜잭션 처리 로직을 제거하면, SqlSessionTemplate의 메소스 호출 단위로 처리 성공 시 자동 커밋이 진행된다. 그러므로 데이터베이스 처리를 트랜잭션으로 묶을 경우 MyBatisTransactionManager 객체를 사용하고, 메소드 단위로 트랜잭션을 사용하는 경우 SqlSessionTemplate의 메소드를 단독으로 호출하면 된다.

 다음은 MyBatis를 테스트하기 위해 사용한 Maven의 pom.xml 파일이다. 사용된 jar 들을 아래 pom.xml을 통해 다운받을 수 있다.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.brm</groupId>
 <artifactId>mybatis-practice</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>mybatis-practice</name>
 <url>https://github.com/hinunbi/mybatis-practice</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>
 <repositories>
  <repository>
   <id>mesir-repo</id>
   <url>http://mesir.googlecode.com/svn/trunk/mavenrepo</url>
  </repository>
 </repositories>
 <dependencies>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>commons-dbcp</groupId>
   <artifactId>commons-dbcp</artifactId>
   <version>1.4</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>1.2.0</version>
  </dependency>
  <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.2.2</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>com.oracle</groupId>
   <artifactId>ojdbc14</artifactId>
   <version>10.2.0.4.0</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.6.1</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>1.6.1</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
 </dependencies>
</project>
 지금까지 MyBatis와 Spring 프레임워크를 이용하여 iBatis 만큼 편리하게 MyBatis를 사용하는 방법을 설명했다. 이 블로그에서 설명한 클래스들 잘 활용하면 불필요한 반복 코드들을 더욱 줄일 수 있고 데이터베이스의 트랜잭션 처리도 좀더 직관적이고 명시적으로 표현할 수 있게 될 것이다. 이 글의 소스는 GitHub에 올려 놓았다. 아래 참고 사이트의 "프로그램 소스" 링크에서 내려받을 수 있다.

맺음말

 MyBatis는 iBatis의 대를 이은 훌륭한 프레임워크이다. MyBatis는 iBatis에서는 지원하지 않았던 내포 관계를 가진 여러 테이블의 레코드들을 복합 구조의 자바 도메인 객체로 매핑하게 해주는 기능을 제공하는 등, 그동안 iBatis에서 아쉬웠던 기능들을 추가적으로 제공해 주고 있다. 그리고 MyBatis 프로젝트는 계속 진행 중임으로 개발이 중단(?)된 iBatis처럼 어느 순간에 최신의 데이터베이스와 맞지 않게 될 위험성도 없다. 그러므로 이 글에서 설명한 자원 해제와 트랜잭션의 해결 방법을 적용한다면, iBatis만큼 편리하면서도 기능적으로도 더욱 풍부한 생산적인 프레임워크로 MyBatis를 활용할 수 있을 것이다.

참고 사이트)


2013년 6월 3일 월요일

교착 상태(Deadlock)

 대규모의 동시 처리 시스템을 개발하는 경우, 전통적인 프로그램 모델은 멀티 프로세스나 스레드를 사용하는 것이다. 멀티 프로세스나 스레드는 공유 자원(메모리, 파일, 디바이스 등)을 사용하여 상태나 정보를 공유한다. 즉 정보를 읽거나 쓴다. 멀티 프로세스나 스레드들은 경우에 따라 공유 자원을 동시에 사용하므로 동시성 문제가 발생한다. 이를 방지하기 위해 프로세스나 스레드들은 상호 배타적인 동기 메커니즘을 사용한다. 즉 프로그램에 잠금(lock)을 사용하여 멀티 프로세스나 스레드들이 공유 자원을 동시적에 사용하는 것을 배타적으로 관리한다. 크리티컬 섹션(critical section), 뮤텍스(mutex), 세마포(semaphore) 같은 것들이 이런 잠금이다. 그런데 멀티 프로세스나 스레드들이 공유 자원을 배타적으로 이용하다 보면 문제가 발생한다. 일반적으로 잠금을 사용하면 공유 자원을 사용하는 데 있어서 올바른 행동을 보장받을 수 있지만, 그럼에도 불구하고 공유 자원을 사용하는 프로세스나 스레드들은 어쩌다가 교착 상태(deadlock)에 빠지게 된다. 다음 그림은 이런 교착 상태의 예이다.




 위 그림처럼 두 스레드가 두 자원을 잠금을 사용하면서 동시에 배타적으로 참조하는 시스템인 경우, 두 스레드는 이상 없이 잘 동작하다가 어느 순간에 아무 것도 실행하지 않고 공유 자원의 잠금이 풀리기만을 기다리는 교착 상태에 빠지게 된다. 그런데 이런 교착 상태가 실행 중 발생하는 것을 예상하는 것은 상당히 어렵다. 왜냐면 일반적인 교착은 위 그림처럼 단순한 것이 아니라, N개의 자원과 스레드들이 참여하여 발생하기 때문이다. N의 자원과 스레드들이 포함된 순환 교착을 예측하거나 해결하는 것은 고도의 프로그램 기술을 가진 개발자나 아키텍트에게도 결코 쉬운 일이 아니다. 일단 이런 문제는 디버깅하는 것도 상당히 어렵다. 일정 규모의 시스템을 개발하거나 운영해본 전문가들은 이런 문제들이 실제로 가끔 발생하며, 원인도 알 수 없고, 유일한 해결책(?)은 시스템의 재기동이란 것을 경험해 보았을 것이다. 이런 불가사의한 문제의 원인 중 하나가 바로 일정 개수 이상의 공유 자원과 스레드들이 포함된 순환 교착(circular deadlock)이다.

 좀더 구체적인 예를 들어보자. 프로그램에서 싱글톤 패턴(Singleton pattern)을 사용하는 경우 교착 상태에 빠질 수 있다. 싱글톤이 바로 공유 자원이며 이 싱글톤에 값을 쓰거나 읽기 위해 잠금을 사용하는 스레드들이 경쟁하기 때문이다. 싱글톤은 상당히 유용한 패턴이고 많은 곳에서 활용된다. 그러므로 개발자가 프로그램에서 사용하지 않더라도 개발자가 쓰는 프레임워크나 라이브러리 내부에서도 사용될 수 있다. 물론 프레임워크나 라이브러리는 정교하게 동기화되기 때문에 거의 교착에 빠지지 않을 것이지만, 그럼에도 불구하고 싱글톤이 개발자도 모르게 사용될 수 있고, 우연치 않게 여러 라이브러리들이 조합되면서 여러 싱글톤이 여러 스레드들과 경쟁하는 상황이 만들어지면서 교착은 발생할 수 있다.

 이번엔 좀더 현실적인 예를 보자. 일반적으로 웹에서 서블릿 프로그램을 개발할 때, 서블릿 클래스에 멤버 변수를 정의하지 말라는 말을 한다. 왜냐면 서블릿 객체는 톰캣이나 웹로직 서버 등과 같은 서블릿 컨테이너에서 싱글톤처럼 사용되기 때문이다. 다시 말해 서블릿 컨테이너는 한 번 생성한 서블릿 객체를 재활용 한다. 그런데 이런 사실은 모르는 개발자들이 의외로 많다. 그 결과 일반 클래스처럼 멤버 변수를 선언하고 웹 요청을 처리하는 과정에서 해당 멤버 변수에 읽고 쓰기를 반복하는 경우, 불규칙적으로 멤버 변수의 자료 손상을 경험하게 된다. 그리고 이 현상은 단위 테스트를 진행할 때에는 전혀 발견되지 않고 있다가, 실제 운영 환경에서 실행될 때 발견되는 경우가 많다. (실제로 모 금융 기관의 시스템에서도 이런 현상이 운영 중에 가끔 발생하여, 결국 운영 중에 해결되었다.) 그 이유는 단위 테스트 과정은 스레드들이 경쟁하지 않는 환경으로 멤버 변수를 훼손하는 원인이 제거되어 있고, 통합 테스트나 부하 테스트, 인수 테스트 과정도 멤버 변수를 훼손할 정도의 스레드 경쟁 상황이 쉽게 발생하지 않다가, 운영 환경에 와서야 비로소 여러 스레드들이 제대로 경쟁함으로 서블릿 클래스의 멤버 변수를 훼손하는 동시 사용의 순간이 등장하기 때문이다.

 또 다른 예로, 데이터베이스에서도 교착은 상당히 빈번히 발생한다. 데이터베이스는 여러 세션들이 공유 자원인 테이블을 공유하면서 빈번히 테이블에 레코드를 쓰거나, 읽거나, 갱신하거나, 삭제한다. 이 과정에서 데이터베이스는 자원의 무결성을 보장하기 위해 동기 메커니즘을 사용한다. 데이터베이스는 묵시적이든 명시적이든 트랜잭션을 사용하는데, 바로 이 트랜잭션이 바로 동기 메커니즘 즉 잠금이다. 그러므로 트랜잭션에 여러 테이블들이 포함되고 여러 세션들이 이 트랜잭션을 동시에 실행하는 경우, 프로그램들은 교착에 빠질 수 있다. 그러므로 대부분의 데이터베이스는 이런 교착 상태를 해결하는 메커니즘을 별도로 내장하는데, 오라클의 경우 교착 상태를 판단하는 데 기본적으로 60초를 기준 시간으로 한다. 오라클 데이터베이스 엔진은 60초 동안 응답이 없으면 교착 상태로 판단하고 관련 세션들 중 일부를 끊어 문제를 해결한다. 그러나 실시간 비즈니스에서 데이터베이스의 교착은 60초 동안 거래가 중단됨을 의미한다. 극단적인 예로 미사일 방어 시스템이 적의 미사일을 60초 동안 추적하지 못했을 경우를 생각해 보라! 또는 금융 거래 시스템에서 이체 마감 시간에 이체가 60초 동안 중단된 경우를 생각해 보라!

 일반적으로 대규모 동시 처리 시스템에서 모든 플랫폼들이 정상적으로 운영 중이고, 모든 미들웨어와 프로그램들이 정상적으로 운영 중임에도 불구하고 교착은 발생할 수 있다. 그리고 이 교착은 시스템 다운은 아니지만 보이지 않는 거래 중지를 만들어 마치 해당 시스템이 다운된 것과 같은 효과를 발휘한다. 일반적으로 기업은 시스템의 가용성 문제를 해결하기 위해 이중화나 HA(High Availability), 클러스터 등 고비용을 투자한다. 그에 비해 애플리케이션 소프트웨어의 성능이나 품질에 대해서는 제대로 대비하거나 관리하지 않고 있는 것이 현실이다. 대비라고 한다면 또다시 고가의 상용 플랫폼이나 미들웨어를 도입하여 문제를 해결하려고 한다!!! 그러나 애플리케이션이 발생시키는 교착의 문제는 애플리케이션이 동작하는 플랫폼이나 미들웨어가 해결해 주는 것이 아니다. 단지 그들은 보조적인 모니터링이나 기술지원을 해줄 수 있을 뿐이다. 즉 애플리케이션에서 해결해야 하는 문제이다. 그러므로 만약 이런 문제를 사전에 대비하는 경우, 교착으로 인한 서비스의 중단을 해결하는 것뿐만 아니라, 경우에 따라서는 최적화된 잠금 관리로 마법 같은 안정성과 성능 향상을 이룰 수 있게 된다.

 기업 입장에서 볼 때, 대규모 동시 처리 시스템 개발에 애플리케이션의 교착 방지 등 가용성을 위한 전문 인력을 추가적으로 활용하는 것이 추가적인 비용처럼 보일지 모르지만, 이들은 소프트웨어의 가용성과 그에 따른 성능을 상대적으로 높여줌으로,  가용성을 위해 필요한 하드웨어나 플랫폼, 미들웨어의 비용을  절감시켜 준다. 즉 개발 및 유지보수에 따른 전체 비용은 줄어 들 수 있다. 그러므로 대규모 동시 처리 시스템을 개발에는 애플리케이션들의 교착을 방지하고 소프트웨어의 가용성을 높이기 위해 멀티 프로세스나 멀티 스레드 애플리케이션 개발 경험 많은 개발자나 아키텍트가 설계와 개발에 참여도록 해야야 한다.