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

2022년 2월 7일 월요일

Apache Camel 애플리케이션 만들기

Apache Camel 애플리케이션 만들기

들어가며

일반적으로 애플리케이션에서 프레임워크를 이용하려면, 개발 소스 프로젝트에 프레임워크가 요구하는 의존, 설정, 코드 등을 입력해야 합니다. 그러므로 새로운 프레임워크를 이용하려는 개발자는 반드시, 이 내용을 프레임워크 관련 자료나 책 등을 학습해 애플리케이션 프로젝트에 적용해야 합니다. Java 애플리케이션 프레임워크로 가장 장 알려진 Spring 프레임워크나 Spring Boot 프레임워크도 오류 없이 기대하는 동작을 실행시키려면, Java 메인 클래스, 설정, 클래스, 메서드, 애노테이션, application.properties 나 application.yaml 등 애플리케이션 개발에 필요한 여러 가지 “관례(convention)” 와 “설정(configuration)”을 이해하고 올바르게 입력해야 합니다.

이런 과정은 Apache Camel 프레임워크를 사용할 때도 동일하게 적용됩니다. 그런데 Apache Camel 프레임워크를 처음 접한 개발자들이 올바른 Apache Camel 애플리케이션을 만들기는 그렇게 간단하지 않습니다. 그 이유는Apache Camel 프레임워크도 “관례(convention)”와 “설정(configuration)”을 정확하게 사용해야 하는데, 이런 “관례(convention)”와 “설정(configuration)” 은 해당 기술을 깊은 이해해야 체득되는 것이기 때문입니다.

Apache Camel 프레임워크를 접한 개발자들이 “관례(convention)”와 “설정(configuration)” 이 잘 구성된 애플리케이션 프로젝트부터 개발을 시작하는 것은 굉장히 중요한 출발점입니다. 그럼에도 책이나 문서들은 이 출발점에 대해 다양하게 설명하지만 선택할 수 없는 차림표처럼 제시할 뿐, 어떤 것이 최선의 “관례(convention)”와 “설정(configuration)” 인지는 잘 설명하고 있지 않습니다.

이런 점에서 이 글 Apache Camel 프레임워크 개발자들에게 유용한 Apache Camel 애플리케이션 프로젝트의 출발점을 제공합니다. 이를 위해 이 글은 Apache Camel 애플리케이션 개발을 시작하는 다양한 방법 중 주요 방법을 설명하고, 그 중 일관된 “관례(convention)”와 “설정(configuration)” 를 포함한 Apache Camel 애플리케이션 프로젝트를 생성하게 해주는 필자의 Spring Boot Apache Camel XML Archetype 을 설명합니다.

Apache Camel 이란?

  Apache Camel 은 잘 정립된 기업 통합 패턴(Enterprise Integraion Patterns) 개념을 이용해, 쉽고 표준적인 방법으로 애플리케이션을 통합할 수 있게 하는 Java로 작성된 프레임워크입니다. Apache Camel 의 컴포넌트는 데이터베이스, 메시지 브로커, HTTP 애플리케이션, 파일 시스템 등 다양한 기술의 엔드포인트를 통합(접속)시킬 수 있습니다. 이미 이런 컴포넌트가 300 개 이상 구현되어 있습니다. 예를 들어, Twitter, AWS, Azure, Kafka, Google, File, FTP, SOAP, Rest API, DBMS, No Sql 등등 대부분의 주요 플랫폼과 기술들의 컴포넌트를 포함합니다. 이런 점에서 Apache Camel 은 기업 통합(Enterprise Integration)을 위한 완벽한 스위스 칼(Swiss knife)입니다.

Apache Camel 프로젝트 만들기

Apache Camel 은 독립적으로 실행될 수도 있으나, 다양한 애플리케이션 프레임워크와 결합해 실행하는 내장형 통합 프레임워크 입니다. 그 중 가장 현대화된 방식으로 Spring Boot 나 Quarkus 애플리케이션에 Apache Camel 을 내장해 애플리케이션을 실행하는 것입니다. 그 외 더 다양한 방식으로 실행할 수 있으나, 실무적으로 이 두 방식이 가장 유용성이 높을 것으로 보입니다. 이 글은 Spring Boot 나 Quarkus 와 결합한 Apache Camel 애플리케이션 프로젝트를 만드는 법을 소개합니다. Apache Camel 애플리케이션 소스 생성하는 방법으로 웹 기반 생성과 Maven Archetype 을 이용한 방법을 소개합니다. 다른 방법에 관심있는 독자는 독자 자신의 탐구 과제로 남기겠습니다.

실행 환경

Apache Camel 애플리케이션 생성 환경은 다음과 같습니다. 사전에 설치돼 있어야 합니다.

  • Linux 또는 macOS 권장
  • JDK 8+
  • Maven 3.5+

Spring initaializr 를 이용한 프로젝트 만들기

Apache Camel 애플리케이션 프로젝트는 Spring initaializr(https://start.spring.io/) 사이트를 이용해 만들 수 있습니다. 이 사이트를 이용하면, 생성한 애플리케이션 프로젝트에서 아마치 카멜과 Spring Boot 프레임워크를 함께 이용할 수 있습다. Apache Camel 은 Spring 프레임워크와 잘 결합되고, Spring 프레임워크에서도 Apache Camel 을 이니셜라이져 사이트에서 내장할 수 있게 지원합니다. Spring 이니셜라이저 사이트에서 다음 정보를 입력해 Apache Camel 의존을 포함하면 Apache Camel 애플리케이션을 만들 수 있습니다.

이 글에서는 Spring Initializr 에서 생성하는 프로젝트 형식은 Java Maven 프로젝트로 지정했습니다. Gradle 프로젝트로도 생성할 수 있으나, Apache Camel 프로젝트는 Maven 프로젝트를 선호하기 때문입니다. 그 이유는 “Simple is Better” 원칙을 따르기 때문일 것입니다.

  • Project : Maven Project

  • Spring Boot: 2.6.3

  • Project Metadata

    • Group: org.example
    • Artifact: camel-sprig-hello
    • Name: camel-spring-hello
    • Description: Demo project for Spring Boot
    • Package name: org.example.camel-spring-hello
    • Packaging: Jar
    • Java: 11
  • Dependencies: Spring Web, Apache Camel

    camel-spring-hello

위 값들을 입력 후, “GENERATE” 버튼을 클릭하면 camel-hello.zip 파일로 Apache Camel 애플리케이션 프로젝트 소스를 다운로드 합니다.

생성된 camel-spring-hello 프로젝트 구조는 다음과 같습니다.

camel-spring-hello  
├── HELP.md  
├── mvnw  
├── mvnw.cmd  
├── pom.xml  
└── src  
 ├── main 
 │   ├── java 
 │   │   └── org 
 │   │       └── example 
 │   │           └── camelspringhello 
 │   │               └── CamelSpringHelloApplication.java 
 │   └── resources 
 │       └── application.properties 
 └── test 
     └── java 
         └── org 
             └── example 
                 └── camelspringhello 
                     └── CamelSpringHelloApplicationTests.java

Spring Initializr 가 생성한 Apache Camel 프로젝트의 Apache Camel 프레임워크 버전은 Spring Boot 버전에 따라 결정됩니다. 그러므로 원하는 Apache Camel 프레임워크 버전을 맞추려면 Spring Boot 버전을 조정할 필요가 있습니다.

생성된 Apache Camel 프로젝트는 Spring Boot Maven 프로젝트로 생성됐으므로, 다음 명령으로 실행할 수 있습니다.

mvn spring-boot:run

그러나 생성된 프로젝트는 필요한 의존 라이브러리만 포함되었을 뿐, 로직이 구현되어 있지 않았으므로 실행 결과를 기대할 수 없습니다.

Code with Quarkus 를 이용한 프로젝트 만들기

Apache Camel 애플리케이션 프로젝트는 Code with Quarkus(https://code.quarkus.io) 사이트를 이용해 만들 수 있습니다. 이 사이트를 이용하면, 생성한 애플리케이션 프로젝트는 Apache Camel 프레임워크와 Quarkus 프레임워크를 함께 사용할 수 있습니다. Quarkus 프레임워크가 커뮤니티에 소개된 기간을 길지 않으나, Red Hat 이 Red Hat 제품에서 Spring Framework 을 대체하고, Apache Camel 을 사용하게 하는 방향으로 발전하고 있습니다.

  • CONFIGURE YOUR APPLICATION
    • Group: org.example
    • Artifact: camel-quarkus-hello
    • Build Tool: Maven
    • Extentions: Camel Core

camel-quarkus-hello

생성된 camel-quarkus-hello 프로젝트 구조는 다음과 같습니다.

camel-quarkus-hello  
├── README.md  
├── mvnw  
├── mvnw.cmd  
├── pom.xml  
└── src  
    ├── main 
    │   ├── docker 
    │   │   ├── Dockerfile.jvm 
    │   │   ├── Dockerfile.legacy-jar 
    │   │   ├── Dockerfile.native 
    │   │   └── Dockerfile.native-distroless 
    │   ├── java 
    │   │   └── org 
    │   │       └── example 
    │   │           └── GreetingResource.java 
    │   └── resources 
    │       ├── META-INF 
    │       │   └── resources 
    │       │       └── index.html 
    │       └── application.properties 
    └── test 
        └── java 
            └── org 
                └── example 
                    ├── GreetingResourceTest.java 
                    └── NativeGreetingResourceIT.java

생성된 소스 구조는 Spring Boot 와 유사합니다. 추가로 Docker 이미지 생성 관련 설정도 포함됩니다. 그런데 생성된 애플리케이션 소스는 Apache Camel 관련 의존만 포함하므로, Apache Camel 소스를 어떻게 구현할 것인가 에 대해서는 개발자가 결정해야 합니다. Quarkus 프레임워크는 개발자들에게 생소하므로, 사용하기 위한 사전 학습 또는 Red Hat 컨설팅의 도움이 필요합니다. 좀더 자세한 소개는 아래 참고 사이트를 방문해 주십시오.

생성된 Apache Camel 프로젝트는 Maven 프로젝트로 생성됐으므로, 다음 명령으로 실행할 수 있습니다.

./mvnw compile quarkus:dev

그런데 필자가 Code with Quarkus 2.7 에서 다운로드한 camel-quarkus-hello 는 컴파일이 잘 되지 않았습니다. 어떤 이유가 있을 텐데… 이런 문제는 오픈 소스를 사용하는 경우 빈번히 발생하는 문제입니다. 이 글은 Apache Camel 프로젝트를 만드는 방법을 소개하는 글이므로, 관심있는 독자께서는 직접 컴파일을 시도해 보시고, 문제가 있을 경우 문제 원인을 찾아 해결할 수 있을 것입니다.

Apache Camel Archetype 을 이용한 프로젝트 만들기

Apache Camel 은 Maven Archetype 을 이용해 Apache Camel 애플리케이션 프로젝트를 생성하는 방식도 지원합니다. 이 방식의 장점은 웹 환경이 없어도 간단히 Maven 명령을 이용해 Camel 애플리케이션 프로젝트를 생성할 수 있다는 점입니다. 다음은 명령은 Apache Camel 3.14.1 버전과 Spring Boot 프레임워크가 결합된 Apache Camel 애플리케이션 프로젝트를 생성합니다.

mvn archetype:generate \
 -B \
 -DarchetypeGroupId=org.apache.camel.archetypes \
 -DarchetypeArtifactId=camel-archetype-spring-boot \
 -DarchetypeVersion=3.14.1 \
 -DgroupId=org.example \
 -DartifactId=camel-spring-archetype-hello

Spring Boot 외 다양한 프레임워크나 기술 기반의 프로젝트를 생성할 수 있습니다. 아래 참고 사이트를 참조해 주십시오.

생성된 camel-spring-archetype-hello 프로젝트 구조는 다음과 같습니다.

camel-spring-archetype-hello  
├── pom.xml  
└── src  
 ├── main 
 │   ├── java 
 │   │   └── org 
 │   │       └── example 
 │   │           ├── MySpringBean.java 
 │   │           ├── MySpringBootApplication.java 
 │   │           └── MySpringBootRouter.java 
 │   └── resources 
 │       ├── META-INF 
 │       │   ├── LICENSE.txt 
 │       │   └── NOTICE.txt 
 │       └── application.properties 
 └── test 
     ├── java 
     │   └── org 
     │       └── example 
     │           └── MySpringBootApplicationTest.java 
     └── resources

생성된 소스는 Apache Camel Java DSL 라우트 정의는 포함되어 있으나, XML DSL 은 포함되어 있지 않습니다. XML DSL 을 포함한 애플리케이션 생성은 Red Hat Fuse 에서 만 제공하기 있습니다. 최근들어 커뮤니티에서는 Java DSL 을 선호하는 측면이 있으나, XML DSL 이 갖는 장점도 분명하므로, Apache Camel 애플리케이션은 XML DSL 과 Java DSL 을 함께 사용하는 것을 권장 드립니다. 그리고 생성된 파일 이름에 MySpring 이 붙고, 라이선스 파일이 META-INF 디렉토리 아래 위치하는데… 이런 것들을 애플리케이션 생성 후, 수정이 필요한 부분 들입니다.

Red Hat Fuse Maven Archetype 을 이용한 프로젝트 만들기

Apache Camel 프로젝트는 Red Hat 이 주도하는 오픈 소스 프로젝트입니다. 그러므로 Red Hat 은 Apache Camel 을 포함한 상용 오픈 소스 통합 제품으로 Red Hat Fuse 를 판매합니다. Red Hat Fuse 는 커뮤니티 오픈 소스인 Apache Camel 에 확장된 개발 도구, 개발 문서, 라이브러리, 레지스트리, 컨설팅 서비스, 기술 지원 서비스 등을 추가적으로 제공합니다. 그 중 Red Hat Fuse Maven Archetype 를 이용해 Apache Camel 애플리케이션 프로젝트를 만드는 방법을 제공합니다. 이 방법을 이용해 만들어진 프로젝트 소스는 커뮤니티 Apache Camel 기반이 아닌 Red Hat Camel 프로젝트가 만들어지나, 온전히 동작되는 소스 구조를 갖고 있으므로, Red Hat Fuse 를 구매한 고객의 개발자는 이 기능을 유용하게 이용할 수 있습니다. 다음은 Red Hat Fuse spring-boot-camel-xml-archetype 을 이용해 Red Hat Camel 애플리케이션을 생성한 예입니다.

wget https://gitlab.com/hinunbi/spring-boot-camel-xml-archetype/-/raw/main/configuration/settings.xml   

mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate \
 -B \
 -s settings.xml \
 -DarchetypeCatalog=https://maven.repository.redhat.com/ga/io/fabric8/archetypes/archetypes-catalog/2.2.0.fuse-sb2-780040-redhat-00002/archetypes-catalog-2.2.0.fuse-sb2-780040-redhat-00002-archetype-catalog.xml \
 -DarchetypeGroupId=org.jboss.fuse.fis.archetypes \
 -DarchetypeArtifactId=spring-boot-camel-xml-archetype \
 -DarchetypeVersion=2.2.0.fuse-sb2-780040-redhat-00002 \
 -DgroupId=org.example \ -DartifactId=fuse-hello

위 명령 실행에 settings.xml 파일 필요한 이유는, Red Hat Fuse 애플리케이션을 생성하기 위한 Archetype 이 Apache Maven Central 저장소에는 등록되어 있지 않고, 상용 제품이다 보니, Red Hat Maven 저장소에만 등록되어 있기 때문입니다.

생성된 fuse-hello 프로젝트 구조는 다음과 같습니다.

fuse-hello/  
├── LICENSE.md  
├── configuration  
│   └── settings.xml  
├── pom.xml  
└── src  
 ├── data 
 ├── main 
 │   ├── fabric8 
 │   │   └── deployment.yml 
 │   ├── java 
 │   │   └── org 
 │   │       └── example 
 │   │           ├── Application.java 
 │   │           └── MyTransformer.java 
 │   └── resources 
 │       ├── application.properties 
 │       ├── logback.xml 
 │       └── spring 
 │           └── camel-context.xml 
 └── test 
     └── java 
         └── org 
             └── example

Red Hat Fuse Camel 애플리케이션은 개발자가 애플리케이션을 개발할 때 필요한 Spring Boot Undertow WAS 와 Camel Java DSL, XML DSL 과 최근 보안 취약점이 발견된 log4j 가 아닌 logback.xml 을 포함하고, Red Hat OpenShift 플랫폼에 컨테이너 이미지로 배포하기 위한 파일과 설정 들도 포함되어 있습니다. 단위 테스트 소스가 포함되어 있지 않은 점을 제외하면 개발자들이 개발을 시작할 때 가장 좋은 Camel 애플리케이션 소스 구조를 갖고 있습니다. 그러나 Red Hat Fuse 를 구매하지 않은 경우, 생성된 소스와 라이브러리 사용 시, 저작권 문제가 걸릴 수 있습니다.

Apache Camel 애플리케이션 만들기

Apache Camel 애플리케이션은 위와 같은 방식으로 최초 프로젝트 소스를 생성한 후, 프로젝트에 300 개 이상의 통합 컴포넌트 의존을 추가함으로, 현대 애플리케이션의 통합에 필요한 대부분의 기능을 지원할 수 있습니다. 그러나 위 생성 방법들을 이용해 생성된 Apache Camel 애플리케이션은 오픈 소스 결과물의 불완전성, 생성된 소스의 불합리한 구조, 상용 오픈 소스가 갖는 저작권 등을 때문에, 개발자는 생성된 소스를 다시 정비해 사용해야 합니다. 그리고 이렇게 정비한 소스로 Apache Camel 애플리케이션 개발 출발점을 삼아야 합니다. 그런데 이 과정은 반복되나 사람이 하는 일이라 일관되지 않는 경우가 빈번합니다. 그렇다고 계속 정비해 이용 하자니, 불편함이 따릅니다.

이런 문제점을 해결하기 위해, 필자는 위 방법을 종합한 Apache Camel 애플리케이션 Archetype 을 새로 개발했습니다. 필자가 개발한 Apache Camel Archetype 은 커뮤니티 오픈 소스에 기반한 Apache Camel 애플리케이션을 생성함에도, 소스 구조는 Red Hat Fuse 구조로 생성되고 Camel Java DSL과 XML DSL 을 모두 사용하고, Red Hat OpenShift(Kubernetes) 플랫폼에 배포될 수 있는 설정과 Maven 플러그인 등을 자동으로 포함합니다. 사용 방법은 위에서 설명한 Maven Archetype 을 이용한 Apache Camel 애플리케이션 생성과 유사합니다. 필자의 Archetype 을 사용해 Apache Camel 애플리케이션을 생성하는 명령은 다음과 같습니다. 좀더 자세한 설명은 필자의 spring-boot-camel-xml-archetype Git 저장소 를 참고해 주십시오.

git clone https://gitlab.com/hinunbi/spring-boot-camel-xml-archetype.git  
cd spring-boot-camel-xml-archetype  
git checkout tags/0.0.1
mvn install archetype:update-local-catalog  
  
mvn org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate \  
 -B \
 -DarchetypeGroupId=hinunbi.camel.archetypes \
 -DarchetypeArtifactId=spring-boot-camel-xml-archetype \
 -DarchetypeVersion=0.0.1 \
 -Dspring-boot-version=2.5.4 \
 -Dcamel-version=3.14.1 \
 -Dfabric8-version=5.12.0 \
 -Djkube-version=1.5.1 \
 -DgroupId=org.example \
 -DartifactId=camel-hinunbi-hello  

필자가 테스트한 Apache Camel 애플리케이션 생성에 사용한 Apache Camel 버전과 Spring Boot 버전 등, 이 글을 쓰는 시점에서 유효합니다.
오픈 소스들은 역동적으로 발전함으로, 이 글을 읽는 독자가 실행하는 시점에 프레임워크들의 버전이 업그레이드 된 경우, 업그레이드 버전으로 테스트 할 수 있을 것입니다. 다만 Apache Camel 이 의존하는 Spring Boot 버전에 따라 위 명령에 -Dspring-boot-version=* 값을 정확히 입력해야 점은 유의해야 합니다.

생성된 camel-hinunbi-hello 프로젝트 구조는 다음과 같습니다.

camel-hinunbi-hello/  
├── LICENSE.txt  
├── NOTICE.txt  
├── ReadMe.adoc  
├── pom.xml  
└── src  
    ├── main 
    │   ├── java 
    │   │   └── org 
    │   │       └── example 
    │   │           ├── Application.java 
    │   │           ├── MySpringBean.java 
    │   │           └── MySpringBootRouter.java 
    │   └── resources 
    │       ├── application.properties 
    │       ├── logback.xml 
    │       └── spring 
    │           └── camel-context.xml 
    └── test 
        ├── java 
        │   └── org 
        │       └── example 
        │           └── ApplicationTest.java 
        └── resources

필자가 개발한 Apache Camel Maven Archetype 을 이용해 생성한 Apache Camel 프로젝트 소스는 Red Hat Fuse Camel 애플리케이션 소스와 같은 구조이면서 순수하게 Apache Camel, Spring Boot 오픈소스로만 구성된 소스가 생성된 것을 볼 수 있습니다.

생성된 Apache Camel 프로젝트는 Spring Boot maven 프로젝트로 생성됐으므로, 다음 명령으로 실행할 수 있습니다.

mvn spring-boot:run

맺음말

일련의 애플리케이션들은 협업을 하기 위해, 좋은 아키텍처가(모노리스/마이크로서비스) 필요하듯이,
좋은 애플리케이션은 소스 구조가 좋아야 합니다.
소스 구조가 좋으려면, 소스는 사용하는 프레임워크들의 관례를 일관되게 지켜야 합니다.
그래야 개발자들은 소스 구조에 대한 공통의 이해를 갖고 개발자들 사이 의사 소통(communication)이 원할해 질 수 있습니다.
Apache Camel 애플리케이션도 이런 소스 구조 관례를 잘 지켜야 개발 생산성이 올라갈 수 있습니다.

이 글에서는 여러 프레임워크에서 Apache Camel 을 포함한 소스를 생성하는 방법과 생성된 소스 구조를 설명했습니다.
그리고 필자가 생각하는 최선의 관례를 포함한 필자의 Archetype 을 소개했습니다.
Apache Camel 에 관심을 갖고 통합 업무에 Apache Camel 프레임워크를 적용해 보려는 개발자들이 이 글을 읽고, 따라 실행해 Apache Camel 애플리케이션을 만들어 본다면,
Apache Camel 애플리케이션 개발을 시작할 때 그 출발점을 어떻게 해야할 지 결정하는데, 도움이 될 것입니다.

참고 자료

  1. Enterprise Integration Patterns
  2. 기업 통합 패턴
  3. Spring Boot Camel XML Archetype
  4. Spring Boot Initializr
  5. Code with Quarkus
  6. Apache Camel Archetype
  7. Apache Maven Archetype
  8. Quarkus + Apache Camel 소개

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년 11월 11일 화요일

Apache Camel을 이용한 마이크로 웹 서비스 개발


1. 들어가며

일반적으로 기업은 사업 상 멀리 떨어진 외부 협력사들이나 자사의 지점들과 정보나 업무를 주고 받아야 한다. 예를 들어 쇼핑몰은 배송지 주소와 물품 배송 정보 등을 인터넷을 통해 배송 업체와 주고 받아야 한다. 즉 쇼핑몰 전자 상거래 시스템은 배송 업체의 물류 시스템과 인터넷에서 원격으로 접속해서 정보를 주고 받아야 한다. 그런데 문제가 되는 점은 이들 개별 회사들이 인터넷의 표준 프로토콜인 TCP/IP 접속을 합의했다 손치더라도 애플리케이션 프로토콜이나 데이터 형식에 대해서는 그다지 합의하지 못했다는 점이다. 즉 각 사의 애플리케이션 프로토콜이나 데이터 형식은 각 사의 환경에 맞춰 각기 달리 발전함으로써 양 사가 통합하려면 한쪽이든 양쪽이든 프로토콜과 데이터 형식을 새로 합의해야 한다.

과거에는 (여전히 현재도) 인터넷 또는 TCP/IP 망에서 떨어진 두 시스템은 데이터 전송에 이미 합의한 TCP/IP 계층의 전송 프로토콜을 적극 이용하는 방향으로 발전해 왔다. 즉 전송을 위해 TCP/IP 소켓을 활용하고, 소켓 접속 수립에 양 애플리케이션이 합의한 절차를 추가하고, 데이터 형식은 전문이라 불리는 (일반적으로 고정 길이 필드를 가진) 메시지를 이용했다. 그런데 이런 방식으로 원격 시스템들을 통합하는 경우 개발자가 부담해야 할 개발 범위와 시간은 상당하게 된다. 통합을 위해 각 사에서 사용하는 다른 프로토콜과 데이터 형식을 서로 맞추거나, 한 쪽에 맞춰야 하고, 통합을 위한 테스트도 양 사가 동시에 수행해야 하기 때문이다. 기업들은 이런 문제를 해결하기 위해 통신과 전문을 편리하게 처리해 주는 통신/전문 라이브러리나 미들웨어 패키지들을 구매하기도 한다. 그러나 벤더 독자적 솔루션을 사용하는 기업은 상이한 솔루션을 사용하는 다른 원격지와 통합하려는 경우 다시 유사한 문제를 해결해야 한다.

이와 같은 문제로 인터넷의 원격지 시스템들 사이 통합에 모두가 동의할 수 있는 수많은 표준들이 제안되었고, 그 중 SOAP(Simple Object Access Protocol)을 많이 사용되고 있다. 그러나 SOAP이 처음 세상에 나왔을 때, SOAP은 이름처럼 간단함을 추구하려 했으나 실제 사용하려면 지나치게 자유도는 높고 사용 절차도 상당히 복잡하다는 문제를 가지고 있었다. 그 결과 관심을 가졌던 전문가들조차 SOAP 사용에 어려움을 호소하거나 심지어 SOAP을 외면하는 사태까지 이르렀었다. 그러나 시간이 지나면서 SOAP을 편리하게 사용할 수 있는 도구들과 프레임워크들이 시행착오를 통해 발전하면서 SOAP의 사용이 과거보다 상당히 편리해 졌고, 인터넷 상의 원격지 시스템들을 통합하는 데 이만큼 잘 정비된 표준도 없기에, 현재는 점차 SOAP을 원격지 통합에 사용해 가는 추세다.

SOAP 이외에 인터넷 상의 원격지 시스템들을 통합하는 표준이나 기술로 XML-RPC, JSON-RPC, Restful, Spring Remote, Camel Remote 등등 HTTP 프로토콜을 기반으로 애플리케이션 계층을 정의하는 여러 프로토콜들이 있다. HTTP 프로토콜은 요청 응답 패턴에 헤더와 본문을 가진 메시지를 사용하고 사용하는 TCP 포트도 기업 인터넷 방화벽이 접속을 허용하므로, 대부분의 애플리케이션 프로토콜들이 HTTP 프로토콜을 전송 프로토콜로 사용한다. 우리가 일반적으로 알고 있는 RPC(Remote Procedure Call) 기술인 CORBA, DCOM, 자바 RMI 등은 사실 인터넷과 같은 WAN 망을 위한 프로토콜이 아니다. 이런 프로토콜은 간단한 요청 응답 패턴 이상의 송수신이 발생하고, 경우에 따라서는 브로커가 개입해야 하고, 사용하는 TCP 포트도 일반적으로 인터넷 방화벽이 개방하지 않는다. 즉 이런 기술들은 LAN에서 사용되는 기술들이다. 그러므로 이런 기술을 WAN 망에 적용하면 도리어 성능이 떨어지거나 플랫폼에 종속되거나 상호 상호 접속이 불가능할 수도 있게 된다. 즉 RPC에 사용되는 프로토콜들도 접속 상황에 따라 적절하게 구분해서 사용해야 제대로 된 효용을 발휘할 수 있는 것이다.

최근에는 마이크로 서비스(Microservice)라는 개념이 확산되고 있다. 마이크로 서비스는 아주 작은 코딩으로 업무를 처리하는 기능 중심의 컴포넌트 애플리케이션을 말한다. 즉 약 100 줄 내외의 짧은 프로그램이나 간단한 설정으로 업무를 수행하게 하는 기술이다. 모든 기능을 집적한 강력한 단일형 애플리케이션에서 기능 별로 전문화된 컴포넌트 애플리케이션으로 분업화하는 기술이라고도 볼 수 있다. 이 기술의 장점은 서비스의 추가/삭제/수정에 다른 서비스들의 영향을 최소화 할 수 있다는 점이다. 모든 서비스를 함께 운영하는 단일형 애플리케이션의 경우 운영되는 서비스들 중 일부를 수정하려면 애플리케이션 내 모든 서비스들도 영향을 받거나 중지해야 하는 문제가 생길 수 있다. 이점은 기업 통합 패턴의 단단한 결합(tight coupling)과 느슨한 결합(Loose Coupling)의 원리 비교와 일맥 상통한다. 마이크로 서비스는 이제 막 등장하는 개념으로 앞으로 어떻게 발전/수정/쇠퇴할 지 좀더 지켜봐야 할 것이다. 그럼에도 필자는 이 글에서 SOAP 웹 서비스를 마이크로 서비스로 구현해 보려고 한다.

SOAP 프로토콜을 이용하려면 웹 서비스를 기술하는 WSDL도 알아야 하고 SOAP 구현체에 맞추어 프로그램도 개발해야 한다. 그러므로 SOAP을 이용한 개발 방법은 SOAP 구현체에 따라 상당히 달라진다. 그리고 SOAP은 웹 서비스에 주로 사용된다. 여기서 말하는 웹 서비스란 인터넷에 HTTP 프로토콜을 통해 노출된 서비스를 말한다. 즉 SOAP 웹 서비스는 인터넷에서 호출 가능한 서비스로 데이터 전송 프로토콜로 HTTP를 사용하고 데이터의 전송 형식과 원격 기능 호출 절차에 SOAP 프로토콜을 사용하는 서비스를 말한다. 그러므로 SOAP 웹 서비스를 구축하기 위해서는 웹 서버가 필요하고 SOAP 프로토콜을 지원하는 구현체(프레임워크)도 필요하다.

SOAP 웹 서비스는 두 가지 접근 방법으로 개발한다. 계약 우선 개발(contract-first development) 방법과 서비스 우선 개발(service-first development) (또는 코드 우선 개발(code-first development)또는 계약 나중 개발(contract-last development)) 방법이다. 계약 우선 개발(contract-first development) 방법이란 WSDL(Web Service Description Language)를 우선 사용해 인터넷 상의 웹 서비스를 정의한 후 웹 서비스 서버나 클라이언트들이 정의된 WSDL에 따라 웹 서비스를 개발 방법이다. 서비스 우선 개발(service-first development) 방법이란 인터넷 상에 웹 서비스를 노출하는 서버가 자바 같은 개발 언어로 서비스를 우선 개발하고 개발된 서비스를 웹 서비스로 노출하는 개발 방법이다. 이때 웹 서비스 서버는 노출된 웹 서비스의 WSDL도 자동으로 노출한다. 이후 웹 서비스 클라이언트들은 웹 서비스 서버가 노출한 WSDL을 사용해 계약 우선 개발 방법으로 웹 서비스 호출을 개발한다. 계약 우선 개발 방법의 장점은 WSDL을 이용해 서비스 구현 없이 서비스의 서명과 입출력을 정의할 수 있다는 점이다. 즉 웹 서비스 서버와 클라이언트가 웹 서비스 구현 없이도 웹 서비스를 문서만으로 우선 정의할 수 있다는 점이다. 반면 서비스 우선 개발은 웹 서비스 서버가 웹 서비스를 우선 구현한 후 웹 서비스 클라이언트에게 WSDL을 알려줌으로써 웹 서비스 클라이언트가 웹 서비스 호출을 구현할 수 있게 한다. 이 방법의 장점은 이미 만들어진 내부 서비스를 웹 서비스로 노출할 때 편리하고, 자동화된 웹 서비스 노출 관례를 따르므로 서비스 개발에 집중할 수 있다는 점이다. 반면 이 방법의 단점은 웹 서비스 노출 시, WSDL이 기계적으로 생성됨으로, WSDL을 직접 기술하는 것보다 웹 서비스 정의 융통성이 떨어질 수 있다는 점이다. 그러나 이런 문제도 웹 서비스 어노테이션이나 설정 등을 통해서 웹 서비스 노출 서명을 수정할 수 있으므로 사실 과거에 서비스 우선 개발이 가졌던 단점은 상당히 완화된다. 필자는 SOAP 웹 서비스 서버 개발에는 서비스 우선 개발 방법을 사용할 것이고, SOAP 웹 서비스 클라이언트 개발에는 계약 우선 개발 방법을 사용할 것이다.

첨언하자면 이 글은 Apache Camel Developer's Cookbook 12장 Web Services가 설명하지 않는 SOAP 웹 서비스의 서비스 우선 개발 방법에 대해 필자가 도전하면서 얻은 노하우를 설명하는 글이다.


2. 웹 서비스 서버 프로젝트 생성

웹 서비스를 구현하기 위해서는 SOAP 기반 웹 서비스 구현을 돕는 아파치 CXF(Apache CXF) 프레임워크와 마이크로 서비스를 가능하게 하는 아파치 카멜(Apache Camel) 통합 프레임워크와 애플리케이션 프레임워크인 스프링(Spring) 프레임워크가 필요하다. 그리고 이들 프레임워크들을 편리하게 추가하고 서비스도 간단히 실행하기 위해 maven(3.1 이상) 프로젝트로 웹 서비스 서버를 개발할 것이다. Maven을 이용해 쉘(명령창)에서 웹 서비스 서버 프로젝트를 다음 같이 생성한다.

생성된 프로젝트 디렉터리 아래 pom.xml 파일에 다음의 의존들을 추가한다.

포함되는 의존들은 CXF를 사용하기 위한 Camel CXF 컴포넌트와 자동으로 의존에 포함되는 CXF 프레임워크와 CXF 웹 서비스를 웹 상에 구동해 주는 CXF jetty 라이브러리들이다.


3. 서비스 입출력 정의

일반적으로 서비스를 구현하려면 클라이언트 요청과 서비스 응답을 정의해야 한다. (이곳의 입출력 클래스는 앞서 언급한 Apache Camel Developer's Cookbook의 웹 서비스 예를 참조했다. 그런데 Cookbook의 예는 계약 우선 개발 방법을 사용하므로 WSDL로부터 Cookbook의 웹 서비스 입출력 클래스 생성되나, 필자는 서비스 우선 개발 방법을 사용하므로 필자의 웹 서비스는 입출력 클래스를 우선 정의한다.)

그러므로 서비스 입력 출력 클래스를 다음과 같이 정의한다.

입출력 클래스는 com.brm.ws.payment.types 패키지 아래 놓았다. 이 패키지 이름은 나중에 SOAP WSDL 입출력 정의에서 네임스페이스로 사용된다. CXF 프레임워크는 자바의 리플렉션 기능을 사용해 위에 정의한 입출력 클래스를 WSDL 입출력 정의로 생성해낸다. 그리고 생성된 WSDL 입출력 정의는 SOAP 마샬링의 메타데이터로 활용된다. 그러므로 SOAP 원격 통신으로 전송되는 입출력 클래스는 마샬링 정보가 WSDL로 정의되므로 별도의 마샬링 메타데이터는 필요하지 않다. 그러나 예를 들어 자바 RMI나 JMS같은 SOAP 이외의 프로토콜을 이용하는 경우, 입출력 객체를 전달하고자 한다면 해당 기술에 맞도록 마샬링 메타데이터를 정의하거나, 입출력 클래스가 Serializable 인터페이스를 상속하도록 해야 한다. Serializable 인터페이스를 상속하는 방법은 기업 통합 패턴의 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현" 예를 팁코 액티브엔터프라이즈 대신 아파치 카멜로 대체하는 "아파치 카멜을 이용한 비동기 구현"이란 글에서 JMS 통신을 위해 사용될 예정이니, 그 글을 참고하기 바란다.

SOAP은, 입출력 마샬링 메커니즘뿐만 아니라, 웹 서비스의 오류 정보를 전달하는 fault 메커니즘도 제공한다. 서비스 우선 개발 방법에서는 서비스 메소드가 하나의 반환 형식이 고정되므로, 이 기능은 예외를 이용해 전달한다. (이 점은 서비스 우선 개발 방법이 계약 우선 개발 방법과 비교하여 융통성이 떨어지는 부분이기도 하다. 그러나 웹 서비스 서버와 클라이언트는 fault 메커니즘 대신 서비스 응답에 '반환 코드'나 '오류 상세 메시지'를 포함하는 방식으로 입출력을 합의할 수 있다. 이 경우 SOAP 서비스 클라이언트는, fault 처리를 위한 별도 예외 처리 로직 대신, 서비스의 응답으로부터 '반환 코드'나 '오류 상세 메시지'를 추출해 예외를 처리한다. 게다가 이 방법은 충분히 편리하다. 그러므로 이 점이 꼭 서비스 우선 개발 방법의 약점이라고만은 할 수 없다.)

이 글에서는 기술적인 측면에서 SOAP fault를 웹 서비스 어떻게 클라이언트에게 전달할 수 있는지를 보여줄 것이므로, 다음과 같이 Fault 클래스를 정의한다.

위 Fault 클래스는 reason 멤버 변수를 하나 정의해 간단하게 오류를 설명한다. 서비스 오류를 설명하는 추가적인 정보가 필요한 경우 이곳에 필드를 추가할 수 있을 것이다.


4. 서비스 인터페이스 정의

서비스 입출력을 정의한 후 서비스 기능을 노출하는 서비스 인터페이스를 정의한다. 이 서비스는 위에서 정의한 서비스 입출력 정의의 입력 클래스와 출력 클래스를 사용한다. 여기서 주목할 점은 아직까지 웹이나 SOAP에 대한 고려가 없다는 점이다. 즉 서비스 인터페이스는 순전히 일반적인 자바 인터페이스와 메소드로 정의한다. 정의된 서비스 인터페이스는 다음과 같다.

위 서비스 인터페이스에서 TransferException 예외에 주목한다. 서비스 우선 개발에서는 예외를 이용해 웹 서비스 서버가 fault를 웹 서비스 클라이언트에게 전달한다. Fault 정보를 포함한 TransferException 클래스 정의는 다음과 같다.

CXF 프레임워크의 동작 메커니즘으로 보면 TransferException가 SOAP fault로 전달되지만 TransferException은 예외를 위한 클래스이므로 의미의 중첩을 피하기 위해 실제 SOAP fault의 상세 정보는 여기처럼 Fault 객체를 사용하는 것이 바람직하다. 이것으로 서비스 우선 개발 방법의 서비스 인터페이스 정의는 완료된다.


5. 서비스 빈 구현

서비스 인터페이스가 정의되면, 일반적인 POJO 스타일의 서비스를 구현한다. 여전히 웹 서비스라든지 SOAP이라든지에 대한 구현 상의 고려는 별달리 추가되지 않는다. 즉 이 일반적인 POJO 스타일의 서비스 빈은 손쉽게 웹 서비스로 전환될 것이다. 이 예는 웹 서비스의 뼈대 구현에 초점을 맞추므로, 서비스 빈은 이체를 성공했다고 가정하고 간단하게 성공을 응답한다. 그러므로 구현된 DefaultPayment 서비스 빈 은 다음과 같다.

위 서비스 빈은 백만 원을 넘는 이체 요청은 예외를 발생시킨다. 정상 거래와 비정상 거래를 모두 테스트 하기 위해 이체 한도를 서비스 기능에 포함시켰기 때문이다. 보다시피 위 서비스 빈은 전형적인 POJO 스타일의 자바 클래스다.


6. 웹 서비스 서버 구현

서비스 우선 개발 방법에 따라, 위와 같이 일반적인 POJO 스타일의 서비스 빈을 완성했으면 이제 이 평범한 서비스 빈을 웹 서비스로 노출시킬 차례다. 이를 위해서는 아파치 카멜이 제공하는 camel 네임스페이스뿐만 아니라 cxf 네임스페이스도 스프링 빈 정의 XML에 포함해야 한다. 이들 네임스페이스를 포함한 스프링 XML의 beans 엘리먼트는 다음과 같은 속성들을 갖는다.

위와 같이 카멜과 웹 서비스를 위한 네임스페이스를 추가했다면, 서비스 인터페이스를 웹 서비스로 노출시키는 웹 서비스 빈을 지정한다. 이 빈에 노출될 웹 서비스의 웹 URL과 서비스 인터페이스를 지정한다. 실제 서비스를 구현한 빈도 지정한다.

웹 서비스 빈과 구현 빈을 지정했다면, 이제 카멜 라우팅 정의에 이 두 빈을 포함시켜 웹 서비스 서버로 동작하게 한다. 웹 서비스 서버를 노출하는 카멜 라우팅 정의는 다음과 같다.

이 카멜 라우팅 정의는 카멜 CXF 컴포넌트를 이용해 PaymentServiceEndpoint 를 웹 서비스 소비자(서버)로 노출하고, 웹 서비스 클라이언트로부터 수신한 요청을 다시 paymentService 빈에게 전달하게 한다. 즉 from 엔드포인트는 웹 서비스 서버를 노출하고 to 엔드포인트는 웹 서비스 구현을 호출한다. 그리고 route에 handleFault 속성을 참으로 지정해 서비스 빈에서 발생한 예외를 SOAP fault 메시지로 전달한다. 카멜 라우팅 정의를 이용한 웹 서비스 노출은 이렇게 간단하다. 아래는 모든 설정을 포함한 완전한 스프링 빈 정의 XML이다.

이것으로써 POJO 스타일의 서비스가 SOAP 웹 서비스로의 확장이 완성됐다. 실질적으로 웹 서비스로 노출하기 위해 추가된 코드와 설정은 다 합해도 100줄도 채 되지 않았다. 카멜의 장점 중 하나는 이렇게 카멜 정의 파일만으로도 즉시 카멜 컨테이너를 실행할 수 있다는 점이다.


7. 웹 서비스 서버 실행

이제 웹 서비스 서버를 실행해 보자. 프로젝트 디렉터리에서 maven camel:run 골(goal)을 실행한다. camel:run 골을 실행할 수 있은 이유는 프로젝트 생성 과정에서 pom.xml에 camel-maven-plugin이 포함됐기 때문이다.

다음은 웹 서비스 서버 실행 결과다.

그런데 눈치 빠른 독자라면 무엇인가 이상한 점을 발견했을 것이다. 이 예의 SOAP 웹 서비스 서버는 Tomcat이나 그 밖의 WAS를 이용해 기동되지 않았을뿐더러, 웹 서비스 서버를 기동하기 위해 Jetty와 같은 내장형 서블릿 컨테이너를 스프링 빈으로 복잡하게 설정한 후 다시 여러 빈을 엮는 작업을 하지도 않았다. 웹과 관련된 유일한 설정은 “웹 서비스 구현”에서 cxf:cxfEndpoint를 정의할 때 주소로 지정한 http://localhost:9090/paymentService가 전부다. 위 로그를 보면 CXF 프레임워크가 ReflectionServiceFactoryBean 등을 이용해 cxf:cxfEndpoint에서 지정한 http://localhost:9090/paymentService 주소로 웹 서버를 자동으로 기동한 것을 볼 수 있다. 그리고 이 모든 과정이 관례(CoC, Convention Over Configuration)에 따라 자동으로 완성된다. 즉 개발자는 일반적인 POJO 서비스 빈을 30줄도 안 되는 아주 적은 설정을 사용해 SOAP 웹 서비스를 완성한 것이다. 이런 점에서 이 웹 서비스는 마이크로 웹 서비스다.


8. 웹 서비스 WSDL

CXF 프레임워크는, SOAP 웹 서비스 서버의 실행과 더불어, 웹 서비스의 SOAP 계약인 WSDL을 자동으로 노출한다. 그러므로 웹 서비스 서버를 실행한 후, 웹 브라우저는 웹 서비스가 노출한 웹 URL 주소 뒤에 “?wsdl”을 추가해 조회함으로 웹 서비스의 WSDL을 읽을 수 있다.


9. 웹 서비스 클라이언트 프로젝트 생성

웹 서비스 클라이언트 프로젝트는 웹 서비스 서버 프로젝트 생성에서 사용한 동일한 maven archetype을 사용한다. 서버와 달라지는 점은 'artifactId'가 wsc-example로 바뀌는 정도다.

생성된 프로젝트 디렉터리 아래 pom.xml에 다음과 같이 의존들을 추가한다.

jaxws-api 의존은 CXF 프레임워크의 wsdl2java가 생성한 클래스가 참조하는 의존이고, junit 의존은 웹 서비스 클라이언트를 JUnit 클래스로 만들기 위해 필요한 의존이다.


10. 웹 서비스 클라이언트 입출력 정의

앞서 웹 서비스 서버를 완성했으므로, 웹 서비스 클라이언트의 입출력 개발은 웹 서비스 서버가 노출한 WSDL을 이용해 보자. WSDL이 이미 완성된 경우, 계약 우선 개발 방법을 이용하는 것이 훨씬 수월하기 때문이다. 이 예에서는 WSDL로부터 자바 클래스들을 생성하도록 돕는 CXF 프레임워크의 wsdl2java 유틸리티를 이용해 웹 서비스 클라이언트가 필요한 자바 클래스들을 생성한다. wsdl2java를 다음과 같이 실행하면, 이 예의 웹 서비스 서버 WSDL로부터 웹 서비스 클라이언트 실행에 필요한 자바 패키지와 클래스들이 생성된다.

위 명령을 통해 생성된 패키지와 클래스들을 웹 서비스 클라이언트 프로젝트 디렉터리의 src/java/main 아래 복사한다. 복사하면 다음과 같은 패키지와 클래스들이 보일 것이다.

위와 같이 복사하면 웹 서비스 클라이언트의 입출력 정의는 완성된다. com.brm.ws.payment 패키지는 웹 서비스 서버 개발에서 Payment 인터페이스를 정의한 패키지 위치와 같다. (패키지 이름은 웹 서비스 서버 개발 시 웹 서비스 어노테이션을 사용해 명시적으로 지정할 수도 있다. 사실 요즘은 웹 서비스 어노테이션을 사용해 서비스 우선 개발이더라도 계약 우선 개발의 WSDL을 개발하는 것처럼 섬세하게 웹 서비스 구조를 정의할 수 있다. 즉 어노테이션을 이용해 네임스페이스에 대응되는 패키지 이름이나 서버의 웹 서비스가 정의된 패키지에 대응되는 웹 서비스 인터페이스 이름 등을 명시적으로 지정할 수도 있다. 문제는 어느 방법이 더 수월한 가다.) 패키지 아래 입출력 클래스인 TransferRequest.java, TransferResponse.java와 fault 클래스인 Fault.java를 볼 수 있다. 이렇게 언어별 소스 생성에 자동화 도구를 사용할 수 있다는 점이 계약 우선 개발 방법의 장점 중 하나다.


11. 웹 서비스 클라이언트 인터페이스 정의

웹 서비스의 클라이언트 인터페이스도 wsdl2java 유틸리티가 WSDL로부터 자동으로 생성한다. com.brm.ws.payment 패키지 아래 PaymentPortType.java가 클라이언트의 웹 서비스 인터페이스다.


12. 웹 서비스 호출 구현

wsdl2java 유틸리티를 사용해 웹 서비스 WSDL로부터 자동으로 웹 서비스 클라이언트의 입출력 클래스와 웹 서비스 인터페이스를 모두 생성했으므로 이제 웹 서비스를 호출하기 위한 웹 서비스 클라이언트를 구현해 보자. 웹 서비스 클라이언트를 구현하려면 스프링 빈 정의 XML 파일에 웹 서비스 클라이언트의 CXF 빈과 카멜 라우팅 정의를 설정해야 하고, 이 XML 파일을 이용해 웹 서비스를 호출하는 웹 클라이언트 단위 테스트 클래스를 개발해야 한다. (웹 서비스 클라이언트의 스프링 빈 정의 XML파일도 "웹 서비스 서버 구현"에서 사용한 네임스페이스와 같은 네임스페이스를 사용하므로 이에 대한 지정은 "웹 서비스 서버 구현"을 참조하거나 아래 구현된 camel-context.xml을 참조한다.) 우선 스프링 빈 XML 파일을 설정해 보자. Maven 프로젝트로 생성한 웹 서비스 클라이언트의 src/main/resources/META-INF/spring/camel-context.xml을 다음과 같이 설정한다.

위 스프링 빈 XML 파일에서 cxf:cxfEndpoint 설정은 웹 서비스 서버의 cxf:cxfEndpoint 설정과 구조가 동일하다. 차이점은 웹 서비스 서버의 address 속성 값은 이 주소로 웹 서비스를 노출하고, 웹 서비스 클라이언트는 address 속성 값은 이 주소로 웹 서비스를 요청한다는 점이다. serviceClass 속성에는 웹 서비스 서버의 WSDL로부터 생성된 웹 서비스의 클라이언트 인터페이스를 지정한다.

카멜 라우팅 정의에서 웹 서비스는 생산자 엔드포인트다. 웹 서비스 클라이언트는 카멜의 direct 소비자 엔드포인트를 통해 웹 서비스(웹 서비스 생산자 엔드포인트)를 호출한다. 카멜에서는 생산자 엔드포인트를 동기적으로 호출하는 일관된 방법으로 direct 소비자 엔드포인트를 이용한다. 테스트 클라이언트가 오류를 처리하므로 카멜이 오류를 중간에서 굳이 가로채지 않게 라우팅 정의에 errorHandlerRef 속성을 사용해 카멜이 오류를 무시하도록 했다. errorHandlerRef 속성 값은 오류를 무시하는 noErrorHandler 빈의 참조다.

웹 서비스 클라이언트는 정상 처리와 비정상 처리를 모두 확인하기 위해 다음과 같이 두 테스트 메소드를 포함한 JUnit 테스트 클래스로 작성했다.

이 단위 테스트 클래스는 이체 한도 이내의 이체 웹 서비스 요청과 이체 한도 초과의 웹 서비스 요청을 실행한다. 웹 서비스 단위 테스트 클라이언트 클래스가 카멜의 동기 요청 호출 메소드인 request…를 이용해 direct 소비자를 호출하면 카멜은 이 요청을 웹 서비스 생산자 엔드포인트로 라우팅한다.

이체 한도 이내의 이체 웹 서비스 요청 메소드인 testSmallAmount는 정상적은 응답 객체를 수신하고, 이체 한도 초과의 이체 웹 서비스 요청 메소드인 testLargeAmount는 fault를 수신한다. testLargeAmount 메소드 예외 처리과정을 좀더 자세히 살펴보자. 앞서 웹 서비스 서버의 Payment 서비스 인터페이스는 다음과 같이 정의했었다.

위처럼 웹 서비스 서버의 인터페이스는 TransferException를 transferFunds 메소드의 예외로 지정했었다. 즉 transferFunds 웹 서비스 호출 시 TransferException를 fault로 반환하도록 지정했다. 그리고 TransferException은 다시 Fault 클래스를 포함하고, 이 클래스가 실제 fault 메시지를 포함하도록 개발했었다. 여기서 카멜과 CXF 프레임워크의 오류 처리 관례를 제대로 이해하는 것은 아주 중요하다. 카멜과 CXF를 이용한 웹 서비스 개발의 관례를 제대로 이해하지 못한다면 웹 서비스 개발에 상당한 혼란을 초래할 수 있기 때문이다. 카멜과 CXF 프레임워크의 호출 처리 흐름은 다음과 같다. 정상적인 호출인 경우 입출력 객체만 활용되므로 특별한 어려움은 없다. 그러나 fault를 응답하는 경우 상당한 주위가 필요하다. 일반적으로 카멜은 라우팅 중 발생한 예외를 붙잡아 원인으로 포함시킨 후 CamelExecutionException을 발생시킨다. 이 예의 경우 카멜 예외의 원인은 웹 서비스 호출 중 발생한 TransferException_Exception 예외가 되며, 이 예외는 다시 실제 웹 서비스 서버가 발생시킨 fault를 (여기서는 TransferException을) 감싼다. 그러므로 카멜 메소드를 이용해 웹 서비스를 호출한 클라이언트는 예외가 발생했을 때 CamelExecutionException을 잡고 CamelExecutionException에서 TransferException_Exception를 추출한 후, TransferException_Exception에서 웹 서비스 서버가 발생시킨 fault를 (여기서는 TransferException을) 추출하고, fault에서 필드를(여기서는TransferException에서 Fault 객체를) 최종적으로 추출한다.

여기서 주목할 점은 CXF 프레임워크가 서비스 우선 개발로 작성한 서비스 메소드의 자바 예외 객체를 언어 중립적인 SOAP 입출력 구조가 되도록 WSDL 표현을 자동 생성한다는 점이다. 이 과정에서 CXF 프레임워크는 웹 서비스 서버의 자바 예외를 fault 필드로 표현이 가능하도록 멤버 객체들만 WSDL의 TransferException로 옮긴다. 즉 이 과정에서 자바 예외 클래스의 구조가 사라지고 예외 객체의 멤버들만 WSLD의 fault 필드에 포함된다. 이런 이유로 해서, 이 예의 웹 서비스 서버의 WSDL로부터 생성한 웹 서비스 클라이언트의 TransferException 객체는 자바 예외가 아닌 자바 빈(bean) 클래스가 된 것이다. (이런 혼란이 발생하지 않도록 좀더 섬세하게 웹 서비스를 구축하려면 웹 서비스 어노테이션과 카멜의 예외 핸들러를 이용해야 한다. 그러나 이런 기술을 포함하는 경우 처음 웹 서비스를 맛보거나 실용적인 웹 서비스 구축을 원하는 독자들에게 지나치게 복잡한 설명을 포함해야 할 것 같아, 이에 대한 설명은 포함하지 않았다.)


13. 웹 서비스 클라이언트 실행

이제 웹 서비스를 클라이언트를 실행해 보자. 웹 서비스 클라이언트를 실행하기 전에 웹 서비스 서버를 먼저 실행해야 한다. 글의 "웹 서비스 서버 실행"을 참조한다. 웹 서비스 서버를 실행했으면, maven을 이용해 웹 서비스 클라이언트 테스트를 실행한다. 우리는 maven의 관례에 따라 JUnit 테스트 클래스를 작성했으므로, 테스트 클라이언트의 실행은 다음과 같이 간단하다.

다음은 테스트 클라이언트의 실행 결과이다.

위 결과를 보면 웹 서비스 클라이언트 JUnit 클래스는 적은 금액의 이체 요청(testSmallAmount)의 결과로는 Response 객체에 reply로 OK 값을 반환 받았고, 큰 금액의 이체 요청(testLargeAmount)의 결과로는 예외를 반환 받았다. 그리고 예외를 반환 받은 경우 웹 서비스 클라이언트는 예외 객체 안에서 실제 Fault 정보를 획득할 수 있었다. 기대했던 결과를 얻은 것이다.


14. 맺음말

우리는 이 글에서 서비스 우선 개발(service-first development) 방법으로 SOAP 웹 서비스 서버를 개발했고, 계약 우선 개발(contract-first development) 방법으로 SOAP 웹 서비스 클라이언트를 개발해 보았다. SOAP 웹 서비스 서버는 일반적인 POJO 서비스를 개발하는 절차에 카멜과 CXF 프레임워크 설정을 포함하는 30줄 이내의 스프링 빈 정의 XML을 작성하고, maven으로 camel:run 골(goal)을 실행함으로 완성했다. SOAP 웹 서비스 클라이언트는 웹 서비스 서버의 WSDL로부터 패키지와 클래스들을 생성하고, 카멜과 CXF 프레임워크 설정을 포함하는 30줄 이내의 스프링 빈 정의 XML을 추가하고, 웹 서비스 클라이언트 JUnit 테스트 클래스를 개발하여 maven의 test 단계를 실행함으로 웹 서비스 클라이언트를 테스트 할 수 있었다.

실질적으로 웹 서비스 서버를 구축하는 데 추가된 스프링 빈 정의 XML의 설정은 30줄 이내였고, 웹 서비스 클라이언트가 웹 서비스를 호출하도록 하는 데 추가된 스프링 빈 정의 XML의 설정도 30줄 이내였다. 이런 정도의 개발량으로 웹 서비스를 구축하는 방법으로는 카멜을 활용하는 것 이외는 달리 없을 것이다. 이점이 바로 카멜이 갖는 강력함이다.

이와 같이 짧은 소스나 간단한 설정으로 구축되는 기능 중심의 서비스를 마이크로 서비스(Microservice)라 부른다. 물론 마이크로 서비스가 갖는 여러 가지 특징들이 있긴 있으나, 현재 이 용어는 완전하게 정립되지는 않았다. 그럼에도 아파치 카멜은 마이크로 서비스를 구현하는 데 가장 적합한 도구라는 것을 필자를 포함해 통합 전문가들이 동의하고 있다. 앞으로는 중앙 집중식 단일 시스템에 의한 서비스들의 제공이 아닌 기능 별 분산 자치식 시스템에 의한 마이크로 서비스로의 패러다임이 다가 올 것이다. 그리고 마이크로 서비스 인프라 솔루션을 구축함에 있어, 기업 통합 패턴의 패턴 언어는 중심 방법론이 될 것이고, 아파치 카멜은 중요한 구축 도구로서 사용될 것이다.

이 글은 기업 통합 패턴 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현"을 아파치 카멜로 대체하는 방법을 모색하던 중, 동기 호출 부분을 웹 서비스로 대체하는 과정에서 획득한 기술적인 노하우를 별도의 내용으로 정리한 글이다. 기업 통합 패턴 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현"을 아파치 카멜로 대체한 글에 이 기술을 활용할 것이므로 이 글 이후 이후 조만간 게시할 글도 관심 있게 지켜봐 주길 바란다.


참고 사이트

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년 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를 활용할 수 있을 것이다.

참고 사이트)