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

2017년 7월 7일 금요일

Apache Kafka 에서 정확히 한 번(Exactly-once)이 가능한가?


  이 문서는 “Exactly-once Support in Apache Kafka” 블로그를 번역한 것입니다. 저자의 블로그에서는 정확히 한번(Exactly-once)의 개념을 설명하고 Kafka로 어떻게 이 개념을 이용할 수 있는지 설명합니다. 그러나 이 개념이 왜 어려운지 또 정말로 가능한지에 대해서는 좀더 성찰이 필요할 것 같습니다.

  우리는 목요일에 의미론적 보증을 극적으로 강화하는 Apache Kafka의 새 버전을 출시 했습니다.

  우리는 빠르고 실용적이며 정확한 방식으로 안정적인 스트림 처리를 수행하는 방법을 수년 간의 고민한 끝에 이 릴리스를 발표했습니다. 구현 노력 자체는 약 1 년 정도 걸렸습니다. 이 과정에서 Kafka 커뮤니티는 약  100 페이지의 세부 설계 문서가 논의했고, 비판도 받고, 광범위한 성능 테스트를 수행했고, 특히 정확이 한번(Exactly-once) 기능을 타깃으로 하는 살인적인 수천 라인의 분산 테스트를 수행했습니다.
 
  이 릴리스에 대한 반응은 대부분 "와우, 정말 멋지네"라는 것이 었습니다. 그러나 나는 불행히도 고전적인 실수를 했습니다. 나는 주석을 읽었습니다. 이것은 우리가 거짓말쟁이라고 주장하는 매우 혼란스러운 사람들의 흥분 섞인 주장들이었습니다.

다음은 반응들 중 일부입니다 .

  "정확히 한 번 전달(exactly-once delivery)는 될 수는 없습니다... 아주 간단한 수학적 정리로 인해 불가능합니다. 또한 저자가 혼란스러워서 독자가 모든 것을 불신하게 만드는 것은 의심의 여지가 있습니다. "

  "수학적으로 입증된 사실에 어떻게 대처할 지 재미있는 기사. 이 기사에서 주의 깊게 지정하지 않은 가정을 변경하지 않는다면 모든 경우에 작동할 수는 없으며 이는 시스템에 관한 것입니다. "

  나는 이 반응들이 틀렸다는 것을 믿습니다. 당신이 이것을 생각하는 사람들 중 하나라면, 저는 우리가 실제로 불가능하고 불가능한 것이 무엇인지, 그리고 Kafka에 무엇이 세워 졌는지를 실제로 들여다 보라고 요청합니다. 그리고 더 많은 정보에 입각한 의견을 얻으시길 바랍니다.

  이 부분을 두 부분으로 나누어 봅시다. 첫째, 정확히 한 번(exactly-once)은 이론적으로 불가능한가? 둘째, 어떻게 Kafka는 이 기능을 지원하는가?

정확히 한 번은 불가능한가요?


  정확히 한 번 전달/의미론이 수학적으로 불가능하다는 것의 정확한 이유를 모른 체 사람들의 확신해 찬 주장이 주변을 떠돌아 다니고 있습니다. 그러나 이것이 분명히 일반적인 지식임에도 불구하고 사람들은 이것이 어떤 증거인지 또는 정확히 한 번 의미가 무엇인지에 대한 정확한 정의에 연결되는 경우는 드뭅니다. 이것들은 FLP 결과 나 Two Generals Problem 와 같은 것들을 증거로 연결하지만 정확히 한 번(exactly-once)에 관한 것은 아닙니다. 분산 시스템에서 가능한 것(비동기, 반동기 등)을 제어하는 설정과 또 그것이 무엇인지를 정확하게 기술하지 않는다면 가능하거나 불가능한 것을 이야기 할 수 없습니다. 그리고 나쁜 일이 일어날 수 있는 것을 설명하는 결함 모델도 이야기 할 수 없습니다.

  그러면 우리가 성취하고자 하는 것과 같은 속성을 공식적으로 정의할 수 있는 방법은 있는 걸까요?

  네 있습니다. 그런 속성이 있다는 것이 밝혀졌습니다. 그것은 "원자적 브로드캐스트(Atomic Broadcast)" 또는 "총 주문 브로드캐스트(Total Order Broadcast)" 라고 합니다. 다음은 널리 사용되는 분산 시스템 교과서들 중 하나의 정의입니다.


  읽어보십시오. 내 생각에 이것은 pub/sub 메시징의 컨텍스트에서 사람들이 정확히 한 번 전달한다는 의미입니다. 즉, 메시지를 게시할 수 있으며 하나 이상의 수신 애플리케이션에 정확히 한 번 전달됩니다.

  그렇다면 원자적 브로드캐스트를 해결할 수 있을까요?

  해결할 수 있습니다. 제가 사진을 찍은 책 외에도 수십 개의 알고리즘을 비교 분류한 논문을 읽을 수 있습니다 .

  그러나 분산 시스템 책을 읽지 못하면 어떻게 이것이 사실이라는 것을 스스로에게 확신시킬 수 있습니까?

  원자적 브로드캐스트는 합의(consensus)와 동등한 것으로 드러났으므로 우리는 합의가 이루어 질 수 있는지 여부를 이해하는 것으로 문제를 좁힐 수 있습니다. 이는 아마도 합의가 분산 시스템에서 가장 많이 연구된 문제이기 때문입니다.

  합의가 가능한 것일까요? 아마 그렇다고 느낄 수 있습니다. 이는 Paxos 및 Raft 와 같은 잘 알려진 알고리즘에 의해 공격받는 문제이고 현대 분산 시스템 실행에 널리 의존하기 때문에 발생합니다. 그러나 이론적인 결과를 원한다면 설정 및 실패 모드에 대해 구체적으로 설명해야합니다. 예를 들어 의견에 있는 몇몇 사람들은 "하나의 잘못된 프로세스를 가진 합의의 불가능성"이라는 제목의 "FLP"논문을 인용했습니다. 그것은 좋아 보이지 않습니다! 첫 번째 문장에서 실패 감지기가 "충돌 오류가 있는 비동기 시스템에서 합의를 해결하는 데 사용될 수 있다"고 주장하는 논문을 쉽게 찾을 수 있습니다. 이것을 어떻게 이용할까요? 이것은 이론적으로 분산된 시스템 주장에서 세부 사항이 중요한 부분입니다.  우리는 설정과 결함 모델에 대해 구체적이어야 합니다. FLP 결과는 매우 제한적인 환경에서 합의가 가능하지 않다는 것을 증명합니다. 로컬 타이머 또는 무작위화와 같은 간단한 작업을 허용하면 가능해집니다. 합의 알고리즘은 이런 것들에 의존하여 일종의 시끄럽지만 결국에는 "올바른 시간 동안 하트 비트가 없는 프로세스가 죽었다"와 같은 올바른 오류 감지를 구현합니다. 이것들이 사람들이 "합의를 이끌어 낸다" 알고리즘을 말할 때 사람들이 참조하는 설정입니다.

  (FLP 외에도 많은 사람들이 Two Generals 문제를 "수학적 정리"와 연결시켰습니다. 그 이유는 실제로 전통적인 메시징 시스템의 유추를 볼 수는 있지만 실제로 Kafka는 그 문제와 별로 비슷하지 않습니다.)

  이것은 깊은 주제입니다. 관심이 있으시면 첫 걸음으로 Martin Kleppmann의 멋진 책을 추천 할 수 있습니다 . 진정으로 사로 잡힌 사람들은 참고 문헌들로 한 달 동안은 바쁘게 지낼 수 있을 것입니다.

  그렇다면 어떻게 이것을 실제로 실천할 수 있을까요? 실용적인 측면에서, 합의는 현대 분산 시스템 개발의 주류입니다. AWS에서 거의 모든 서비스를 사용하거나 AWS를 기반으로하는 서비스 위에 구축된 서비스를 사용했다면 합의로 구축된 시스템에 의존하게 됩니다. 이것은 현재 구축되는 시스템이 많지는 않지만 많은 경우에 해당됩니다. Kafka 는 이 중 하나이며, 그 중심적 추상은 분산된 일관된 로그이며, 사실상 가장 순수한 아날로그에서 다중 라운드로의 합의입니다. 따라서 합의가 가능하다고 믿지 않는다면 Kafka도 가능하다고 믿지 않습니다. 이 경우 Kafka에서 정확히 한 번 지원될 가능성에 대해 너무 걱정할 필요가 없습니다!

Kafka로 정확히 한 번 전달되는 애플리케이션을 어떻게 만들 수 있습니까?


  Kafka는 다음과 같은 로그를 갖습니다.

Apache Kafka의 로그

  Kafka의 로그는 강하게 정렬된 레코드 순서며 각 레코드에는 로그의 레코드 위치를 식별하는 순차적 숫자 오프셋이 지정됩니다.

  "생산자"는 이 로그에 레코드를 추가하고, 0 이상의 소비자 애플리케이션은 자신이 제어하는 ​​지정된 오프셋에서 메시지를 읽습니다.

  다음과 같은 애플리케이션을 상상해 봅시다.

  게시자가 메시지를 게시하려고 하고 소비자가 메시지를 읽고 이를 데이터베이스에서 저장하려고 합니다. 우리는 어떻게 이것을 할 수 있고 올바른 해결책을 얻을 수 있을까요?

  발생할 수 있는 두 가지 범주의 문제를 볼 수 있습니다.
  1. 첫 번째 문제는 게시자 애플리케이션이 로그에 메시지를 기록하지만 네트워크를 통해 확인 응답을 받지 못하면 발생합니다. 이것은 이 게시자를 묶어 놓을 것입니다. 이 때 메시지는 실제로 쓰기가 성공했거나 Kafka에 전혀 도착하지 않았을 수 있습니다. 우리는 모릅니다! 우리가 재시도하고 메시지 쓰기가 성공했다면 우리는 복제본을 가질 수 있습니다. 우리가 재시도하지 않고 쓰기가 성공하지 못했다면 우리는 메시지를 잃을 것입니다. 이는 기본 키가 없거나 자동 증가 기본 키가 없는 데이터베이스 테이블에 삽입했을 때와 실질적으로 동일한 딜레마입니다.
  2. 두 번째 문제는 소비자 측면에 있습니다. 소비자 애플리케이션은 로그에서 일부 메시지를 읽고 데이터베이스에 결과를 쓸 수 있지만 위치를 표시하는 오프셋을 업데이트하기 전에 실패할 수 있습니다. 해당 소비자가 다시 시작될 때 (잠재적으로 Kafka 그룹 관리 메커니즘을 사용하는 다른 컴퓨터에서 자동으로) 중복될 수 있습니다. 애플리케이션이 저장된 오프셋을 먼저 업데이트 한 다음 데이터베이스를 업데이트하는 경우, 실패로 인해 재시작시 업데이트가 누락 될 수 있습니다.
  두 문제에 대해 이야기 해 봅시다. 첫 번째 문제는 우리가 게시물에서 발표한 멱등성 지원에 의해 해결됩니다. 따라서 게시자는 중복 가능성에 대한 걱정 없이 성공할 때까지 항상 다시 시도할 수 있습니다 (Kafka는 투명하게 탐지하여 무시합니다).

 믿거나 말거나, 우리는 두 번째 문제에 대해서는 깊이 생각하지 않았습니다. 그러나 우리가 그것을 생각하지 않았기 때문이 아닙니다! Kafka를 알고 있는 사람들을 위해 이미 긴 블로그 게시물이 있기 때문에 우리는 더 깊이 뛰어 들지 않았습니다. 그래서 짧은 요약 설명을 했습니다.

  다음은 좀 더 깊이 있는 토론입니다.

  소비자가 정확히 한 번 처리하도록 하려면 생성된 파생 상태와 업스트림을 가리키는 오프셋을 동기화 상태로 유지해야 합니다. 여기서 중요한 사실은 소비자가 로그에서 오프셋을 제어할 수 있고 원하는 위치에 저장할 수 있다는 것입니다. Kafka 위에 정확하게 한 번 의미를 얻기 위해 이 방법을 사용하는 일반적인 두 가지 방법이 있습니다.
  1. 파생된 상태와 오프셋을 동일한 DB에 저장하고 트랜잭션에서 둘 다 업데이트하십시오. 다시 시작할 때 DB에서 현재 오프셋을 읽고 거기에서 읽기 시작하십시오.
  2. 모든 상태 업데이트와 오프셋을 멱등성(idempotent) 방식으로 작성하십시오. 예를 들어 파생 상태가 발생 횟수를 추적하는 키와 카운터인 경우 오프셋과 함께 카운터를 저장하고 오프셋 <= 현재 저장된 값으로 모든 업데이트를 무시합니다.
  좋습니다. 하지만 "어렵습니다!"라고 반대할 수도 있습니다. 실제로 그렇게 힘들지는 않다고 생각합니다. 그럼에도 저도 트랜잭션은 간단하지 않다고 생각합니다. 여러 테이블을 업데이트하는 경우 트랜잭션 문제가 발생합니다. 오프셋 테이블을 추가하고 이를 업데이트에 포함시키는 것은 로켓 과학이 아닙니다.

  이에 대해 제가 들었던 또 다른 반대는 실제로 "정확히 한 번"이 아니라 실제로는 "효과적 한 번(effectively once)"이라는 것입니다. 나는 (일반적으로는 덜 이해되지만) 이런 측면이 더 좋다고 동의합니다만, 우리는 여전히 정의되지 않은 용어의 정의에 대해 논쟁 중입니다. 우리가 전달과 관련해 잘 정의된 속성을 원한다면 나는 실제로 원자적 브로드캐스트(Atomic Broadcast)가 꽤 좋은 정의라고 생각합니다. 우리가 비공식적으로 말하면, 사람들은 의미에 대해 직관적인 생각을 갖기 때문에 "정확히 한 번"이라고 말하는 것이 좋습니다. (우리가 원자성 에 대한 지원을 발표했다더라도 혼란은 결코 더 적지 않았을 것입니다. ). 더 큰 비판은 사람들이 원하는 진정한 보증이 "정확히"도 "효과적"도 아니며 "한번" 또는 "전달"과 관련된 어떤 것이라는 것입니다. 사람들이 원하는 진정한 보증은 애플리케이션과의 통합에 대해 열심히 생각할 필요없이 오류가 발생했을 때 메시지를 철저히 정확하게 처리하는 것입니다.

  결국, 제가 설명한 해결책은 그다지 복잡하지는 않지만 여전히 애플리케이션의 의미에 대해 생각해야 합니다. 우리는 "애플리케이션에 마법의 요정 가루를 내 뿌릴 수 있습니까" 라는 제목의 블로그에서 이 문제를 다루려고 했습니다 (대답은 "아니오"였습니다).

  우리는 이것을 쉽게 할 수 있을까요? 우리는 할 수 있다고 생각합니다. 여기에서 기능 집합의 두 번째 부분인 트랜잭션이 등장합니다.

  실제로 위에서 제시한 예는 데이터 처리와 결과를 저장 시스템에 통합하는 다른 두 문제를 혼합한 것입니다. 이들은 서로 얽혀 있기 때문에, 개발자는 두 가지 방법을 함께 풀어 내기가 어렵습니다.

  이를 개선하기 위한 아이디어는 애플리케이션을 다른 두 부분으로 분류하는 것입니다. 이 두 부분은 하나 이상의 입력 스트림을 변환하는 (레코드 간에 결합되거나 측면 데이터와 결합될 수 있는) "스트림 처리" 부분과 이 데이터를 데이터 저장소로 전송하는 (동일한 애플리케이션 또는 프로세스에서 실행될 수 있지만 논리적으로는 구분된) 커넥터입니다.

  커넥터는 Kafka로부터 특정 데이터 시스템에 대한 트랜잭션 또는 멱등적 데이터 전달에 대한 추론을 필요로 합니다. 커넥터는 깊게 생각하고 오프셋을 관리해야 하지만 완전히 재사용할 수 있습니다. JDBC를 지원하는 모든 데이터베이스에서 JDBC 커넥터를 정확히 한 번은 제대로 작동하므로,  애플리케이션 개발자는 이를 고려할 필요가 없습니다. 우리는 이미 이런 것들을 가지고 있습니다, 그러므로 이 기능은 개발하기 보다 JDBC 커넥터를 다운로드합니다.

  어려운 부분은 데이터 스트림에서 범용 변환을 올바로 수행하는 것입니다. Kafka의 스트림 API와 함께 트랜잭션 지원도 필요합니다.

  Kafka 스트림 API 는 입력 스트림과 출력 스트림의 상단에 변환을 정의하는 매우 일반적인 API를 제공하는 생산자 및 소비자의 상위 계층입니다. 이 API를 사용하면 애플리케이션에서 수행 할 수 있는 거의 모든 작업을 수행 할 수 있습니다. 고전적인  메시징 시스템 API에 비해 더 강력하지는 않습니다.

  Kafka 스트림 애플리케이션:

  이 애플리케이션은 분산된 "단어 수"를 계산하는 하는 고전적인 빅데이터 예제입니다. 단어 수는 완전히 실시간이며 연속적입니다 (새 문서가 작성 될 때마다 카운트가 증가합니다).

 이 프로그램은 메인 메소드를 가진 보통의 자바 애플리케이션입니다. 이 애플리케이션은 보통의  애플리케이션들처럼 시작되고 배포됩니다. Kafka 소비자는 모든 인스턴스가 들어오는 데이터 스트림을 처리하도록 작동합니다.

  이 애플리케이션은 어떻게 정확성을 보장할 수 있습을까요? 결국엔 입력, 출력, 수신 메시지 전반의 집계 및 분산 처리 등 모든 복잡한 것들을 상상할 수 있습니다.

  그것에 대해 생각해보면 Kafka의 모든 스트림 처리는 다음 세 가지를 수행하고 있습니다.
  1. 입력 메시지 읽기
  2. 상태에 대한 업데이트를 생성(애플리케이션 인스턴스가 실패하고 다른 곳에서 복구되는 경우 내결함성이 필요하기 때문에)
  3. 출력 메시지 생성
  핵심 요구 사항은 이 세 가지가 항상 함께 발생하거나 전혀 발생하지 않도록 보장하는 것입니다. 상태가 업데이트되었지만 출력이 생성되지 않거나 그 반대로 오류가 발생하는 경우를 허용할 수 없습니다.

  우리는 어떻게 이것을 할 수 있을까요?

  우리는 수년에 걸쳐 이것에 관해 정말로 열심히 생각했고, 지금도 이것을 건설하고 있습니다. 기초 작업은 지난 몇 년 동안 한 번도 변경하지 않은 사항이었습니다.
  1. 0.8.1 릴리스에서 Kafka는 상태 변경을 위한 저널 및 스냅샷으로 사용할 수 있는 로그 압축을 추가했습니다. 즉, 임의의 로컬(디스크 또는 메모리 내) 데이터 구조에 대한 일련의 업데이트를 Kafka에 대한 일련의 기록으로 모델링할 수 있습니다. 이를 통해 로컬 연산의 내결함성을 만들 수 있었습니다.
  2. Kafka에서 데이터를 읽는 것은 오프셋을 증가시킵니다. 0.8.2 에서  오프셋 저장에 Kafka 자체를 사용하도록 오프셋 저장 메커니즘을 이동했습니다. 내부에서 오프셋 커밋(Commit)은 Kafka에 쓰여집니다. (소비자 클라이언트가 이 작업을 수행하므로 우리는 모를 수도 있습니다).
  3. Kafka로 데이터를 쓰는 것은 항상 Kafka에 쓰기였습니다.
  위 프로그램은 방금 추가한 (이 세 가지 작업을 투명하게 단일 트랜잭션으로 감싸는) 기능을 설정합니다.   이렇게 하면 읽기, 처리, 상태 업데이트 및 출력 모두가 함께 발생하거나 전혀 발생하지 않습니다.

  이 과정은 느리지 않을까요? 많은 사람들은 분산 트랜잭션이 본질적으로 매우 느리다고 가정합니다. 모든 단일 입력에 대해 트랜잭션을 수행할 필요는 없습니다. 이 경우 입력들을 함께 배치로 처리할 수 있습니다. 배치가 클수록 트랜잭션의 실제 오버헤드는 낮아집니다 (트랜잭션은 트랜잭션의 메시지 수와 관계 없이 일정한 비용을 가집니다). 블로그 포스트는 이것에 대한 성과 결과를 보여 주었고 이는 매우 유망한 성과였습니다.

  결과적으로 스트림 API를 사용하는 애플리케이션에 내 애플리케이션을 인수로 지정하고 출력 시스템과의 통합을 위해 정확히 한 번 커넥터를 사용하는 경우, 이제는 구성 변경만으로 종단 간 정확성을 얻을 수 있습니다.

  정말 멋진 점은 이 기능이 Java API에 전혀 묶여 있지 않다는 것입니다. Java API는 데이터 스트림의 연속적이고 상태를 유지하고 올바른 처리를 모델링하기 위한 범용 네트워크 프로토콜을 둘러싼 단순한 래퍼입니다. 모든 언어에서 이 프로토콜을 사용할 수 있습니다. 우리는 이 기능에  일종의 매우 강력한 클로저 속성을 추가함으로 변환을 수행하고 프로토콜을 구현하는 임의의 프로세스를 통해 입력 및 출력 토픽을 올바르게 연결한다고 생각합니다

정확히 한 번에 대해 생각해 보기로 돌아가기


  나는 회의적인 사람들에게 우리가 한 일을 이해하고 그것을 이해하도록 격려하기 위해 이 글을 썼습니다. 벤더로부터 오는 많은 헛소리들에 나는 회의적입니다. 그러나 나는 이 기능 세트가 정말 흥미롭고 LIAR을 모든 대문자로 외칠뿐 아니라 실제로 무엇을하는지,하지 않는지, 실제로 한계가 무엇인지 이해함으로써 이해가 훨씬 더 높아질 것이라고 생각합니다.

  업계에서는 올바른 결과를 얻을 수 없다는 것, 근본적으로 비효율적이고, 일괄처리 없이는 불완전하다는 것 등,  롤백되는 과정에 있는 스트림 처리와 관련하여 많은 가정이 있었습니다. 나는 정확히 한 번 처리하는 것이 불가능하다는 주변의 광범위하고 모호한 주장이 결국 양동이에 빠지게 된다고 생각합니다. 그것들은 나에게 일종의 분산 시스템인 broscience("나는 형제의 형제에게서 구글에서 일하는 형제가 정확히 한번이 CAP 이론에 위배된다는 말하는 것을 들었습니다”)를 생각나게 합니다. 나에게 진보는 일반적으로 실제로 가능하지 않은 것을 더 깊이 이해하고 문제를 재정의하여 우리를 앞으로 나아갈 실제적인 추상화를 구축하려는 시도로 이루어집니다.

  이런 것이 일어나는 좋은 예는 Spanner 와 CockroachDB 와 같은 시스템에서 수행되는 작업입니다. 이 작업은  가능한 범위 내에서 애플리케이션 개발자에게 유용한 기능을 제공하기 위해 많은 것을 수행합니다. 나는 NoSQL 분야에서 많은 경험을 갖고 있습니다. 이 시스템이 무엇을 하고 있는지는 대부분의 사람들이 불가능하고 비현실적인 조합이라고 생각하는 것으로 잘못 인식되었습니다. 나는 이것이 우리에게 교훈이되어야 한다고 생각한다. 애플리케이션을 구현하는 가난한 사람에게 모든 어려운 문제를 포기하고 구멍을 내기보다는 문제 공간을 재정의하여 올바른, 빠르며 가능한 대부분의 사용 가능한 시스템 기본 요소를 구축하는 방법을 이해해야 합니다.

원문 : Exactly-once Support in Apache Kafka



2014년 7월 2일 수요일

분산 비즈니스 트랜잭션과 오류 해결 방법


애플리케이션에서 우리가 일반적으로 트랜잭션이라 부르는 용어는 실제로 두 가지로 구분된다. 첫째, 시스템 트랜잭션(system transaction)[EAA]이다. 시스템 트랜잭션은 일반적으로 단일 데이터베이스 수준에서 처리되는 단일 트랜잭션을 말한다. 둘째, 비즈니스 트랜잭션(business transaction)[EAA]이 있다. 비즈니스 트랜잭션이란 하나 이상의 요청을 처리하기 위해 여러 시스템 트랜잭션을 포함한 트랜잭션을 말한다. 예를 들어 여행 예약을 진행하는 경우 항공 예약, 호텔 예약, 렌터카 예약, 기차 예약 등 여행하면서 타거나 머물거나 묵게 될 각 장소가 모두 예약돼야 완성되는 트랜잭션이 비즈니스 트랜잭션이다. 비즈니스 트랜잭션은 하나 이상의 요청과 일련의 시스템 트랜잭션을 포함하므로 완성되기까지 긴 시간이 필요하다.

일반적으로 기업은 위 두 트랜잭션과 더불어 분산 시스템 트랜잭션(distributed system transaction)과 분산 비즈니스 트랜잭션(distributed business transaction)이 필요한 경우가 있다. 우선, 분산 시스템 트랜잭션은 분산된 자원들이 트랜잭션 프로토콜에 따라 마치 단일 시스템 트랜잭션처럼 보이도록 하는 기술로 2단계 커밋(two-phase commit) 기술로 구현된다. 비즈니스 로직은 분산 트랜잭션을 시작한 애플리케이션에 집중된다. 반면, 분산 비즈니스 트랜잭션은 하나 이상의 요청에 분산된 비즈니스 애플리케이션들이 여럿 참여하고 참여한 애플리케이션이 일련의 시스템 트랜잭션들을 포함할 수 있는 트랜잭션을 말한다. 분산 비즈니스 트랜잭션은 시스템 트랜잭션이 하나 이상 여러 시스템에서 발생한다. 그러므로 분산 비즈니스 트랜잭션은 나머지 다른 모든 트랜잭션을 부분 집합으로 가질 수 있는 가장 일반적인 형태의 트랜잭션이다. 분산 비즈니스 트랜잭션은 비즈니스가 복잡한 만큼 다양한 형태로 등장할 수 있으므로 정형화된 아키텍처 패턴을 갖지 않는다. 분산 비즈니스 트랜잭션은 애플리케이션 수준에서 ACID를 보장하는 방안을 제공해야 한다.

트랜잭션들의 특성을 보면 다음과 같다.

구분
시스템 트랜잭션
비즈니스 트랜잭션
분산 시스템 트랜잭션
분산 비즈니스 트랜잭션
처리 시간
단시간(수초 이내) 장시간 단시간(수초 이내) 장시간
참여자
단일 로컬 시스템 단일 로컬 시스템 복수 분산 시스템 분산 복수 시스템
원자성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
영속성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
고립성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
무결성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
장애 조치
리두 로그,
롤백 세그먼트 등
오류 해결 전략 수작업 복구 오류 해결 전략
장애 지점
적음
(CPU, 메모리, 디스크)
적음
(CPU, 메모리, 디스크)
많음
(자원, 네트워크, 관리)
많음
(자원, 네트워크, 관리)

일반적으로 분산 환경은 로컬 환경에 비해 여러 가지로 불리하다. (시스템 커밋이 발생하는 위치를 기준으로 분산 시스템 트랜잭션과 분산 비즈니스 트랜잭션을 분산 트랜잭션이라 하고, 시스템 트랜잭션과 비즈니스 트랜잭션을 로컬 트랜잭션이라 하자.) 분산 트랜잭션은 참여 시스템들이 분산됨으로, 경우에 따라 처리 시간이 길어짐으로, 네트워크가 개입함으로, 시스템 관리 주체가 달라짐으로 약점들을 갖게 된다. 만약 분산 트랜잭션에 참여하는 시스템들이 고성능이고, 네트워크도 시스템 버스만큼 성능과 안정성이 보장되고, 업무 흐름이 처리 중에는 절대로 다운이나 정비로 참여 시스템들을 멈추게 하지 않는다면 분산 트랜잭션은 로컬 트랜잭션과 동일한 트랜잭션이 될 것이다. 그러나 현실의 분산 환경은 절대로 로컬 환경과 같을 수 없다. 그러므로 분산 트랜잭션과 로컬 트랜잭션을 동일하게 가정을 하는 것은 현실에 맞지 않는다.

로컬 트랜잭션과 분산 트랜잭션은 로컬 프로시저 호출(Local Procedure Call)과 원격 프로시저 호출(Remote Procedure Call)[EIP]과 많이 닮아 있다. 로컬 프로시저 호출과 원격 프로시저 호출도 단일 프로그램, 단일 시스템, 단일 메모리 공간에서 클라이언트와 서버 프로그램, 네트워크로 연결된 두 시스템으로 실질적으로 다른 인프라 구조를 갖는다. 마찬가지로 로컬 트랜잭션과 분산 트랜잭션도 실질적으로 다른 인프라 구조를 갖는다. 그러므로 로컬 프로시저 호출에서 원격 프로시저 호출로 넘어 갈 때 고려해야 할 수많은 문제들이 로컬 트랜잭션에서 분산 트랜잭션으로 넘어 갈 때도 그대로 등장한다.

기업에서 네트워크를 통해 분산된 애플리케이션이나 시스템(자원)을 결합해 하나의 비즈니스 흐름을 완성하는 애플리케이션 통합은 비일비재하다. 그런데 이 경우 비즈니스 흐름에 참여하는 각 요소들이 원격지에 존재한다는 사실이 쉽게 간과된다. 그리고 로컬 프로그래밍에 익숙한 개발자들은 원격지의 서비스나 자원도 가능하면 로컬 메소드나 자원을 활용하는 것처럼 사용하고 싶어하는 경향이 강하다. 이런 요구로 등장하는 것들이 동기 방식의 원격 프로시저 호출이나 2단계 커밋 같은 기술들이다. 이들은 개발자에게 원격지 서비스나 자원을 마치 같은 시스템의 자원처럼 활용하게 해준다. 이 기술은 언뜻 보기에는 사용하기 편리한 기술처럼 보이지만 비즈니스의 안정성을 위해 상당한 기술적 복잡성과 성능을 희생하게 된다. 즉 참여 시스템(자원)들은 서로 단단히 결합(tight coupling)된다. 그러나 경우에 따라 제품의 도입을 주도하는 이해관계로 인해 이런 사실은 잘 알려지지도 알리려고도 하지 않는다.

분산 시스템의 주요 취약점들에 대해 좀더 살펴보자. 분산 시스템들의 통신은 로컬 시스템 버스 통신이 아닌 이더넷과 같은 저수준 프로토콜을 이용한 원격 통신이다. 그런데 네트워크는 본질적으로 불안정하다. 물론 최근 네트워크 장비들은 일년에 몇 시간도 안 되는 다운 타임을 보장하기도 한다. 그러나 장애 확률은 얼마나 많은 시스템들이 업무 흐름에 참여했느냐에 따라 개별 시스템들의 장애 확률의 합으로 나타난다. 즉 참여 시스템이나 장비들이 많을수록 확률은 높아진다. 예를 들어 업무 흐름에 포함된 10개의 시스템이나 장비가 2시간/년의 장애 시간을 보장하더라도 전체적으로 보면 20시간/년의 장애 시간을 갖는 것이다. 시스템은 게다가 각 시스템의 관리 담당자도 별도로 운영될 가능성이 높다. 이 경우 각 시스템의 담당자는 필요에 따라 시스템을 중지시키거나 다른 작업을 실행시킴으로 비즈니스를 보장하기 위한 가용성이나 성능을 보장받지 못하게 될 수도 있다. 더 중요한 점은 장애 발생 시 장애를 찾거나 복구하는데 점검하고 대처해야 할 지점의 증가로 장애 회복 시간이 가늠 없이 길어 질 수 있다는 점이다. 이런 문제에 대해 시스템을 오래 운영해 본 운영자들은 대부분 동의할 수 있을 것이다. 그러므로 분산 시스템은 분명이 단일 시스템과 다르게 접근해야 한다.

분산 환경이 갖는 취약성으로 인해 분산 시스템 트랜잭션은 해결하기 곤란한 문제를 야기할 수 있다. 즉 처리 시간 동안 참여 시스템이나 자원 중 하나라도 문제가 발생하면 거래는 실패한다. 그러므로 분산 시스템 트랜잭션 기술인 2단계 커밋도 완전한 기술이 아니다.[DTT] 2단계 커밋 기술은 단일 자원만 사용하는 단일 시스템 트랜잭션과 본질적으로 다르다. 2단계 커밋이 안전하다고 생각하는 것은 잘못된 생각이다.

일반적으로 2단계 커밋은 다음과 같은 문제들을 갖는다.

    1) 투표와 커밋 단계 사이 장애로 2단계 커밋에 참여한 전체 시스템의 무결성 훼손이 발생할 수 있으며, 이 경우 무결성을 복원하기 위한 복잡한 수작업이 필요하다.

    2) 2단계 커밋을 지원하지 않는 기존 시스템들과 협업하기 어렵다.

    3) 하나 이상의 요청과 장시간이 걸리는 애플리케이션 통합에 적합하지 않다.

첫째, 통신 장애는 분산 기술에 내재된 속성이다. 네트워크는 신호는 본질적으로 불안정하다. 2단계 커밋의 조정자가 아무리 잘 트랜잭션을 관리하더라도 투표와 커밋 단계 사이에서 네트워크나 참여 자원의 장애가 발생하는 경우 무결성은 파괴된다. 2단계 커밋에서 장애가 발생하면 심각한 문제가 뒤따른다. 일단 2단계 커밋의 오류가 발생하면 애플리케이션은 참여 단계를 프로그램적으로 특정하기 어렵다. 그러므로 2단계 커밋에 참여한 모든 시스템의 상태를 확인해야 하고 일부 시스템의 완료된 커밋과 일부 시스템의 미완료된 커밋을 어떻게 할 것인가에 대해 비즈니스 담당자와 데이터베이스 관리자들과 애플리케이션 담당자들이 모두 합의하는 결론을 도출해야 한다. 참여 자원의 완료된 커밋 조합에 따라 대응 결론이 달라질 수도 있다. 그러므로 2단계 커밋을 마치 단일 시스템 트랜잭션처럼 간단히 생각하고 애플리케이션에 포함시키는 것은 해결할 수 없는 문제를 안고 있는 애플리케이션을 운영하는 것이다. 2단계 커밋을 사용하더라도, 분산 시스템들의 무결성은 정교한 비즈니스 시나리오를 통해 애플리케이션이 해결해야 한다. 둘째, 기업 내 2단계 커밋을 지원하는 시스템이 실제로 많지 않을 수 있다. 그러므로 2단계 커밋을 지원하지 않는 애플리케이션과 협업할 때는 2단계 커밋을 결국 사용하지 못하게 된다. 그러므로 이 경우도 역시 분산 무결성을 정교한 비즈니스 시나리오 분석을 통해 애플리케이션이 해결해야 한다. 셋째, 일반적으로 비즈니스 프로세스는 일련의 시스템 트랜잭션을 포함하는 장시간 업무 흐름을 통해 완성된다. 장시간 처리 비즈니스 프로세스도 2단계 커밋을 적용하면 자원의 무결성과 고립성 문제는 어느 정도 해결할 수 있으나 참여 애플리케이션들 간 성능이나 부하 차이로 인해 전체 성능에 지나친 병목을 발생시킬 수 있다. 이런 경우 2단계 커밋이 제공하는 장점보다 성능 병목으로 인한 단점이 더 커지게 된다. 그러므로 애플리케이션들이 협업하는 애플리케이션 통합에는 2단계 커밋을 적합하지 않을 수 있다.

그러므로 2단계 커밋을 사용해도 좋은 분야가 있고 그렇지 않은 분야가 있다는 것을 반드시 명심해야 한다. 사용의 판단 기준은 애플리케이션의 특성을 먼저 고려한 후 사용 여부를 판단해야 한다. 예를 들어 참여 시스템의 수, 참여 시스템의 성능, 참여 시스템의 2단계 커밋 지원 여부, 비즈니스 프로세스 완성에 필요한 시간, 참여 시스템의 안정성 등등을 고려해야 한다. 일반적으로 2단계 커밋은 비즈니스 애플리케이션 로직이 최소화된 성능과 안정성이 보장된 데이터베이스 시스템들을 하나의 트랜잭션으로 엮는 경우 잘 동작한다.

결국 2단계 커밋을 사용하더라도 비즈니스 트랜잭션의 단계 별 상태를 정교하게 분석해야 하고 일부 성공, 일부 실패에 대한 대응책도 준비해야 한다. 그런데 우리가 2단계 커밋을 사용하는 이유는 이런 무결성 문제를 회피하기 위해서인데 결국 일련의 시스템 트랜잭션들에 대한 성공, 실패 대응책을 고려해야 한다면 2단계 커밋을 사용할 이유가 있겠는가? 혹은 필자가 자주 발생하지도 않는 2단계 커밋의 오류를 너무 확대 해석한 것일 수도 있고, 오류가 발생한다면 무시해 버리면 될 것이 아닌가 반문할 수도 있을 것이다. 그러나 만약에 2단계 커밋이 단순히 극히 짧은 순간의 무결성 보장을 위해 사용된다면 굳이 2단계 커밋을 사용할 이유는 또 무엇인가? 그리고 무결성이 훼손되더라도 무시해 버린다면 더더군다나 2단계 커밋을 사용할 이유는 무엇인가?

2단계 커밋을 사용하지 않더라도 비즈니스 트랜잭션의 무결성과 고립성은 낙관적 오프라인 잠금 패턴(Optimistic Offline Lock pattern)[EAA]이나 비관적 오프라인 잠금 패턴(Pessimistic Offline Lock pattern)[EAA], 성긴 잠금 패턴(Coarse-Grained Lock pattern)[EAA], 암시적 잠금 패턴(Implicit Lock pattern)[EAA] 등을 통해 별도로 보장할 수 있다.

애플리케이션을 통합해야 경우 충분히 고려되지 않은 2단계 커밋을 바로 적용하기 보다 비즈니스 시나리오를 좀더 신중하게 분석해야 한다. 또한 각 참여 시스템의 트랜잭션 단계마다 오류 대응책도 마련해야 한다. 그럼 트랜잭션 오류는 어떻게 해결해야 할까? 애플리케이션 개발자들이 ACID를 보장하기 위해 동기 방식이면서 미들웨어에 의존하는 시스템 트랜잭션이나 2단계 커밋의 관점이 아닌, 비동기 메시징과 애플리케이션 상태 관점에서 비즈니스 트랜잭션 오류에 대한 대응책을 생각해 보자. 비동기 메시징에서는 본질적으로 전체 비즈니스 흐름에 트랜잭션을 적용하기가 사실상 불가능하므로 발생한 오류는 비즈니스적으로 해결한다. 비즈니스 트랜잭션에 발생한 오류를 해결하는 전략들은 다음과 같다.

    1) 취소 전략: 비즈니스 트랜잭션의 오류 해결책으로 가장 간단한 전략이다. 즉 지금까지 진행된 개별 트랜잭션들을 모두 취소시킨다. 어떻게 보면 이 전략은 2단계 커밋의 롤백 전략과 유사하다. 그리고 별로 좋은 전략 같아 보이지도 않는다. 그러나 현실 세계에서는 그런대로 쓸만하다. 취소함으로 발생하는 손실이 거래를 복구함으로 얻는 이익보다 큰 경우 특히 유효하다. 애플리케이션 개발 시 실행(삽입, 갱신) 기능과 취소(삭제, 갱신)기능을 함께 고려하고, 트랜잭션 조정자는 오류를 감지한 후 각 참여 자원의 취소 기능을 요청한다. 2단계 커밋의 롤백과 다른 점은 하나 이상의 요청이 트랜잭션 중간에 개입될 수 있고 긴 시간의 트랜잭션에서 발생한 오류를 해결하는 전략으로, 이런 문제는 2단계 커밋의 롤백으로는 해결되지 않는다.

    2) 재시도 전략: 재시도를 통해 비즈니스 트랜잭션이 성공할 수 있는 업무인 경우 다시 동일한 비즈니스 기능을 시작한다. 예를 들어 외부 자원이 일시적으로 중지된 경우 재시도를 통해 극복될 수 있다. 업무 규칙을 위반한 비즈니스 기능의 요청인 경우 이 전략을 사용하면 안된다. 참여 시스템이 멱등 수신자(Idempotent Receiver)[EIP]가 된다면, 이 전략은 좀더 효과를 발휘한다. 멱등 수신자란 동일한 요구에 대해 수신자의 상태가 변하지 않는 수신자를 말한다. 즉 재시도 전략을 비즈니스 트랜잭션 오류의 전략으로 선택한 경우 각 참여 자원을 멱등 수신자로 만들어야 한다. 멱등 수신자 패턴에 대한 자세한 설명은 [EIP]나 [SDP]를 참조한다. 재시도에는 재시도 횟수를 지정할 수 있다. 재시도 횟수를 넘어선 오류인 경우 다른 해결 전략으로 오류 해결 방법을 변경해야 한다.

    3) 보상 전략: 이미 처리된 비즈니스 오류를 별도의 비즈니스 기능으로 보상하는 전략이다. 예를 들어 은행이 고객의 이체 처리 중 출금은 성공했으나 입금에 실패한 경우 출금 액만큼 은행이 고객에게 입금해주는 전략이다. 이를 위해서는 트랜잭션 오류 상태를 해결 상태로 전이할 수 있는 기능(메소드)을 참여 애플리케이션에 포함해야 한다. 비즈니스 트랜잭션 오류가 발생하면 비즈니스 트랜잭션 조정자는 보상 비즈니스 기능을 참여 시스템에 요청해 비즈니스 트랜잭션의 오류 상태를 정상 상태로 전이시킨다.

비즈니스 트랜잭션을 설계할 때 위 전략들 고려해서 오류 대응책을 마련한다면, 트랜잭션 조정자 애플리케이션이나 트랜잭션 참여 서비스나 자원 애플리케이션들의 역할도 분명해 질 것이고, 2단계 커밋의 미해결 상태의 위험성도 자동적으로 배제할 수 있을 것이다.

분산 애플리케이션 통합의 경우 비즈니스 트랜잭션의 성격을 정확히 분석해 취소, 재시도, 보상 전략 중 해당 트랜잭션에 적합한 전략을 결정해야 한다. 그리고 나서 비즈니스 트랜잭션 조정자, 참여 시스템들에 필요 기능을 구현해야 한다. 이 과정은 생각보다 간단한 과정은 아니다. 비즈니스 트랜잭션의 오류 해결에 대한 명확한 요구를 현업이 정립해야 하고 이에 따라 아키텍트나 개발자들은 트랜잭션 상태 관리, 오류 분석, 전략 실행, 오류 보고, 재시도 제어 등 필요한 요소들을 설계 및 구현해야 한다. 이 때 "기업 애플리케이션 아키텍처 패턴"[EAA]이나 "기업 통합 패턴"[EIP]이나 서비스 디자인 패턴[SDP] 등 기업 애플리케이션을 위한 패턴들을 적절히 활용하고, 필요한 경우 메시징 시스템이나 웹 서비스 시스템을 이용하고, 적절한 프레임워크를 이용할 수도 있을 것이다. 그러므로 이 접근은 시간이나 자원을 좀더 소비한다. 반면 분산 시스템 트랜잭션(2단계 커밋)의 무책임한 사용은 설계 구현을 간단하게 해 시간이나 자원을 절약하게 할지는 몰라도 서비스 중 발생하는 분산 비즈니스 트랜잭션의 오류를 완전히 해결하지 못한다.

비즈니스 트랜잭션 중 무결성을 훼손하는 일부 트랜잭션 오류가 발생 했을 때, 위 세 전략을 언제 적용할지를 생각해 보자. 아마도 애플리케이션 개발자는 가능하면 즉시 해결되도록 전략을 가동시키려 할 것이다. 그러나 시스템이나 네트워크 장애가 길어지는 경우 해결 요청은 어떻게 유지될 수 있을 것인가? 장애가 한 시간가량 지속됐고 무결성이 훼손된 분산 비즈니스 트랜잭션이 10건이라면? 이 오류들을 해결하기 위해 트랜잭션 조정자가 동기 원격 프로시저 호출을 지속적으로 호출해도 참여 시스템이나 네트워크가 복구되지 않는 한 해결되지 못할 것이다. 그러므로 이 문제를 해결하는 가장 좋은 방법은 비동기 방식을 사용하는 것이다. 즉 오류 해결 요청 메시지를 메시징 시스템 큐에 추가하는 것이다. 그러면 참여 시스템은 메시지 큐에서 요청 메시지를 수신해 필요한 해결 기능을 수행할 수 있을 것이다. 그런데 비즈니스 트랜잭션의 오류 해결 기능이나 정상적인 비즈니스 트랜잭션의 기능 수행이나 애플리케이션에서 본다면 별로 다르지 않은 기능이다. 그러므로 정상 비즈니스 트랜잭션은 동기 원격 프로시저 호출 방식으로 오류 해결 비즈니스는 비동기 메시징으로 개발하는 것은 같은 기능을 두 번 전혀 다른 패러다임을 사용해 구현하는 모양새가 된다. 그러므로 둘 중 한 가지 방법을 선택해야 한다. 필자는 비동기 메시징 방식의 애플리케이션 통합을 권장한다.

"기업 통합 패턴(Enterprise Integration Patterns)"의 저자 그레거 호프는 "스타벅스는 2단계 커밋을 사용하지 않는다."[Hohpe]란 제목의 글을 블로그에 게시한 적이 있다. 필자가 언급한 분산 비즈니스 트랜잭션 오류 해결 전략도 그의 글에서 인용한 것이다. 그러므로 기업 애플리케이션 통합에 분산 비즈니스 트랜잭션이 포함될 때, 2단계 커밋을 사용하려는 경우 2단계 커밋이 꼭 필요한지, 2단계 커밋을 사용할 수 있는지, 2단계 커밋의 오류 시 해결할 대응 전략이 있는지, 애플리케이션 수준에서 트랜잭션을 해결할 수 있는지, 트랜잭션 오류에 대한 해결 전략은 완비됐는지 등을 충분히 고려해야 할 것이다.


참고 자료


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

참고 사이트)