본 장에서는 JEUS에서의 트랜잭션 매니저와 그 주변 요소들에 대해 설명한다.
JEUS 트랜잭션 매니저는 Java EE 환경에서 애플리케이션에게 다양한 형태의 트랜잭션 서비스를 제공할 수 있다. JEUS 트랜잭션 매니저는 애플리케이션의 동작에 따라 필요한 서비스를 제공하며, 다양한 리소스 매니저와의 연결을 제공한다. 또한 애플리케이션은 필요에 따라 트랜잭션의 제어 권한을 매니저에 일임할 수도 있고, 자신이 직접 갖을 수도 있다. 트랜잭션 매니저는 리소스 매니저, 애플리케이션 등과 함께 트랜잭션 처리를 위한 작업들을 하며, 그 작업에서 중점적인 역할을 담당한다.
엔터프라이즈 시스템에서 트랜잭션 서비스는 리소스에 대한 대용량의 클라이언트 요청을 안전하고 효율적으로 처리하는데 기본적이고 중요한 서비스이다. 최근에는 리소스의 종류가 다양해지고, 리소스에 대한 요청 규모가 커짐에 따라, 트랜잭션 매니저는 통일된 인터페이스를 제공하고 더욱 안정적으로 동작할 필요성이 생겼다.
JEUS의 트랜잭션 매니저는 Java Transaction Service(JTS)와의 호환성을 제공하며, Java Transaction API(JTA)를 지원한다. 이는 클라이언트가 트랜잭션을 이용할 때 통일된 인터페이스와 기능을 제공하기 위함이다. 자세한 내용은 관련 API 문서나, 스펙 문서를 참고한다.
또한 다양한 형태의 애플리케이션을 지원하기 위하여 애플리케이션이 동작할 수 있는 여러 가지 상황(클라이언트, 서버 애플리케이션)에서의 트랜잭션 서비스를 제공한다.
트랜잭션 매니저는 안정적인 서비스 제공을 위하여 문제 상황이 발생하더라도 트랜잭션의 무결성을 보장해야 한다. 이를 위해 JEUS 트랜잭션 매니저는 트랜젹션 복구(Transaction Recovery) 기능을 제공하여, 다양한 문제 상황 이후에 트랜잭션을 복구할 수 있도록 한다.
JEUS에서의 트랜잭션 작업에 참여하는 요소는 다음과 같이 크게 3가지로 나누어진다.
첫 번째로 서비스를 받는 입장에서의 애플리케이션이 있다.
애플리케이션은 웹 컨테이너나 EJB 컨테이너, 클라이언트 컨테이너를 통해서 다양한 리소스 매니저로 접속할 수 있다. 애플리케이션은 스스로 트랜잭션의 작업과 끝을 지정하여 트랜잭션을 조정할 수도 있고, 컨테이너의 트랜잭션 매니저가 그러한 작업들을 대신 할 수도 있다.
두 번째로 그 애플리케이션에 트랜잭션 관리 기능과 그에 따르는 인터페이스를 제공하는 트랜잭션 매니저가 있다.
트랜잭션 매니저는 애플리케이션의 동작 위치에 따라 서버 또는 클라이언트 트랜잭션 매니저로 나뉘며, 실제 리소스 매니저에 직접적으로 트랜잭션 작업을 준비, 반영 요청을 전달하는 일을 한다.
세 번째로 실제 트랜잭션들이 영향을 주게 되는 리소스 매니저(예를 들면 DB)가 위치하게 된다.
리소스 매니저는 이러한 요청들을 받아들여 실제 작업을 하게 된다.
각 구성 요소는 트랜잭션 처리를 위하여 표준 API인 JTA를 이용하여 서로 통신한다.
본 절에서는 각 요소에 대해 설명하며 각각에 대해 동작 방식을 알아보도록 한다. 트랜잭션의 기본적인 내용은 관련 서적이나 자료 등을 참고한다.
애플리케이션은 JEUS 트랜잭션 매니저를 이용하여 리소스 매니저에 트랜잭션 관련 작업을 수행한다. 애플리케이션은 몇 개의 리소스 매니저를 이용하느냐에 따라 로컬 트랜잭션과 글로벌 트랜잭션 2가지를 사용할 수 있다. 또한 트랜잭션의 결정 권한을 갖는 주체에 따라서 설정 및 프로그래밍 방식에 따라 나뉠 수 있다.
다음은 이러한 분류에 대한 설명이다.
하나의 리소스 매니저를 사용하는 트랜잭션 작업들은 하나의 로컬 트랜잭션으로 묶일 수 있다. 애플리케이션은 리소스 매니저의 드라이버를 사용해서 로컬 트랜잭션을 시작하거나 종료한다.
기본적으로 JEUS 트랜잭션 매니저는 로컬 트랜잭션 처리에는 관여하지 않는다. 애플리케이션이 작업을 글로벌 트랜잭션으로 처리할 수 있지만, 하나의 리소스 매니저를 이용하는 단순한 트랜잭션의 작업을 할 경우라면 로컬 트랜잭션이 훨씬 효율적이므로 로컬 트랜잭션을 사용하길 권장한다.
애플리케이션이 2개 이상의 리소스 매니저를 이용하여 일련의 트랜잭션 작업을 시도할 경우, 트랜잭션 매니저는 이 트랜잭션들을 하나의 글로벌 트랜잭션으로 묶어서 처리할 수 있다. 이 글로벌 트랜잭션은 흔히 2 Phase Commit 프로토콜을 사용하여 사용 중인 리소스들에 대해 일괄적인 트랜잭션 작업을 가능하게 해준다.
트랜잭션 매니저는 트랜잭션을 commit할 때 애플리케이션의 실행 흐름을 추적해서 1 Phase Commit을 할지 2 Phase Commit을 할지를 결정한다. 이 결정은 트랜잭션 내에 사용된 리소스 매니저의 개수와 타입에 따라서 결정된다. JEUS 트랜잭션은 중복된 트랜잭션을 지원하지 않으며, 2개의 트랜잭션이 섞여서 진행하는 것은 허락하지 않는다.
애플리케이션은 javax.transaction.UserTransaction 객체를 사용해서 트랜잭션의 시작과 종료 시기를 지정할 수 있다. 트랜잭션을 commit할 것인지 rollback할 것인지는 전적으로 애플리케이션에 의해 결정된다. 그러나 commit을 했음에도 불구하고, 리소스 매니저가 트랜잭션을 처리할 수 없을 때 JEUS 트랜잭션 매니저가 rollback시킬 수도 있다.
EJB에서는 이 User Managed 트랜잭션을 Bean-Managed 트랜잭션(BMT)이라고 한다.
Container Managed 트랜잭션은 컨테이너가 트랜잭션의 시작과 종료시기를 결정하는 방식이다. 트랜잭션의 commit, rollback 등의 결정은 전적으로 컨테이너에 의해 결정된다. EJB에서만 사용할 수 있으며 Bean의 각 메소드 별로 트랜잭션 속성 NotSupported, Required, Supports, RequiresNew, Mandatory, Never를 지정해서 사용한다.
JEUS는 서버 트랜잭션 매니저와 클라이언트 트랜잭션 매니저의 2가지 트랜잭션 매니저를 제공한다.
서버 트랜잭션 매니저는 글로벌 트랜잭션을 관리하기 위한 모든 기능을 제공한다. 반면에 클라이언트 트랜잭션 매니저는 서버 트랜잭션의 대행(proxy) 역할만을 수행한다.
서버 트랜잭션 매니저는 Java Transaction API를 완전 구현했으며, 서버 애플리케이션이 트랜잭션을 사용할 경우 동작한다.
글로벌 트랜잭션이 시작되면 서버 트랜잭션 매니저는 글로벌 트랜잭션과 관련된 모든 정보를 수집해서, 트랜잭션의 Coordinator로서 작업을 진행한다. 상황에 따라서 Root Coordinator와 Sub Coordinator로 역할이 나뉜다.
Root Coordinator : 트랜잭션이 시작되는 트랜잭션 매니저가 담당하며, 여기서 시작된 트랜잭션이 전파되는 매니저는 Sub Coordinator가 된다.
Sub Coordinator : Root Coordinator로부터 결정을 받아 해당하는 작업을 수행한다.
즉, 능동적으로 트랜잭션을 관리하는 역할이 Root Coordinator, 이 내용을 따라 하는 수동적 역할이 Sub Coordinator가 할 일이다.
다음은 Root Coordinator와 Sub Coordinator의 관계에 대한 예제이다.
클라이언트 트랜잭션 매니저는 서버 트랜잭션 매니저의 대행자(proxy)로 작동한다. 글로벌 트랜잭션의 신호를 받은 서버 트랜잭션 매니저가 Root Coordinator의 역할을 하게 되며, 해당 트랜잭션에 관한 정보를 모두 모아서 처리한다. commit 시점에 클라이언트 트랜잭션 매니저는 commit 신호를 Root Coordinator에 전송하고, commit의 결과를 받게 된다.
애플리케이션은 다양한 리소스 매니저와 작업을 할 수 있다. 커넥션 매니저는 리소스 매니저(이하 RM)와의 연결을 관리한다.
JEUS에서는 4가지 타입의 커넥션 매니저를 제공한다. 글로벌 트랜잭션을 위해서 커넥션 매니저는 트랜잭션과 관련되는 정보를 트랜잭션 매니저에 보고한다.
Java Database Connectivity(JDBC)는 Java 언어로 개발할 때 사용하는 데이터베이스와 애플케이션 간의 연결을 정의한 것이다. 애플리케이션은 JDBC RM을 Naming Server로부터 lookup해서 사용한다. 자세한 것은 “제6장 JNDI Naming Server”와 “제8장 DB Connection Pool과 JDBC”를 참고한다.
Java Message Service(JMS)는 Queue와 Topic 그리고 거기에 저장된 메시지에 액세스하기 위한 API를 정의한 스펙이다. 자세한 내용은 "JEUS MQ 안내서"나 JMS 스펙을 참고한다.
Tmax는 TmaxSoft에서 개발된 TP-monitor이다. JEUS에서는 Tmax의 트랜잭션 매니저와 투명한 양방향 서비스를 위해서 WebT라는 Bridge를 제공한다. WebT는 양방향 메소드 호출을 지원한다. 그래서 Tmax위에서 일반적인 EJB 클라이언트가 작동될 수 있으며, JEUS에 있는 EJB Bean을 동일한 방법으로 호출할 수 있다.
Java Connector Architecture (JCA) RM
JCA는 JavaEE 스펙 중의 하나로서 JavaEE 호환 플랫폼과 다양한 EIS(Enterprise Information System)와의 통합 메커니즘을 제공한다. XA 트랜잭션을 지원하는 Connector를 사용해서, 애플리케이션이 다양한 트랜잭션 작업을 하나의 글로벌 트랜잭션으로 다룰 수 있다. 자세한 내용은 "JEUS JCA 안내서"를 참고한다.
본 절에서는 JEUS 트랜잭션 매니저의 설정 방법에 대해 설명한다.
JEUS 사용자는 설정을 통해 다양한 선택을 할 수 있다. 이러한 선택 사항들은 JEUS 트랜잭션 매니저의 성능 및 동작에 큰 영향을 미치게 된다. 그러므로 각각의 내용을 숙지하고 알맞은 설정을 하도록 한다.
트랜잭션 매니저의 설정은 JEUSMain.xml 파일을 직접 수정하거나, WebAdmin에서 설정할 수 있다. 기본적으로 WebAdmin을 통해 설정하기를 권장한다.
다음은 서버 트랜잭션 매니저의 설정에 대한 예로 JEUSMain.xml의 각 <engine-container>의 <tm-config> 태그에 설정한다.
[예 9.1] <<JEUSMain.xml>>
<jeus-system> <node> . . . <engine-container> . . . <tm-config . . . </tm-config> . . . </engine-container> . . . </node> . . . </jeus-system>
이 외에도 Java 시스템 프로퍼티를 이용하여 설정할 수도 있다. 서버 트랜잭션 매니저의 경우 JEUSMain.xml의 설정 파일을 이용할 수 있지만, 클라이언트 애플리케이션은 그럴 수 없기 때문에 이런 방식을 제공한다. 이후 내용은 JEUSMain.xml에 작성되어야 하는 내용을 중점적으로 다룬다. 서버 트랜잭션 매니저의 경우 시스템 프로퍼티를 이용한 설정을 지양하고, JEUSMain.xml을 이용한 설정이나 WebAdmin을 이용한 설정 방법을 주로 사용하도록 한다.
JEUS 트랜잭션 매니저에서는 다른 트랜잭션 매니저 간의 통신을 지원하기 위해서 여러 개의 Worker Thread를 사용한다. Worker Thread Pool은 다음과 같은 속성으로 설정된다.
다음은 Worker Thread Pool 설정에 대한 예로 JEUSMain.xml의 <engine-container> 태그 내부의 <tm-config><pooing>...</pooling></tm-config> 태그에서 설정한다.
[예 9.2] Worker Thread Pool 설정 : <<JEUSMain.xml>>
<jeus-system> <node> . . . <engine-container> . . . <tm-config> <pooling> <min>10</min> <max>50</max> <period>1800000</period> </pooling> . . . </tm-config> . . . </engine-container> . . . </node> . . . </jeus-system>
다음은 설정 태그에 대한 설명이다.
JEUS 트랜잭션 매니저는 예외 상황 처리를 위해서 다양한 타임아웃 메커니즘을 사용한다. 타임아웃 메커니즘의 값을 조정해서 애플리케이션 시스템에 가장 적합하도록 트랜잭션 매니저를 튜닝할 수 있다. 타임아웃의 단위는 모두 ms이다.
다음은 타임아웃 설정에 대한 예이다.
[예 9.3] 타임아웃 설정 : <<JEUSMain.xml>>
<jeus-system> <node> . . . <engine-container> . . . <tm-config> <pooling> <min>10</min> <max>50</max> <period>1800000</period> </pooling> <active-timeout>300000</active-timeout> <prepare-timeout>300000</prepare-timeout> <prepared-timeout>300000</prepared-timeout> <commit-timeout>300000</commit-timeout> <recovery-timeout>100000</recovery-timeout> . . . </tm-config> . . . </engine-container> . . . </node> . . . </jeus-system>
다음은 설정 태그에 대한 설명이다.
트랜잭션 매니저 용량을 설정해서 JEUS 트랜잭션 매니저는 내부 구조를 최적화시킨다. 트랜잭션 매니저가 동시에 처리하는 글로벌 트랜잭션의 개수를 고려해서 값을 설정한다. 웹 컨테이너나 EJB 컨테이너에서 설정하려면 JEUSMain.xml의 <tm-config><capacity>에 값을 설정한다.
[예 9.4] 트랜잭션 매니저 용량 설정 : <<JEUSMain.xml>>
<jeus-system> <node> . . . <engine-container> . . . <tm-config> . . . <capacity>20000</capacity> . . . </tm-config> . . . </engine-container> . . . </node> . . . </jeus-system>
트랜잭션 매니저는 역할에 따라 Root Coordinator와 Sub Coordinator로 나누어진다.
Sub Coordinator는 Root Coordinator로부터 트랜잭션 관련 사항을 전파받아야 하기 때문에 Root Coordinator에 자신을 등록시켜 Root가 자신의 존재를 알게 한다.
이와 관련된 설정으로 다음에 나열된 2가지가 있다. 사용자는 퍼포먼스와 안정성을 고려해서 옵션을 적절히 사용하도록 한다.
-Djeus.tm.forcedReg=<true or false>
이 설정은 Sub Coordinator가 Root Coordinator에게 언제 자신을 등록시킬 것인가에 대한 설정이다.
설정값 | 설명 |
---|---|
true | 트랜잭션 전파 상황에서 Sub Coordinator는 곧바로 자신을 등록한다. (기본값) |
false | 실제로 Sub Coordinator에 연결된 리소스 매니저를 사용할 때 Root Coordinator에 등록한다. 이렇게 할 경우 실제 Sub Coordinator에 등록 된 리소스 매니저를 사용하지 않는 EJB Call이 많은 경우에, 트랜잭션 매니저 간의 통신 횟수를 줄일 수 있다. |
-Djeus.tm.checkReg=<true or false>
설정값 | 설명 |
---|---|
true | Sub Coordinator가 자신을 등록할 때 Root로부터 ACK를 기다린다. 만약 일정 시간 내에 ACK가 도착하지 않으면 등록 실패로 간주하고 대기 중인 트랜잭션들을 Rollback을 시킨다. (기본값) |
이와 더불어 트랜잭션 매니저 사이의 통신 방식을 결정해 줄 수도 있다. 여기서 말하는 통신 방식이란 Non-blocking I/O와 Blocking I/O 2가지이다.
<tm-config> <use-nio>...</use-nio> </tm-config> 또는 -Djeus.tm.usenio=<true or false>
기본값은 서버일 경우 true, 클라이언트일 경우 false이다.
true일 경우 Non-blocking I/O를 이용하여 결정한다.
클라이언트 트랜잭션 매니저는 클라이언트 애플리케이션이 글로벌 트랜잭션을 시작하고 종료하기 위해서 만들어졌다. 본 절에서는 클라이언트에 특별한 의미를 갖는 설정들에 대해 설명한다.
클라이언트 애플리케이션은 JEUSMain.xml의 설정 등을 사용하지 않기 때문에 애플리케이션 실행 스크립트에 시스템 프로퍼티 형태로 설정을 지정한다.
클라이언트 트랜잭션 매니저는 클라이언트 애플리케이션이 JNDI lookup을 사용할 때 초기화된다. 그러나 클라이언트 애플리케이션에 따라 트랜잭션 매니저를 사용하지 않는 경우가 있다. 이럴 때 생기는 추가적인 오버헤드를 없애기 위해 이 내용을 실행 스크립트에 저장한다.
-Djeus.tm.not_use=<true or false>
클라이언트 트랜잭션 매니저는 클라이언트 컨테이너에서만 사용된다. 사용하려면 다음을 클라이언트 애플리케이션 스크립트에 추가한다.
-Djeus.tm.version=client
클라이언트 트랜잭션 매니저는 다른 트랜잭션 매니저와 통신하기 위해서 TCP/IP Port를 사용한다. 기본적으로 클라이언트 트랜잭션 매니저는 적절한 Port를 스스로 찾아서 사용하나, 직접 설정하려면 다음을 클라이언트 애플리케이션 스크립트에 추가한다.
-Djeus.tm.port=<port number>
다음은 Worker Thread Poolㅇ르 설정하는 방법이다.
Min
Max
다음은 타임아웃을 설정하는 항목에 대한 설명이다.
글로벌 트랜잭션이 시작되면 이 시간 안에 Commit이 실행되어야 한다. 그렇지 않으면 트랜잭션 매니저가 Rollback시킨다.
클라이언트 컨테이너에서 이 값을 설정하려면 클라이언트 애플리케이션의 스크립트에 다음을 추가한다.
-Djeus.tm.activeto=<time in milliseconds>
Root Coordinator는 이 시간 내에 Sub Coordinator와 리소스 매니저로부터 ‘prepare’ 신호를 받아야 한다. 만약 받지를 못하면 Root Coordinator는 글로벌 트랜잭션을 Rollback시킨다.
클라이언트 컨테이너에서 이 값을 설정하려면 클라이언트 애플리케이션의 스크립트에 다음을 추가한다.
-Djeus.tm.prepareto=<time in milliseconds>
Sub Coordinator는 자신의 Root Coordinator로부터 이 시간 안에 Commit을 해야 할지, Rrollback을 해야 할지를 나타내는 글로벌 decision을 받아야 한다. 만약 이 시간 내에 받질 못하면 Root Coordinator 로 다시 ‘prepare’에 대한 응답 메시지를 보낸다. 그래도 여전히 시간 내에 글로벌 decision이 오지 않는다면, 글로벌 트랜잭션을 Rollback시킨다.
클라이언트 컨테이너에서 이 값을 설정하려면 클라이언트 애플리케이션의 스크립트에 다음을 추가한다.
- Djeus.tm.preparedto=<time in milliseconds>
Root Coordinator는 이 시간 내에 Sub Coordinator와 리소스 매니저로부터 Commit이나 Rollback에 대한 결과를 받아야 한다. 만약 결과가 오지 않으면, Root Coordinator는 글로벌 트랜잭션을 ‘Uncompleted List’에 기록해서 트랜잭션이 완전히 끝나지 않았음을 남겨둔다.
클라이언트 컨테이너에서 이 값을 설정하려면 클라이언트 애플리케이션의 스크립트에 다음을 추가한다.
- Djeus.tm.committo=<time in milliseconds>
트랜잭션을 복구하는 경우에 사용된다. 트랜잭션 매니저는 트랜잭션 복구를 위해서 복구될 트랜잭션 정보를 가져오려고 한다. 만약 다른 트랜잭션 매니저에서 이 시간 내에 복구 정보를 알려주지 않으면 해당 트랜잭션을 Rollback시킨다.
클라이언트 컨테이너에서 이 값을 설정하려면 클라이언트 애플리케이션의 스크립트에 다음을 추가한다.
-Djeus.tm.recoveryto==<time in milliseconds>
본 절에서는 JEUS 트랜잭션 프로그래밍 예제를 설명한다.
로컬 트랜잭션과 클라이언트 관리(Client-managed) 트랜잭션, Bean 관리(Bean-managed) 트랜잭션 그리고 컨테이너 관리(Container-managed) 트랜잭션에 대해서 설명한다. 사용자 및 애플리케이션 프로그래머는 현재 자신의 애플리케이션이 어떤 양식으로 운영되어야 하는가를 확실히 지정한 후 프로그래밍을 해야 한다. 그래야만 예측된 결과를 얻을 수 있으며, 추후 문제 발생시 쉬운 문제해결이 가능하다.
애플리케이션은 트랜잭션 작업을 하나의 트랜잭션으로 관리할 수 있다. 작업이 하나의 리소스 매니저를 통해서 처리된다면 로컬 트랜잭션을 형성한다. 그렇지 않다면 트랜잭션을 관리하기 위해서 글로벌 트랜잭션을 사용할 수밖에 없다.
트랜잭션 프로그래밍 패턴에는 4가지가 있다.
로컬 트랜잭션(Local Transaction)
클라이언트 관리 트랜잭션(Client Managed Transaction)
Bean 관리 트랜잭션(Bean-Managed Transaction)
컨테이너 관리 트랜잭션(Container Managed Transaction)
로컬 트랜잭션(Local Transaction)은 하나의 리소스 매니저에 대한 트랜잭션 작업을 관리하는 데 효과적인 방법이다. 가볍고 빠르기 때문에 가능하면 로컬 트랜잭션을 사용한다. 로컬 트랜잭션은 JEUS 트랜잭션 매니저와는 아무런 관계가 없으며, 모든 타입의 컨테이너에서 사용할 수 있다.
다음은 로컬 트랜잭션을 사용하는 간단한 예제이다. 비록 Java 애플리케이션이지만 코드 일부분은 서블릿이나 EJB 등과 같은 다른 JavaEE 프로그래밍에서도 사용할 수 있다.
[예 9.5] <<Client.java>>
import javax.naming.*; import javax.rmi.PortableRemoteObject; import java.util.*; import javax.transaction.UserTransaction; import java.sql.*; import javax.sql.*; public class Client { private static Connection con; private static Connection getConnection() throws NamingException, SQLException { // get a JDBC connection } private static String insertCustomer(String id, String name, String phone) throws NamingException, SQLException { // insert a product entity given by the arguments to DB } private static void deleteCustomer(String id) throws NamingException, SQLException { // delete a product entity given by the arguments from DB } public static void main(String[] args) { try { // get a JDBC connection con = getConnection(); // set the autocommit attribute as false con.setAutoCommit(false); // insert customers for (int i=0 ; i<number/2 ; i++) { System.out.println("inserting customer id="+i+"c... from Client"); customers[i] = insertCustomer(i+"c", "Hong Kil Dong "+i, "000-123-1234-"+i); } System.out.println("completed inserting customers!!"); con.commit(); // delete customers for (int i=0 ; i<number/2 ; i++) { System.out.println( "deleting customerid="+customers[i]+" ... from Client"); deleteCustomer(customers[i]); } System.out.println("completed deleting customers!!"); con.commit(); } catch (NamingException ne) { System.out.println("Naming Exception caught : " + ne.getMessage()); } catch (SQLException sqle) { System.out.println("SQL Exception caught : " + sqle.getMessage()); } catch(Exception e) { try { con.rollback(); } catch (Exception ee) { System.out.println("Transaction Rollback error : " + ee.getMessage()); } System.out.println("Error caught : " + e.getMessage()); e.printStackTrace(); } finally { try { if (con!=null) con.close(); } catch (SQLException se) {} } } }
클라이언트 컨테이너(Client Managed Transaction)의 애플리케이션에서 글로벌 트랜잭션을 사용할 수 있다. 클라이언트는 UserTransaction을 이용하여 직접 트랜잭션을 관리하고, 실제 작업을 해주는 EJB를 호출한다. 다음은 간단한 예제이다.
글로벌 트랜잭션을 시작하기 전에 ‘java:comp/UserTransaction’이나 'java:/UserTransaction'을 lookup해서 javax.transaction.UserTransaction의 인스턴스를 가져온다.
애플리케이션에서 트랜잭션을 사용하기 위해선 'java:comp/UserTransaction'을 lookup하고 standalone client에서 트랜잭션을 사용하기 위해선 'java:/UserTransaction'을 lookup한다. 이렇게 lookup해온 javax.transaction.UserTransaction 인스턴스의 begin()을 호출해서 글로벌 트랜잭션을 시작한 다음에, EJB Bean을 여러 번 호출한다. EJB Bean의 트랜잭션 작업을 Commit하려면 UserTransaction 인스턴스의 commit() 메소드를 실행해서 트랜잭션이 완료되었음을 알린다.
정상적으로 Commit되었다면 메소드는 끝나게 된다. 장애가 발생해서 글로벌 트랜잭션이 Commit되지 못한다면 메소드에서 Exception을 던지게 된다. javax.transaction.RollbackException은 JEUS 트랜잭션 매니저가 글로벌 트랜잭션이 Rollback되었다는 것을 보장하는 Exception이다. 이외에 다른 Exception은 JEUS 트랜잭션 매니저가 예측할 수 없는 Exception이 발생해서 글로벌 트랜잭션을 Rollback하려고 한다는 것을 나타낸다. 그러나 글로벌 트랜잭션에 포함된 모든 트랜잭션 작업들이 완전히 Rollback된다는 것은 아니다. 이럴 때는 catch 문 안에서 다시 한 번 직접 UserTransaction의 rollback() 메소드를 호출해 글로벌 트랜잭션을 Rollback시켜준다.
[예 9.6] <<Client.java>>
package umt; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.rmi.PortableRemoteObject; import java.util.*; import javax.transaction.UserTransaction; public class Client { public static void main(String[] args) { UserTransaction tx = null; try { InitialContext initial = new InitialContext(); ProductManager productManager = (ProductManager)initial.lookup("productmanager"); System.out.println(""); System.out.println("< Testing ProductManager EJBBean " + "Using User Managed Transaction >>"); System.out.println(""); int number = 10; String products[] = new String[number]; tx = (UserTransaction)initial.lookup("java:/UserTransaction"); tx.begin(); // insert products for (int i=0 ; i<number/2 ; i++) { System.out.println("inserting product id="+i+"b ..."); products[i] = productManager.insertProduct( i+"b","ball pen", i*10); // bean call } for (int i=number/2 ; i<number ; i++) { System.out.println("inserting product id="+i+"f..."); products[i] = productManager.insertProduct( i+"f", "fountain pen", (i-number/3)*50); } System.out.println("completed inserting products!!"); // delete products for (int i=0 ; i<number ; i++) { System.out.println("deleting productid="+products[i]+" ..."); productManager.deleteProduct(products[i]); // bean call } System.out.println("completed deleting products!!"); tx.commit(); } catch (javax.transaction.SystemException se) { System.out.println("Transaction System Error caught : " + se.getMessage()); } catch (javax.transaction.RollbackException re) { System.out.println( "Transaction Rollback Errorcaught : " + re.getMessage()); } catch(Exception e) { try { tx.rollback(); } catch (Exception ee) { System.out.println("Transaction Rollback error : " + ee.getMessage()); } System.out.println("Error caught : " + e.getMessage()); e.printStackTrace(); } } } }
다음의 EJB Bean은 트랜잭션 작업을 하는 메소드를 가지고 있다. 클라이언트에서 글로벌 트랜잭션을 관리하기 위해서는 EJB의 트랜잭션 타입이 CMT(container-managed)여야 한다. 기본값은 CMT이므로 아무것도 지정하지 않아도 되지만, 명시적으로 지정하고 싶다면 다음과 같이 TransactionManagerType 값을 지정해준다.
[예 9.7] <<ProductManagerEJB.java>>
package umt;
import javax.ejb.*;
import javax.annotation.*;
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class ProductManagerEJB implements ProductManager {
....
public String insertProduct(String id, String name, double price) {
// insert a product entity given by the arguments to DB
}
public void deleteProduct(String id) {
// delete a product entity indicated by the argument from
// DB
}
.....
}
Bean 관리 트랜잭션(Bean-Managed Transaction)은 웹 컨테이너와 EJB 컨테이너에서 애플리케이션은 JTA를 사용해서 글로벌 트랜잭션 경계를 정할 수 있다. 애플리케이션이 글로벌 트랜잭션을 세밀하게 조절할 필요가 있을 때 유용한다. 실행 순서에 따라 클라이언트의 요청을 처리할 수 있기 때문에 애플리케이션은 자신의 판단에 따라서 Commit과 Rollback을 할 수 있다.
다음은 EJB 컨테이너에서 애플리케이션이 글로벌 트랜잭션을 실행하는 예제이다.
BMT(Bean Managed Transaction)에서는 EJB Bean이 글로벌 트랜잭션 영역을 처리한다. 그러므로 모든 클라이언트는 단지 트랜잭션 작업을 하는 메소드를 호출하기만 하면 된다.
[예 9.8] <<Client.java>>
package bmt; import javax.naming.*; import javax.rmi.PortableRemoteObject; import java.util.*; public class Client { public static void main(String[] args) { try { ProductManager productManager; ....... // Getting a reference to an instance of // ProductManager EJB bean. ....... productManager.transactionTest(); } catch(Exception e) { e.printStackTrace(); } } }
EJB Bean은 글로벌 트랜잭션을 시작하기 위해서 javax.transaction.UserTransaction를 사용한다. javax.ejb.EJBContext(본 예제에서는 EJB가 Session Bean이므로 javax.ejb.SessionContext)를 사용해서 UserTransaction의 인스턴스를 얻어온다. 글로벌 트랜잭션은 메소드의 실행으로 begin되고 Commit된다.
EJB가 BMT로 작동하려면 Annotation으로 TransactionManagement 값을 TransactionManagementType.BEAN으로 지정한다.
[예 9.9] <<ProductManagerEJB.java>>
package bmt; import javax.ejb.*; import javax.naming.*; import java.sql.*; import java.util.*; import javax.annotation.*; import javax.transaction.UserTransaction; @Stateless @TransactionManagement(TransactionManagementType.BEAN) public class ProductManagerEJB implements ProductManager { @Resource SessionContext ejbContext; public void transactionTest() { UserTransaction tx = null; try { int number = 20; String products[] = new String[number]; tx = ejbContext.getUserTransaction(); tx.begin(); // insert products for (int i=0 ; i<number/2 ; i++) { System.out.println("inserting product id="+i+"b ..."); products[i] = insertProduct(i+"b", "ball pen", i*10); } for (int i=number/2 ; i<number ; i++) { System.out.println("inserting product id="+i+"f..."); products[i] = insertProduct(i+"f", "fountain pen", (i-number/3)*50); } System.out.println("completed inserting products!!"); // delete products for (int i=0 ; i<number ; i++) { System.out.println("deleting product id="+products[i]+" ..."); deleteProduct(products[i]); } System.out.println("completed deleting products!!"); tx.commit(); } catch (javax.transaction.SystemException se) { throw new EJBException( "Transaction System Error caught : " + se.getMessage()); } catch (javax.transaction.RollbackException re) { throw new EJBException( "Transaction Rollback Error caught : " + re.getMessage()); } catch(Exception e) { try { tx.rollback(); } catch (Exception ee) { throw new EJBException("Transaction Rollback error : " + ee.getMessage()); } throw new EJBException("Error caught : " + e.getMessage()); } } private String insertProduct(String id, String name, double price) throws NamingException, SQLException { // insert a product entity given by the arguments to DB } private void deleteProduct(String id) throws NamingException,SQLException { // delete a product entity indicated by the argument from // DB } // some EJB callback methods }
JavaEE 스펙에 의하면 애플리케이션은 글로벌 트랜잭션 경계(demarcation) 지정을 컨테이너에 위임할 수 있다. 웹 컨테이너나 EJB 컨테이너는 메소드와 관련된 글로벌 트랜잭션을 관리한다. 애플리케이션은 설정 파일에 메소드 별로 트랜잭션 속성을 정할 수 있다. 이 방법이 글로벌 트랜잭션을 처리하는 가장 쉬운 방법이다.
다음은 EJB 컨테이너가 관리하는 글로벌 트랜잭션의 예를 보여준다.
CMT(Container Managed Transaction)에서 서버 측 EJB 컨테이너가 EJB의 글로벌 트랜잭션 작업을 관리하는 것을 보여준다.
[예 9.10] <<Client.java>>
package bmt; import javax.naming.*; import javax.rmi.PortableRemoteObject; import java.util.*; public class Client { public static void main(String[] args) { try { ProductManager productManager; // getting a reference to an instance of // ProductManager EJB bean. productManager.transactionTest(); } catch(Exception e) { e.printStackTrace(); } } }
CMT의 장점은 EJB Bean 개발자가 비즈니스 로직 코드에서 더 이상 트랜잭션 관련 코드를 작성하지 않도록 한다는 것이다.
EJB Bean의 메소드에 적절한 트랜잭션 속성만 지정하면 글로벌 트랜잭션관련 작업은 모두 EJB 컨테이너에 위임된다. 다음의 예제에서 transactionTest()는 트랜잭션 관련 코드가 아무 것도 없다. EJB Bean이 CMT로 작동하도록 EJB 컨테이너에 알려주려면, TransactionManagement Annotation을 생략하거나 TransactionManagerType.CONTAINER 값으로 지정한다.
[예 9.11] <<ProductManagerEJB.java>>
package cmt;
import javax.ejb.*;
import javax.naming.*;
import java.sql.*;
import java.util.*;
import javax.annotation.*;
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class ProductManagerEJB implements ProductManager {
public void transactionTest() {
try {
int number = 20;
String products[] = new String[number];
// insert products
for (int i=0 ; i<number/2 ; i++) {
System.out.println("inserting product id="+i+"b...");
products[i] = insertProduct(i+"b", "ball pen", i*10);
}
for (int i=number/2 ; i<number ; i++) {
System.out.println("inserting product id="+i+"f...");
products[i] = insertProduct(i+"f", "fountain pen", (i-number/3)*50);
}
System.out.println("completed inserting products!!");
// delete products
for (int i=0 ; i<number ; i++) {
System.out.println("deleting product id="+products[i]+" ...");
deleteProduct(products[i]);
}
System.out.println("completed deleting products!!");
} catch(Exception e) {
throw new EJBException("Error caught : " + e.getMessage());
}
}
private String insertProduct(String id, String name, double price)
throws NamingException, SQLException {
// insert a product entity given by the arguments to DB
}
private void deleteProduct(String id) throws NamingException,
SQLException {
// delete a product entity indicated by the argument from
// DB
}
// some EJB callback methods
}
트랜잭션 매니저를 직접 사용하는 경우 JNDI Context에서 java:appserver/TransactionManager의 이름으로 lookup하면 트랜잭션 매니저를 얻을 수 있다. 트랜잭션 매니저는 트랜잭션을 시작, 종료할 수 있고 트랜잭션 객체를 얻거나 XAResource 객체를 등록하는 기능을 제공한다. 이에 대한 자세한 설명은 "Java Transaction API (JTA) specification"을 참고한다.
JEUS에서 트랜잭션 매니저를 얻거나 UserTransaction을 얻는 방법은 Glassfish 또는 SunOne Application Server와 호환된다. 따라서 Hibernate와 같은 3rd-party library를 사용할 때 호환되는 서버의 설정을 그대로 사용할 수 있다.
본 절에서는 트랜잭션 복구 작업에 대해 설명한다. 트랜잭션의 복구는 예상치 못한 문제 상황에 있어 트랜잭션 무결성을 지원하기 위한 중요한 기능이다. JEUS 사용자는 앞서 설명한 관련 설정 및 트랜잭션 복구 과정에 대해 숙지한다.
트랜잭션 매니저는 트랜잭션을 사용할 때 생길 수 있는 장애에 대한 대책을 가지고 있어야 한다. 장애 대책은 주로 진행중이던 트랜잭션의 무결성을 보장하는데 중점을 둔다. 트랜잭션 매니저는 트랜잭션 매니저의 역할에 따라서 복구 방식에 차이를 보인다. 즉 매니저가 Root 또는 Sub Coordinator인지, JEUS가 아닌 다른 외부 TM과 작업중인지에 따라서 복구 방식이 달라진다.
다음의 그림은 트랜잭션 매니저의 복구 과정을 간단히 보여준다.
1, 2) 트랜잭션 매니저가 장애 상황에서 복구 될 경우, 매니저는 트랜잭션 로그로부터 복구할 트랜잭션 정보를 얻어오게 된다.
3) 그 이후 로그의 내용에 따라 해당 리소스 매니저로 Recover 명령을 보낸다.
4) 리소스 매니저는 이에 대한 응답으로 현재 자신이 처리하지 못한 트랜잭션들의 XID를 매니저로 넘겨준다.
5) 트랜잭션 매니저는 이 XID들과 로그로부터 얻어진 정보를 가지고 Commit 또는 Rollback 판정을 리소스 매니저로 전달한다.
트랜잭션 매니저의 역할에 따라 조금씩 복구 방식의 차이는 있지만 큰 흐름은 앞서 설명한 내용을 벗어나지 않는다. 이후부터는 복구에 참여하는 각각의 요소에 대한 간략한 설명과 관련 설정들에 대해 설명한다.
트랜잭션 복구에서 트랜잭션 매니저는 가장 중심적인 역할을 한다. 다음은 트랜잭션 매니저 역할에 따른 복구 방식의 차이에 대한 설명이다.
Root Coordinator
트랜잭션 매니저가 Root Coordinator일 때는 [그림 9.2]와 같은 동작을 한다.
Sub Coordinator
Sub Coordinator는 Root Coordinator에게 하나의 리소스 매니저처럼 등록이 된다. 그리고 모든 Decision을 Root로부터 받아오므로 Sub Coordinator 자신에게 등록 된 리소스 매니저와 로그 파일에 대해 Xid를 얻어오는 작업까지만 수행한다. 그 이후 Xid를 Root Coordinator로 보내고, 판정을 기다린다. Root Coordinator로부터 판정이 넘어오면, 그 내용을 리소스 매니저에게 전달해 복구 작업을 마무리한다.
외부 트랜잭션 매니저와의 작업
JEUS 트랜잭션 매니저는 Root Coordinator로서 동작을 하고, 외부 트랜잭션 매니저는 JEUS를 하나의 리소스 매니저로 인식을 한다. 그러므로 판정을 내리는 작업을 제외한 모든 작업들을 수행한다. 그 이후 외부 트랜잭션 매니저가 JEUS에게 Recover 명령 및 판정을 내리면, 그에 따라 복구 작업을 완료한다.
만약 복구 작업 도중, 어떤 문제에 의해 복구가 실패할 경우 트랜잭션 매니저는 그 리소스에 대해 추가적인 복구를 시도한다. 이는 2분에 한 번씩 시행되며, 사용자는 시도 횟수를 지정해줄 수 있다. 시도 횟수 지정을 위해 다음 프로퍼티를 스크립트에 추가한다. 기본값은 10이다.
-Djeus.tm.recoveryTrial=<number of trial>
트랜잭션 복구에 사용되는 로그 파일은 다음의 2가지가 있다.
트랜잭션의 진행 상황을 기록한 로그
사용했던 XA 리소스 관련 로그
로그 파일은 JEUS_HOME\logs\TM의 하위 디렉터리에 컨테이너별로 생성된다. 만약 복구가 필요없는 트랜잭션때문에 기동 중 복구 관련 문제가 발생한다면, 이 로그를 삭제한 후 JEUS를 다시 실행한다.
하지만 시스템 관리자가 직접 복구할 경우나 아니면 성능 향상이 필요한 경우 Logging을 막을 수도 있다. 이 기능을 설정하려면 다음을 실행 스크립트에 추가한다.
-Djeus.tm.noLogging=true
웹 컨테이너나 EJB 컨테이너에서 설정하려면 JEUSMain.xml의 <command-option> 태그에서 위와 동일하게 설정해준다.
이 외에 현재 복구 상황을 따로 로그 파일로 분리해 볼 수도 있다. 이는 복구 상황에 대한 로그를 면밀히 살핌으로써, 트랜잭션 관련 문제를 쉽게 파악하고 해결하기 위함이다. 사용자 또는 애플리케이션 프로그래머는 이 로그를 통해 트랜잭션 ID등을 파악하여 문제해결에 이용할 수 있다. JEUSMain.xml에 다음과 같이 설정한다.
[예 9.12] <<JEUSMain.xml>>
<tm-config> <recovery-log-file> <name>recoveryHandler1</name> <file-name>blah.log</file-name> <valid-day>3</valid-day> </recovery-log-file> </tm-config>
이 로그는 다음의 경로에 생성된다.
JEUS_HOME\logs\<node-name>\<container-name>
설정 내용은 로그 파일 핸들러 설정과 동일하므로 이를 참고한다. 글로벌 트랜잭션 로그는 바이너리 포맷으로 복구되는 경우 사용되며, 복구 상황 로그는 일반적인 JEUS 로그 파일이다.
리소스 매니저에서 장애가 발생했다면, 시스템 매니저는 콘솔 툴(jeusadmin)을 사용해서 수동으로 복구하도록 한다. 그 이유는 JEUS 트랜잭션 매니저는 리소스 매니저가 다시 트랜잭션 처리가 가능한 상태인가를 알아낼 수 없기 때문이다. 리소스 매니저의 문제를 해결한 후에 jeusadmin의 tmresync 명령어로 복구를 진행한다. jeusadmin의 자세한 사용법은 “JEUS Reference Book”의 “4.2. jeusadmin”을 참조한다.