Java Transaction Design Pattern by 오리대마왕

이 글은 대부분의 업무 응용 프로그램을 만들기 위해서 반드시 알아야 하는 개념인 트랜젝션(Transaction) 를 소개하며, Java에서의 트랜잭션 처리 개념과 대표적인 설계 패턴을 소개한다. 이 글은 Java 초보자를 대상으로 하지만, 적어도 기초적인 JDBC 활용법(데이터소스로부터 커넥션 얻어서 커밋하는 류의)에 대한 선수지식이 필요하다. 이 글에서 언급하는 내용은 내 머릿속에서 나온 것은 아니고, 다음의 2개의 글의 내용을 짜깁기 한 것이다.
  • J2EE Transaction Frameworks: Building the Framework: onjava.com 에 2001년에 소개된 아주 오래된 자료이다. Transaction의 정의, Transaction 처리의 유형 및 구분 등등의 여러 개념을 설명하고 있다.
  • Java Transaction Design Strategies: InfoQ 에 2006년에 소개된 소책자이다. PDF로 되어있으니 읽기도 좋다. 책 이름에서 알 수 있듯이 programmatic, declarative 등의 transaction model 과 함께 EJB와 Spring 으로 이러한 transaction model을 적용할 때 활용할 수 있는 3가지 design pattern을 소개한다. 실무적으로 알아두면 좋은 내용이 많으니 한번 읽어보는 것도 좋겠다. 참고로 이 책의 존재는 Spring Reference 를 읽다가 알게 되었다.


트랜잭션 개요

트랜잭션이란?

조대협님이 작성한 글 자바로 구현하는 트렌젝션 프로그래밍에서 따온다. 참고로 난 트"랜잭"션이라 하였는데 조대협님 원글에서는 트"렌젝"션이라 하셨네. 내용은 원문 그대로 옮긴다.

1. 트렌젝션이란 무엇인가? (What is transaction ?)
트렌젝션이란, 중단없이 시작에서부터 종료까지 한번에 수행되어야 하는 하나의 작업단위를 이야기한다. 수행이 끝난후에는 중간에 작업이 실패되었을 경우에는 작업 수행전의 상태로 그대로 돌아가야 한다.

이해를 돕기위해서 쉽게 예를 들어서 설명하도록 하자, A계좌에서 B계좌로 1,000원을 계좌 이체를 한다고 가정하자. 이 작업은 다음과 같은 순서로 이루어지게 된다.
1) A계좌에서 1,000원을 인출한다.
2) B 계좌에 1,000원을 더한다.

만약 위의 작업을 수행하던 도중 A계좌에서 1,000원이 인출된 후에, 은행 시스템의 오류로 인해서 B계좌에 1,000원이 더해지지 않는다면, 중간에 1,000원이라는 돈은 공중에서 증발해 버린것이 된다. 이럴때는 다시 계좌이체를 수행하기 이전의 상태로 되돌려서 A계좌에로부터 1,000원을 인출하지 말아야 한다.

그래서 위의 1),2) 작업은 한꺼번에 이루어져야 한다. 계좌이체 작업과 같이 한번에 이루어져야 하는 작업을 트렌젝션이라고 부른다.

트랜잭션 기본속성 - ACID

역시 위와 같은 조대협님 글에서 따온다.
2. 트렌젝션의 기본속성 ACID (Transaction attribute “ACID” )
트렌젝션은 크게 4가지 특성을 가지는데 Atomicity,Consistency,Isolation,Durability로, 이 네가지를 줄여서 ACID라고 부른다.
그럼 이제 부터 이 ACID속성 각각에 대해서 좀 더 상세하게 알아보도록 하자.

Atomicity (원자성)
Database modifications must follow an all or nothing.
원자성이란, 하나의 트렌젝션이 하나의 단위로만 처리가 되어야 한다는것이다. 하나의 트렌젝션안에 여러가지 step 의 트렌젝션이, 하나로 처리가 되어야한다. 위의 계좌 이체처럼, 계좌에서 돈을 빼고, 그 돈을 다른 계좌에 넣는 것과 같이 두개이상의 step으로 구성되어 있더라도, 계좌 이체라는 하나의 트렌젝션으로 처리가 된다.
그래서, 어느 step에서라도 트렌젝션이 실패가 되었을 경우에는 모든 상태가 트렌젝션 상태 전으로 rolled back되서, 이전 상태를 유지해야 한다.
즉 트렌젝션의 원자성은 트렌젝션이 완전히 수행되거나, 아무것도 수행되지 않은 All or Nothing의 이미를 가지게 된다.
Consistency (일관성)
states that only valid data will be written to the database
트렌젝션이 종료된후에 저장되는 데이타는 오류없는 데이타만 저장되어야 한다.
다시 풀어서 이야기하자면 계좌이체 과정에서, 인출은 되었는데, 다른 계좌로 돈이 안넘어갔다던지, 트렌젝션이 끝난후에, 잘못된 데이타 값으로 변경되었던지, 데이타베이스 constraint가 깨졌던지 했을때, Consistency가 잘못되었다고 이야기하고, 이런경우에 그 트렌젝션 내용을 저장하지 말고, 이전 상태로rollback되어야 한다.
Isolation (격리성)
Multiple transactions occurring at the same time not impact each other execution
격리성이란, 트렌젝션중에 의해서 변경된 내용이 트렌젝션이 완료되기전까지는 다른 트렌젝션에 영향을 주어서는 안된다는 이야기이다.
Tx A라는 트렌젝션 수행중 EMP 테이블의 값을 변경했을때, Tx A가 완료되지 않은 상태에서 Tx B가 EMP 테이블의 값을 참고할 경우에, Tx A에 의해 변경된 값을 참고하는것이 아니라(아직 TxA가 완료되지 않았기 때문에) 기존의 값을 참고하게 해야한다.
Durability (지속성)
Ensures that any transaction committed to the database will not be lost.
지속성이랑, 트렌젝션이 완료된후에의 값이 영구저장소에 제대로 기록이 되어야한다는 의미이다. 트렌젝션이 완료되었는데, 디스크 IO에러,네트워크 등으로 그 값이 제대로 기록이되지 않거나 해서는 안된다는 이야기다.

위에 언급된 트랜젝션의 4가지 특성 중 격리성(Isolation) 에 대해서도 파고들 구석이 무척 많다. 스프링 프레임웍의 경우 트렌젝션 설정에서 이를 정의할 수 있고, DBMS 의 메뉴얼에서도 이를 찾아볼 수 있다. 궁금하면, 자바지기 박재성님의 PPT 자료, Spring JDBC를 참고하자.

관리 범위에 따른 트랜잭션 구분 - 지역 vs. 광역 트랜잭션

하나의 트랜잭션이 관리하는 자원의 범위에 따라 지역 트랜잭션(local transaction) 과 광역 트랜잭션(global transaction) 으로 구분 할 수 있다. 지역 트랜잭션과 광역 트랜잭션을 설명하기 위해서는 자원 관리자(Resource Manager) 와 트랜잭션 관리자(Transaction Manager) 개념을 먼저 설명해야 한다. 이 두 관리자들을 묶어 트랜잭션 참여자(Transaction Participants)라고 한다. 놀새님의 글, Distributed Transaction Introduce을 참고하면 자세한 그림을 볼 수 있다. 여기선 트랜잭션 참여자 각각의 간단한 설명만 소개한다. 놀새님 글의 설명을 조금 다듬었다.
  • 자원 관리자: 트랜잭션 자원(RDBMS)에 대한 관리의 책임을 진다.
  • 트랜잭션 관리자: 트랜잭션의 begin, commit, rollback을 책임진다. 또한 분산 트랜잭션의 경우, 분산된 자원 관리자들에 대한 "Two phase commit" protocol에 대한 책임을 진다.
내 경우 광역 트랜잭션을 써 본 적이 없어, 트랜잭션 처리를 꽤 많이 해 왔음에도 저런 개념이 있다는 것을 몰랐다. 좀 더 찾아보니, 지역 트랜잭션의 경우 DBMS가 트랜잭션 관리자와 자원 관리자 두 역할을 혼자서 수행한다. 그러니 지역 트랜잭션 만 써 본 나는 "그냥 트랜잭션 처리는 DBMS가 알아서 하나보다" 하고 넘어갔지. 좀 더 들어가면 TP 모니터(Transaction processing monitor) 유형 등등의 토픽이 있는 것 같으나(^^;) 일단 여기서 발을 뺀다. 자, 그럼 이제 다시 본론인 지역, 광역 트랜잭션으로 돌아가보자.

  • 지역 트랜잭션은 하나의 지역 자원 관리자만이 관여하는 트랜잭션이다.
  • 분산 트랜잭션(Distributed Transaction) 혹은 광역 트랜잭션은 여러 시스템에 걸쳐 실행되는 트랜잭션으로, 트랜잭션 실행을 위해서는 광역 트랜잭션 관리 시스템과 트랜잭션에 관여하는 모든 시스템들의 지역 데이터 관리자 간의 조정(coordination) 이 필요하다.
광역 트랜잭션 관련해서는 2PC(2 Phase Commit) 등등 토픽이 더 있는데, 관련 글들이 많으니 일단 여기선 skip~! 궁금하면 놀새님 글과 J2EE Transaction Frameworks 을 읽어보자. 광역 트랜잭션을 위해선 JDBC 드라이버도 XADriver 등의 광역 트랜잭션 지원되는 드라이버로 바꿔야 하고, 트랜잭션 관리자가 필요하다. 많은 글에선 광역 트랜잭션은 왠만하면 피하라고 한다. 운영도 힘들고, 오류가 발생했을 경우 원인을 규명하기도 쉽지 않다. 내 경험으로는, 떨어진 DBMS 간에 트랜잭션을 묶을 필요가 있을 경우 그냥 Oracle 의 DB 링크를 걸어 마치 지역 트랜잭션인 것 처럼 대응했었다. 그러나 JMS 등의 아얘 다른 기술을 묶기 위해선 광역 트랜잭션으로 갈 수 밖에 없겠지.

트랜잭션 프로그래밍

두가지 트랜잭션 프로그래밍 모델

트랜잭션을 프로그램으로 옮기는 형태를 의미하는 프랜잭션 프로그래밍 모델은 프로그램적 트랜잭션 모델(programmatic transaction model) 과 선언적 트랜잭션 모델(declarative transaction model) 두 가지가 있다.

  • 프로그램적 트랜잭션 모델: 프로그램 코드 상에 트랜잭션을 구성하는 연산(Operation)의 묶음 혹은 절차를 상세히 기술한다.  가장 일반적인 방식은 쓰레드 상에 트랜잭션 처리를 위한 연산을 명시(marking)하는 것이다. 이에 따라 트랜잭션은 쓰레드를 umarking 하여 지연될 수 있고, 트랜잭션 컨텍스트를 지연 지점에서 재개 지점으로 전파하여 재개할 수 있다. (정확히 무엇을 의미하는 지 모르겠는데, 코드를 통해서 트랜잭션을 잠시 멈추거나 재개할 수 있다는 것으로 해석했다.) 커밋 요청은 트랜잭션에 참여하는 모든 자원 관리자에게 트랜잭션 내의 연산에 의한 변경을 영구적으로 저장할 것을 요청한다. 반대로 롤백 요청은 자원 관리자에게 트랜잭션 내 연산에 의한 변경을 취소할 것을 요청한다.
  • 선언적 트랜잭션 모델: MS 트랜잭션 서버에 의해 관리되는 COM 컴포넌트 또는 EJB 와 같은 컴포넌트 기반 트랜잭션 처리 시스템은 선언적 트랜잭션 모델을 지원한다. 컴포넌트는 배포 시점에 트랜잭션 대상으로 명시된다. 이 방식은 두가지를 시사하는데, 첫째로 트랜잭션을 명시할 책임이 어플리케이션에서 컴포넌트를 관리하는 컨테이너로 이동한다는 것을 의미한다. (따라서 컨테이너-관리 트랜잭션이라는 이름이 붙는다). 둘째로는 트랜잭션 명시 시점이 어플리케이션 빌드 시점에서 컴포넌트 배포 시점으로 지연된다는 것이다.
정의가 좀 어렵긴 한데, 간단히 자바 개발자 시점으로 생각하면 "getConnection 해서 try 로 묶고, catch exception 하면 commit, 아니면 rollback 하라" 는 걸 코드로 짠다면 프로그램적 트랜잭션 모델, "이 컴포넌트 내의 연산은 트랜잭션 처리되어야 하며, ~~ exception 이 난 경우엔 rollback 하여라" 를 xml 등으로 명시했다면 선언적 트랜잭션 모델이라고 구분하면 되겠다.

선언적 트랜잭션 모델의 경우, 트랜잭션 적용 대상이 되는 작업의 처리 특성을 설정을 통해 시스템에 알려야 한다. 여기서 처리 특성이라 함은 트랜잭션의 ACID 속성과 관련이 깊다. 어떤 작업은 반드시 별도의 트랜잭션으로 구분되어야 하고, 어떤 작업은 트랜잭션의 범위 외어야 하는 등등이다. JTA 는 6가지 트랜잭션 속성(Transaction Attritbute) 을 정의하고 있는데, Required, Mandatory, RrequiresNew, Supports, NotSupported, Never 가 그들이다. Spring Framework 의 경우 Propagation level 에서 이를 설정할 수 있다. 상세설명은 anyframe 의 [참고] Propagation Behavior, Isolation Level를 참고한다.

EJB를 많이 해 본 개발자라면 모르겠지만, 난 Java 개발자면서도 EJB 를 한~번도 해 본 적이 없기에 Spring Framework 을 써 보기 전에 선언적 트랜잭션 모델이라는 것이 있는지도 몰랐다. 이를 위해서는 위의 설명에도 나와 있듯이, 컨테이너가 꽤 많은 작업을 떠 맡아야 하는데 대부분 EJB Container 들이 이를 맡기 때문이겠지. Spring 을 쓴 다면 Spring Container가 이를 담당한다.

세가지 트랜잭션 모델

Java Transaction Design Strategy에서는 위의 프로그래밍 모델과 관리 범위를 섞은 3개의 트랜잭션 모델로 구현 형태를 구분한다. 지역 트랜잭션 모델(Local Transaction Model), 프로그램적 트랜잭션 모델(Programmatic Transaction Model), 선언적 트랜잭션 모델(Declarative Transaction Model) 이 그 3가지 이다. 프로그래밍 모델과 이름이 겹치는데, 앞으로 동일한 명칭이 나온다면 여기서 이야기하는 트랜잭션 모델로 이해하면 된다. 참고로, 트랜젝션 프로그래밍 모델에서의 프로그램적 트랜잭션 모델과 트랜잭션 모델에서의 프로그램적 트랜잭션 모델은 동일하지 않다. 아래에 소개된 지역 트랜잭션 모델과 프르그램적 트랜잭선 모두 트랜잭션 프로그래밍 모델 구분에서 프로그램적 트랜잭션 모델에 해당한다.

  • 지역 트랜잭션 모델: 프레임웍이 아닌 실제 데이터 소스를 제공하는 로컬 자원 관리자가 트랜잭션을 관리한다. DB의 경우,  자원 관리자는 DBMS 와 DB 드라이버를 통해 구현된다. JMS의 경우에는 특정 JMS 공급자가 구현한 큐 커넥션 팩토리가 자원 관리자에 해당한다. 지역 트랜잭션 모델에서는 개발자가 트랜잭션이 아닌 커넥션을 관리하고, 실제 트랜잭션을 관리하는 것은 자원 관리자이다.
  • 프로그램적 트랜잭션 모델: JTA(Java Transaction API) 와 트랜잭션 서비스 구현체를 통해 트랜잭션을 관리하며, 지역 트랜잭션의 약점을 보완한다. 프로그램적 트랜잭션 모델에서 프로그래머는 커넥션이 아닌 트랜잭션을 관리하는 코드를 작성한다. 일반적으로 프로그램적 트랜잭션 모델은 권장되지 않지만, EJB의 경우 클라이언트에 의해 트랜잭션이 시작되는 경우에는 유용하게 활용된다.
  • 선언적 트랜잭션 모델: EJB에서는 컨테이너 관리 트랜잭션(CMT;Container-Managed Transaction) 이라고도 하며, 프레임웍 혹은 컨테이너가 트랜잭션의 시작과 종료를 담당한다. 개발자는 XML 등으로 기술된 환경 인자를 통해 프레임웍에게 트랜잭션 처리 정보를 전달한다.
별다른 transaction framework 없이 개발했다면 아마 대부분 지역 트랜잭션 모델을 사용해 왔을 터이고, EJB 나 Spring Framework 등등을 사용했다면 아마도 선언적 트랜잭션 모델을 활용했을 것이다. 프로그램적 트랜잭션 모델은 좀 특별한 경우 정도에만 쓰인다고 보면 될 것 같다.

지역 트랜잭션 모델의 문제점은 아마 다들 잘 알고 있을 것이다. 일단 개발자가 오류를 범하기 쉽다. 제대로 connection 을 close 하지 않거나, autocommit 등을 해제하지 않는다던가 하는 문제를 개발자가 일일이 신경써야 한다. 또한 지역 트랜잭션 모델로는 광역 트랜잭션을 관리할 수 없다. 따라서 간단한 웹 어플리케이션 정도에 적합한 방법이다.

따라서 본격적인 트랜잭션 처리를 위해선 프로그램적 트랜잭션 모델 혹은 선언적 트랜잭션 모델로 넘어가는 것이 권장되는데, 이 중에서 선언적 트랜잭션 모델은 프로그램적 트랜잭션 모델에 비해 여러 장점이 있기 때문에 많은 경우 선언적 트랜잭션 모델이 권장된다. 선언적 트랜잭션은 개발자가 직접 트랜잭션을 시작하지 않아도 되고, 트랜잭션 속성이 빌드 시점이 아닌 배포 시점에 결정되기 때문에 재사용하기에도 잇점이 있다. 선언적 트랜잭션 모델 기반으로 작성된 코드를 한번 보면 왜 이 모델이 개발자 입장에서 좋은 지 쉽게 알 수 있을 것이다. 코드 예시는 Java Transaction Design Strategy 등에서 쉽게 찾아볼 수 있다.

트랜잭션 설계 패턴

Java Transaction Design Strategy은 제목에서 알 수 있듯이, 대표적인 3가지 트랜잭션 설계 패턴을 제시하고 있다. Client owner transaction design pattern, Domain service owner transaction design pattern, Server delegate owner transaction design pattern 이다. (한국어로 바꿔보려 했지만 난감하네.) 각 패턴은 "누가 트랜잭션에 대한 관리 책임을 가지고 있는가?" 에 의해 구분된다. 각각에 대한 간략한 설명은 다음과 같다.
  • Client owner transaction design pattern: Clinet Delegate 컴포넌트가 JTA 트랜잭션을 관리한다. 이 패턴은 웹 혹은 스윙 어플리케이션에서 EJB 와 원격 Stateless SessionBean 을 사용할 경우 많이 쓰이지만, 원격 어플리케이션이 아닌 경우에도 적용할 수 있다. 다수의 로컬 fine-grained 서비스 객체를 가진 어플리케이션에서 하나의 클라이언트 요청이 여러 서비스 객체들에 대한 다수의 요청을 통해 처리되는 경우이다.
  • Domain service owner transaction design pattern: 아마도 가장 널리 사용되는 모델일 것이다. 도메인 서비스 컴포넌트가 트랜잭션 처리를 관장한다. EJB 라면 도메인 서비스 컴포넌트를 Stateless SessionBean 로 구현하고, 선언적 트랜잭션 모델을 적용한다. Spring 이라면 POJO 형태의 도메인 서비스 컴포넌트와 선언적 트랜잭션 모델로 구현할 수 있다.
  • Server delegate owner transaction design pattern: 아키텍처가 Command 패턴 혹은 Server Delegate 설계 패턴을 사용할 경우 적용할 수 있다. 원격 요청에 대한 서버의 접근 지점인 Server Delegate 컴포넌트가 트랜잭션을 관장한다. 그 외의 다른 컴포넌트는 직접 트랜잭션을 관리하지 않는다.
하나 하나를 좀 더 자세히 살펴보자. 참고로 모두 Java Transaction Design Strategy 에 상세히 소개되어 있으니 이를 참고하자. 클래스다이어그램 등등을 따오고 싶으나 저작권 관계를 잘 몰라서 내용만 축약한다. (이것도 문제가 될까?) 책 원문에는 EJB 와 Spring Framework 로 이를 구현한 예제까지 제시되어 있어 쉽게 이해할 수 있을 것이다.

Client owner transaction design pattern

클라이언트가 트랜잭션을 관장하는 것 자체가 그다지 바람직하지는 않지만, 꼭 필요한 경우가 있을 경우 적용할 수 있는 패턴이다. 클라이언트가 트랜잭션을 관리하는 경우는 대부분 서버측 서비스가 매우 세분화 되어있고, 이를 통합해 주는 서비스가 제공되지 않는 상황에 해당한다. 여러 서버측 서비스에 걸친 요청을 트랜잭션 처리해야 할 경우 클라이언트가 이를 관리할 수 밖에 없다. 그러나 이는 유지보수 등 여러 문제를 불러일으킬 우려가 있다.

보통 서버가 관리한다면 트랜잭션의 범위가 서버 어플리케이션 내부이지만, 이 경우는 클라이언트에서 시작해서 서버 요청까지 범위가 확장되어야 한다. EJB 라면 클라이언트 쪽 구현은 반드시 프로그램적 트랜잭션 모델로, 서버 쪽 구현은 반드시 선언적 트랜잭션 모델로 구현해야 한다. 원격 서버일 경우 Spring Framework 는 원격 서버로의 트랜잭션 전파(Propagation) 을 지원하지 않기 때문에, RMI 등등의 protocol 을 사용해서 이를 구현해야 한다. 이는 Spring Framework reference 문서에도 명시되어 있다.
The Spring Framework does not support propagation of transaction contexts across remote calls, as do high-end application servers. If you need this feature, we recommend that you use EJB. However, consider carefully before using such a feature, because normally, one does not want transactions to span remote calls.


Domain service owner transaction design pattern

그냥 어지간한 경우는 모두 이 패턴이라고 볼 수 있다. Spring 구현을 보자면 트랜잭션 관리를 하는 Service POJO 들을 만들고, 이 Service 객체안에 조회, 생성, 수정 등등의 기능들을 구현한다. 필요에 따라 이들은 하나 이상의 DAO 등을 호출할 수 있지만, DAO 는 트랜잭션에 참여하지만, 트랜잭션 자체에 대한 내용을 알지 못한다. 클라이언트는 트랜잭션 처리를 하지 않는다. Spring 이나 EJB 모두 Service 객체에 선언적 트랜잭션 모델을 적용하여 패턴을 구현할 수 있다.

Server delegate owner transaction design pattern

위의 Domain service owner 패턴은 Service 컴포넌트가 어느정도 덩치가 있어 하나의 트랜잭션이 컴포넌트 범위를 벗어나지 않는 것을 가정하고 있다. 그러나 하나하나의 Service 컴포넌트가 세부 수준으로 작성되어 있어 복수의 Service 요청들을 묶어 트랜잭션으로 관리해야 하는 경우라면 이 Server delegate 패턴을 고려함 직 하다.

적용

우리나라의 일반적인 환경이라면, 대부분 Domain service owner transaction design pattern을 이미 활용하고 있을 것이다. 요즘은 많은 SI 프로젝트이 Spring framework 를 적용하고 있다고 하니, 선언적 트랜잭션 모델도 손쉽게 적용할 수 있을 것이다. 선언적 트랜잭션 모델을 써 보니, 예전의 connection 가져오고, try -catch 묶고, exception 발생하면 rollback 처리하던 형태로 돌아가지 못할 것 같다. 또한 국내 많은 SI 프로젝트에서 쓰는 iBatis 의 경우, Spring 의 iBatis ORM 지원 기능을 사용한다면 더더욱 쾌적하게 개발할 수 있을 것이다.

핑백

  • 오리대마왕님 집 : 2009년 내 이글루 결산 2010-01-04 23:28:07 #

    ... mp; 대표 글도서 (21회) / [독후감]조엘 온 소프트웨어를 넘어서IT (10회) / Java Transaction Design Pattern음악 (2회) / [공연감상]Mr.Big 내한공연 (2009/10/25)가장 많이 읽힌 글은 [독후감]공중그네 ... more

덧글

  • 제우스 2009/10/13 16:51 #

    한RSS에서 이글을 중요한 글로 표시해놨어요
    저 같은 '초짜!!!!' 에게는 중요한 글이네요 ㅋㅋ
  • 오리대마왕 2009/10/14 15:09 #

    허허 와이러십니까~ 다 아시면서!
  • 김성균 2009/10/15 21:34 #

    퍼가요~
  • 정대X 2012/05/20 12:12 # 삭제

    퍼갑니다!! 좋은자료 감사합니다.
※ 로그인 사용자만 덧글을 남길 수 있습니다.