본문 바로가기

Dev.../플밍 관련 자료

[펌] JTS 이해하기 - 장막 뒤의 마법

 

Part 1에서 트랜잭션이 무엇이며 신뢰성 있는 분산 애플리케이션 구축에 이들이 필수적인지에 대한 기초적인 사항들을 다루었다. 이번 회에서는 J2EE 애플리케이션이 트랜잭션으로 어떻게 체계화되며, JTS와 J2EE 컨테이너가 트랜잭션 구분, 리소스 등록, 트랜잭션 전파등을 포함한 트랜잭션 서비스들을 컴포넌트 프로그래머들에게는 거의 보이지 않게 만드는 방법을 검토하겠다.

 

Part 1에서 트랜잭션을 검토하고 그들의 기본 특성인 원자성, 일관성, 격리성 및 영속성을 살펴보았다. 트랜잭션은 기업 애플리케이션의 기초적인 구성 요소이다. 트랜잭션 없이는 내결함성을 갖춘 기업 애플리케이션의 구축이 거의 불가능할 것이다. 다행히도 Java Transaction Service (JTS)와 J2EE 컨테이너는 여러분을 위해 트랜잭션 관리 작업의 상당 부분을 자동적으로 수행하며, 따라서 여러분은 여러분의 컴포넌트 코드에 직접 트랜잭션 인식 기능을 통합할 필요가 없다. 그 결과는 거의 일종의 마법이라 할 수 있다. 몇 가지 간단한 규칙을 따름으로써 J2EE 애플리케이션은 추가적인 컴포넌트 코드 없이 트랜잭션 의미론을 자동적으로 얻을 수 있다. 이 글은 트랜잭션 관리가 어떻게, 그리고 어디에서 수행되는지를 보여줌으로써 이 마법의 일부의 신비를 벗기는 데 목표를 둔다.

 

 

JTS란 무엇인가?

 

JTS는 컴포넌트 트랜잭션 모니터이다. 이것은 무슨 뜻인가? Part 1에서 우리는 애플리케이션을 대신해 분산되어 있는 트랜잭션들의 실행을 조정하는 트랜잭션 처리 모니터 (TPM)라는 개념을 소개하였다. TPM은 거의 데이터베이스만큼 오래된 것이다. IBM이 1960년대 후반에 최초로 CICS를 개발했는데, 이것은 지금도 여전히 사용되고 있다. 고전적인 (혹은 절차적인) TPM은 정의된 트랜잭션들을 트랜잭션 자원 (데이터베이스 등)에 대한 연속적인 작업으로써 절차적으로 관리한다. CORBA, DCOM, RMI와 같은 분산 객체 프로토콜의 출현으로 트랜잭션에 대한 보다 객체 지향적인 견해가 바람직하게 되었다. 객체 지향 컴포넌트에 트랜잭션적인 의미론을 주려면 TPM 모델의 확장이 필요한데, 여기에서는 트랜잭션이 트랜잭션적인 객체에 대한 메쏘드를 호출하는 측면에서 정의된다. JTS는 바로 그것이다. 컴포넌트 트랜잭션 모니터 (때때로 객체 트랜잭션 모니터), 즉 CPM이다.

JTS와 J2EE의 트랜잭션 지원 설계는 CORBA 객체 트랜잭션 서비스 (OTS)의 영향을 많이 받았다. 사실 JTS는 OTS를 구현하고, 트랜잭션 경계를 정의하는 하위 레벨 API인 자바 트랜잭션 API와 OTS간의 인터페이스 역할을 한다. 새로운 객체 트랜잭션 프로토콜을 만드는 대신 OTS를 사용하면 기존 표준들에서 구축할 수 있으며 J2EE와 CORBA 컴포넌트간의 호환을 위한 길이 열린다.

 

언뜻 보면 절차적 트랜잭션 모니터에서 CPM으로의 이행은 단지 용어의 변화로만 보여진다. 그러나 차이는 더 크다. CPM 내의 한 트랜잭션이 확약되거나 롤백되면 트랜잭션에 관여한 객체에 의해 수행된 모든 변경 사항들이 하나의 그룹으로 확약되거나 취소된다. 그런데 CTM은 그 트랜잭션 과정 동안 객체가 무엇을 했는지를 어떻게 알까? EJB 컴포넌트와 같은 트랜잭션 컴포넌트들은 commit()rollback() 메쏘드를 가지고 있지 않고 그들이 수행한 것을 트랜잭션 모니터에 기록하지도 않는다. 그러면 J2EE 컴포넌트가 수행한 작업이 어떻게 트랜잭션의 일부가 될까?

 

투명한 자원 등록

 

애플리케이션 상태는 컴포넌트에 의해 조작되지만 여전히 트랜잭션 자원 관리자 (예 : 데이터베이스와 메시지 큐 서버)에 저장되며, 이는 분산 트랜잭션에 자원 관리자로 등록될 수 있다. Part 1에서 우리는 복수의 자원 관리자가 하나의 트랜잭션에 어떻게 등록되고 트랜잭션 관리자에 의해 조정될 수 있는지에 관해 설명했다. 자원 관리자는 특정 트랜잭션과 애플리케이션 상태 변화를 연결시키는 방법을 알고 있다.

 

그러나 이것은 우리 질문의 포커스를 컴포넌트에서 자원 관리자로 옮기는 것일 뿐이다. 컨테이너는 어떤 자원들이 트랜잭션에 관여했는지를 어떻게 알고 이들을 등록시킬 수 있을까? 일반적인 EJB 세션 빈에서 볼 수 있는 다음 코드를 검토해 보자.

 

Listing 1.빈이 관리하는 트랜잭션에 자원을 투명하게 등록시키기

  InitialContext ic = new InitialContext();  UserTransaction ut = ejbContext.getUserTransaction();  ut.begin();  DataSource db1 = (DataSource) ic.lookup("java:comp/env/OrdersDB");  DataSource db2 = (DataSource) ic.lookup("java:comp/env/InventoryDB");  Connection con1 = db1.getConnection();  Connection con2 = db2.getConnection();  // perform updates to OrdersDB using connection con1  // perform updates to InventoryDB using connection con2  ut.commit();

 

이 예제에는 현재의 트랜잭션에 JDBC 연결을 등록시키는 코드가 없다는 점에 주목한다. 컨테이너가 이를 수행한다. 이것이 어떻게 일어나는지를 살펴보자.

 

세 가지 유형의 자원 관리자

 

한 EJB 컴포넌트가 데이터베이스나 메시지 큐 서버, 혹은 다른 트랜잭션 자원들에 접근하려고 할 때 이 컴포넌트는 자원 관리자에 연결된다 (보통 JNDI를 사용하여). 더구나 J2EE 사양은 단지 세 유형의 트랜잭션 자원만 인식하는데, JDBC 데이터베이스, JMS 메시지 큐 서버 및 "JCA를 통해 접근되는 다른 트랜잭션 서비스"가 그것이다. 마지막 클래스에 있는 서비스들 (ERP 시스템과 같은)은 JCA (J2EE COnnector Architecture)를 통해 접근되어야 한다. 이 자원 유형들 각각에 대해, 컨테이너나 제공자가 자원을 트랜잭션에 등록시키는 것을 돕는다.

 

Listing 1에서 con1con2DriverManager.getConnection()에서 반환되는 것과 같은 평범한 JDBC 연결처럼 보인다. 우리는 이러한 연결을 JDBC DataSource에서 얻는데, 이것은 JNDI 내의 데이터 소스의 이름을 검색하여 얻는다. 데이터 소스를 검색하기 위해 우리의 EJB 컴포넌트에서 사용되는 이름(java:comp/env/OrdersDB)은 컴포넌트에 한정된 것이다. 컴포넌트 배치 기술자의 resource-ref 섹션은 이 이름을 컨테이너가 관리하는 범 애플리케이션적인 일부 DataSource의 JNDI 이름과 매핑시킨다.

 

숨겨진 JDBC 드라이버

 

모든 J2EE 컨테이너는 트랜잭션 처리가 가능한 풀 방식의 DataSource 객체들을 생성할 수 있지만, J2EE 사양은 여러분에게 그 방식을 보여주지 않는다. 이것은 스펙 밖의 일이기 때문이다. J2EE 문서를 검색해 보면 JDBC 데이터 소스를 생성하는 방법에 대해서는 아무것도 발견하지 못할 것이다. 여러분은 대신 여러분의 컨테이너에 관한 문서를 살펴 보아야 할 것이다. 여러분의 컨테이너에 따라, 데이터 소스 생성은 데이터 소스 정의를 특성이나 설정 파일에 추가하는 작업을 포함할 수도 있고 GUI 관리 툴을 통해 수행될 수도 있다.

 

각 컨테이너 (혹은 PoolMan과 같은 연결 풀 매니저)는 DataSource를 생성하기 위해 자체적인 메커니즘을 제공하고 JTA 마법이 숨겨지는 것은 이 메커니즘 내이다. 연결 풀 관리자는 지정된 JDBC 드라이버에서 연결을 얻지만, 애플리케이션으로 반환하기 전에 이를 Connection을 구현하는 외관으로 포장하여 애플리케이션과 기반 연결 사이에 자신을 둔다. 연결이 생성되거나 JDBC 작업이 수행될 때 wrapper는 현재의 쓰레드가 트랜잭션 맥락에서 수행되고 있는지를 트랜잭션 관리자에게 물어보고 트랜잭션(존재할 경우)에 Connection을 자동으로 등록시킨다.

 

다른 유형의 트랜잭션 자원인 JMS 메시지 큐와 JCA connector는 사용자에게 자원 등록을 숨기기 위해 유사한 메커니즘을 사용한다. 배치시에 J2EE 애플리케이션에 JMS 큐를 사용가능하도록 만들 때 여러분은 다시 제공자에 따라 독자적인 메커니즘을 사용하여 관리된 JMS 객체 (큐 연결 팩토리와 수신지)를 생성하는데, 이를 JNDI 이름 공간에 공개한다. 제공자가 생성한 관리된 객체는 컨테이너가 제공한 연결 풀 관리자에 의해 추가된 JDBC wrapper와 유사한 자동 등록 코드를 가지고 있다.

 

투명한 트랜잭션 제어

 

두 유형의 J2EE 트랜잭션 - 컨테이너가 관리하는 트랜잭션과 빈이 관리하는 트랜잭션-은 트랜잭션을 시작시키고 끝내는 방법이 틀리다. 한 트랜잭션이 시작되고 끝나는 곳을 트랜잭션 구분 (transaction demarcation)이라고 부른다. Listing 1의 예제 코드는 빈이 관리하는 트랜잭션(때로 프로그램적인 트랜잭션이라고 불림)을 보여준다. 빈이 관리하는 트랜잭션은 UserTransaction 클래스를 사용하는 컴포넌트에 의해 명시적으로 시작되고 끝난다. UserTransactionejbContext를 통해 EJB 컴포넌트에 사용할 수 있게 되고, JNDI를 통해 다른 J2EE 컴포넌트에 사용할 수 있다.

 

컨테이너가 관리하는 트랜잭션 (즉 선언적 트랜잭션)은 컴포넌트의 배치 기술자 내에 있는 트랜잭션 속성에 따라 애플리케이션을 대신하여 컨테이너에 의해 투명하게 시작되고 끝난다. 여러분은 transaction-type 속성을 Container나 Bean으로 설정함으로서 EJB 컴포넌트가 빈이 관리하는 트랜잭션이나 컨테이너가 관리하는 트랜잭션 지원 중 어떤 것을 사용할지 지시할 수 있다.

 

컨테이너가 관리하는 트랜잭션으로 여러분은 EJB 클래스나 메쏘드 레벨에 트랜잭션 속성을 지정할 수 있다. 여러분은 EJB 클래스에 대한 기본 트랜잭션 속성 세트를 지정할 수 있고, 또한 다른 메쏘드들이 각기 다른 트랜잭션 의미론을 가지고 있어야 할 경우 각 메쏘드에 대해 속성을 지정할 수 있다. 이러한 트랜잭션 속성은 어셈블리 디스크립터의 컨테이너- 트랜잭션 섹션에 지정된다. 예제 어셈블리 디스크립터가 Listing 2에 나와 있다. trans-attribute에 대해 지원되는 값은 다음과 같다.

  • Supports
  • Required
  • RequiresNew
  • Mandatory
  • NotSupported
  • Never

trans-attribute는 메쏘드가 트랜잭션 내의 실행을 지원하는지, 트랜잭션에서 메쏘드가 호출되었을 때 컨테이너가 어떤 조치를 취해야 하는지, 그리고 트랜잭션 외부에서 호출되었을 때 컨테이너가 어떤 조치를 취해야 하는지를 결정한다. 컨테이너가 관리하는 트랜잭션 속성 중 가장 일반적인 것이 Required이다. Required가 설정되면 실행중인 트랜잭션은 그 트랜잭션에 여러분의 빈을 등록할 것이다. 그러나 실행 중인 트랜잭션이 없다면 컨테이너는 당신을 위해 한 트랜잭션을 구동할 것이다. 우리는 다양한 트랜잭션 속성들간의 차이와 언제 이들 각각을 사용해야 하는지를 이 시리즈의 Part 3에서 검토할 것이다.

 

Listing 2. 샘플 EJB 어셈블리 디스크립터
<assembly-descriptor>  ...  <container-transaction>    <method>      <ejb-name>MyBean</ejb-name>      <method-name>*</method-name>    </method>    <trans-attribute>Required</trans-attribute>  </container-transaction>  <container-transaction>    <method>      <ejb-name>MyBean</ejb-name>      <method-name>updateName</method-name>      </method>   <trans-attribute>RequiresNew</trans-attribute>  </container-transaction>  ...</assembly-descriptor>

 

강력하지만 위험한

 

Listing 1의 예제와는 달리 선언적 트랜잭션 구분에서는 컴포넌트 메쏘드에 트랜잭션 관리 코드가 없다. 이 점은 결과로 나오는 컴포넌트를 더 읽기 쉽게 하지만 (트랜잭션 관리 코드와 뒤섞여 있지 않기 때문에), 또다른 더 중요한 이점을 가지고 있는데, 컴포넌트의 트랜잭션 의미론이 컴포넌트의 소스 코드를 수정하지 않고 심지어는 여기에 접근하지 않고도 애플리케이션 어셈블리 시점에 변경될 수 있다는 점이다.

 

트랜잭션 구분을 코드와 별개로 지정할 수 있다는 것은 아주 강력한 기능이지만 어셈블리 시에 잘못된 결정을 하면 여러분 애플리케이션을 안정적이지 못하게 하거나 그 성능을 심각하게 해칠 수 있다. 컨테이너가 관리하는 트랜잭션을 올바로 구분하는 책임은 컴포넌트 개발자와 애플리케이션 어셈블러 사이에 공유되어 있다. 컴포넌트 개발자는 애플리케이션 배치자가 애플리케이션의 트랜잭션들을 어떻게 체계화할지에 대해 현명한 판단을 내릴 수 있도록 컴포넌트의 역할에 대해 충분한 문서를 제공해야 한다. 애플리케이션 어셈블러는 트랜잭션이 애플리케이션의 일관성을 지키고 성능을 해치지 않게 구분될 수 있도록 애플리케이션 내의 컴포넌트들이 어떻게 상호작용하는지를 이해해야 한다. 우리는 이 시리즈의 Part 3에서 이 문제들을 설명하겠다.

 

투명한 트랜잭션 전파

 

두 유형의 트랜잭션 모두에서 자원 할당은 투명하다. 컨테이너는 트랜잭션 과정 동안 사용되는 어떤 트랜잭션 자원이라도 현재의 트랜잭션에 자동으로 등록한다. 이 프로세스는 Listing 1에서 얻어지는 데이터베이스 연결과 같은 트랜잭션 메쏘드 뿐 아니라 이것이 호출하는 메쏘드, 심지어는 원격 메쏘드에 의해 사용되는 자원까지도 확장된다. 이것이 어떻게 일어나는지를 살펴보자.

컨테이너는 트랜잭션을 쓰레드와 연결시킨다.

 

가령 객체 A의 methodA()가 트랜잭션을 시작시키고, 객체 B의 methodB() 를 호출하여 JDBC에 연결되고 데이터베이스를 업데이트시킨다고 해 보자. B에 의해 얻어진 연결은 A가 생성한 트랜잭션에 자동으로 등록될 것이다. 컨테이너는 이것을 해야 할지 어떻게 알았을까?

 

트랜잭션이 초기화될 때 트랜잭션 문맥은 실행 쓰레드와 연결된다. A가 트랜잭션을 생성하면 A가 실행하고 있는 쓰레드가 트랜잭션에 연결된다. 로컬 메쏘드 호출은 호출자와 동일한 쓰레드에서 실행되기 때문에 A가 호출한 메쏘드 역시 그 트랜잭션의 문맥 내에 있을 것이다.

 

 

원격지의 스켈러튼 객체

 

객체 B가 실제로 다른 쓰레드, 심지어는 다른 JVM에서 수행되고 있는 EJB 컴포넌트에 대한 스텁이라면 어떠한가? 놀랍게도, 원격 객체 B가 접근하는 자원들이 여전히 현재의 트랜잭션에 등록되어 있다. EJB 객체 스텁 (호출자의 문맥에서 실행되는 부분), EJB 프로토콜 (IIOP사의 RMI) 및 원격지에 있는 스켈러튼 객체는 모두 힘을 합쳐 이것이 투명하게 발생되도록 한다. 스텁은 호출자가 트랜잭션을 수행하고 있는지를 결정한다. 수행하고 있다면 트랜잭션 ID, 즉 Xid가 메쏘드 매개변수와 함께 IIOP 호출의 일부로 원격 객체에 전해진다. (IIOP는 CORBA 원격 호출 프로토콜로써, 트랜잭션 문맥이나 보안 문맥과 같은 실행 문맥의 다양한 요소를 전달하기 위해 제공된다. (참고자료) 호출이 트랜잭션의 일부라면 원격 시스템에 있는 스켈러튼 객체가 원격 쓰레드의 트랜잭션 문맥을 자동으로 설정하여, 실제 원격 메쏘드가 호출되었을 때 이것이 이미 트랜잭션의 일부가 되어 있다. (스텁과 스켈러튼 객체는 또한 컨테이너가 관리하는 트랜잭션의 시작과 확약을 관리한다.)

 

트랜잭션은 EJB 컴포넌트, 서블릿, JSP 페이지 (혹은 컨테이너가 지원할 경우 애플리케이션 클라이언트)등 어떤 J2EE 컴포넌트에 의해서도 초기화될 수 있다. 이것은 요청이 있을 때 여러분 애플리케이션이 서블릿이나 JSP 페이지에서 트랜잭션을 시작시킬 수 있고 서블릿이나 JSP 페이지 내에서 일부 처리를 수행할 수 있고 여러 서버상의 엔터티 빈과 세션 빈에 페이지 로직의 일부로 접근할 수 있으며 이 모든 작업들이 투명하게 한 트랜잭션의 부분이 되도록 할 수 있음을 의미한다. 그림 1은 트랜잭션 문맥이 서블릿에서 EJB로의 실행 경로를 따라가는 방식을 보여준다.

 

그림 1. 한 트랜잭션 내의 여러 컴포넌트들
Multiple components in a single transaction

 

최적화

 

컨테이너가 트랜잭션을 관리하도록 하면 컨테이너는 우리를 위해 특정한 최적화 결정을 내릴 수 있다. 그림 1에서 우리는 하나의 서블릿과 여러 개의 EJB 컴포넌트가 한 트랜잭션의 문맥 내에서 데이터베이스에 접근하는 것을 볼 수 있다. 각각은 데이터베이스로의 Connection을 얻는다. 이것은 각각이 정확히 동일한 데이터베이스에 접근하고 있는 경우일 수 있다. JTS는 다른 컴포넌트들로부터 동일한 자원으로 여러 연결이 이루어진 경우라도 여러 자원이 트랜잭션에 관여하고 있는지 아닌지를 추적할 수 있고 트랜잭션의 실행을 최적화할 수 있다. Part 1의 내용을 회상해보면 여러 자원 관리자들을 하나의 트랜잭션에 참여시키려면 2단계 확약 프로토콜의 사용이 필요한데, 이것은 단일 자원 관리자가 사용하는 1단계 확약보다 훨씬 비싸다. JTS는 단 하나의 자원 관리자가 트랜잭션에 등록되어 있는지를 결정할 수 있다. 트랜잭션에 관여하고 있는 모든 자원들이 동일하다고 추적할 경우 2단계 확약을 건너 뛰고 자원 관리자가 스스로 트랜잭션을 처리하도록 한다. by itself.

 

결론

 

투명한 트랜잭션 제어, 자원 할당 및 트랜잭션 전파를 가능케 하는 마법은 JTS의 부분이 아니고, 대신 장막 뒤에서 J2EE 애플리케이션을 대신해 J2EE 컨테이너가 JTA와 JTS 서비스를 어떻게 사용하는지의 일부분이다. 이러한 마법이 투명하게 일어나도록 협력하는 수많은 엔터티들이 장막 뒤에 있다. EJB 스텁과 스켈러튼, 컨테이너 벤더가 제공하는 JDBC 드라이버 wrapper. 데이터베이스 벤더가 제공하는 JDBC 드라이버, JMS provider 및 JCA connecters들이 그것이다. 이 엔터티들 모두가 트랜잭션 관리자와 상호작용하기 때문에 여러분의 애플리케이션 코드가 이를 수행할 필요가 없다.

 

Part 3에서 우리는 J2EE 문맥 내에서 트랜잭션을 관리하는 것과 관련된 몇 가지 실질적인 사항들 - 트랜잭션 구분 및 격리-과 애플리케이션 영속성, 안정성 및 성능에 이것이 미치는 영향을 살펴보겠다.

 

참고자료

 

 

 

<출처 : IBM developerWorks>

원문보기 : http://www-903.ibm.com/developerworks/kr/java/library/j-jtp0410.html