david's daily developer note

[BE] Oracle Local & Distributed Transaction 본문

[Develop] Web/Back-end

[BE] Oracle Local & Distributed Transaction

mouse-david 2010. 8. 6. 16:57
728x90

데이터베이스 프로그래밍을 하다 보면 흔하게 마주치는 문제점 중 하나가 바로 트랜잭션 처리를 어떻게 할 것인가입니다. 혼자 개발을 할 때는 하고 싶은대로 해도 상관없겠지만, 보통 프로젝트를 수행하다보면 어떤 트랜잭션 프로그래밍 모델을 사용할 것인지를 결정을 하게 됩니다.

.NET 2.0에서는 이러한 프로그래밍 모델을 좀 더 편리하게 구현하기 위한 새로운 선택사항으로 System.Transactions라는 네임스페이스를 내놓았습니다. 이 글에서는 기존 트랜잭션 프로그래밍 모델들을 살펴보고, System.Transactions가 나오게된 배경을 알아본 후 본격적인 소개를 하도록 하겠습니다. 

이 글을 쓰는데 참고한 MSDN 문서는 다음과 같습니다.

Introducing System.Transactions in the .NET Framework 2.0

http://msdn.microsoft.com/library/en-us/dndotnet/html/introsystemtransact.asp


1. 기존 트랜잭션 프로그래밍 모델

.NET 1.x에서 우리가 선택할 수 있는 선택은 다음과 같습니다.

* 로컬 트랜잭션

  1) 저장 프로시저(Stored Procedure)에서 제어

  2) ADO.NET의 xxxTransaction 개체를 사용

* 분산 트랜잭션

  1) COM+ 수동 트랜잭션

  2) COM+ 자동 트랜잭션

로컬 트랜잭션(Local Transaction)은 DB 내부의 리소스 관리자(Resource Manager)에 의해 트랜잭션이 수행되는 것을 의미합니다. 이에 비해 분산 트랜잭션(Distributed Transaction)은 별도의 트랜잭션 관리자가 각 DB의 리소스 관리자에 통신을 하여 전체 트랜잭션을 수행하는 것입니다. 일반적으로 단일 DB에 대해서 작업할 경우에는 로컬 트랜잭션을 사용하며, 하나 이상의 DB에 대해서 트랜잭션을 걸어야 하는 경우에는 분산 트랜잭션을 사용합니다.

그러면 각 트랜잭션 모델들을 하나씩 살펴보도록 할까요?

1-1. 로컬 트랜잭션 : 저장 프로시저에서 제어

과거에 가장 많이 사용되던 방법 중 하나라고 볼 수 있습니다. 다음과 같이 저장 프로시저(이하 SP로 약칭) 내에서 BEGIN TRANSACTION을 호출하여 트랜잭션을 시작하고, COMMIT/ROLLBACK TRANSACTION에 의해 트랜잭션을 처리하게 됩니다.

CREATE PROCEDURE Proc1
…
AS
   -- 트랜잭션 시작
   BEGIN TRANSACTION
   -- 트랜잭션 작업 수행
   …   
   -- 오류 체크
   If @@Error <> 0
      -- 트랜잭션 Rollback
   ROLLBACK TRANSACTION
   …
   -- 트랜잭션 Commit
   COMMIT TRANSACTION

SP에서 트랜잭션을 처리할 때의 장점 중 하나는 무엇보다도 성능 상에서는 가장 빠르다는 것입니다. SP 호출 한번, 즉 단일 라운드 트립만으로 트랜잭션 처리가 가능하기 때문입니다.

단점은 무엇일까요? 간단히 잘라 말하면 '성능을 제외한 나머지 다'라고 할 수 있습니다. 워낙 많지만 주요한 몇가지만을 들어보도록 하죠. 아직도 트랜잭션 처리를 SP 에서 해야 한다고 주장하는 사람이 있으면 아래 내용을 보여주고 답변을 들어보시기 바랍니다.

첫째, SP에서 트랜잭션을 제어한다는 것은 비즈니스 로직이 SP 안에 담겨야 한다는 것을 의미합니다. 기본적으로 이는 아키텍쳐 상에서 2-Tier 구조를 강요하게 됩니다. 간혹가다 보면 이 SP를 호출하는 컴포넌트를 만들어놓고 이를 비즈니스 로직 컴포넌트라면서 자기네들은 3-Tier를 사용한다고 주장하는 사람들이 있습니다. 정확하게 말하자면 비즈니스 로직 컴포넌트가 아니라 SP를 호출하는 래퍼에 불과한건데 말이지요.

둘째, 2-Tier 구조가 강요된다는 것은 결과적으로 확장성에서 심각한 결점을 가지게 됩니다. 비즈니스 로직이 SP 내에 있다는 것은 전체적으로 부하 자체가 DB 서버에 집중된다는 것을 의미합니다. 사용자가 크지 않은 초기 상태에서는 상관없겠지만, 점점 사이트(혹은 시스템)의 규모가 커지게 되면서 DB 서버의 부하가 기하급수적으로 증가하여 성능이 저하됩니다. 보통 이런 사이트들을 가보면 웹 서버는 팽팽 놀고 있는데 DB 서버 쪽은 엄청난 CPU 점유율을 보이게 됩니다.

이 경우 문제를 해결하기 위해서는 결국 DB 서버를 확장하는 방법 밖에 없는데, 클러스터링을 하든 스케일 업(Scale-up : 서버의 CPU, 메모리와 같은 리소스를 추가 설치하거나 보다 성능이 좋은 새로운 박스로 교체를 하는 것을 의미)을 하든 비용면에서 비싼 댓가를 치를 수 밖에 없습니다. 점점 가면 갈수록 확장에 들어가는 비용에 비해 얻어지는 성능 향상도 줄어들게 됩니다.

셋째, SP의 프로그래밍 모델은 그다지 편리하지 않습니다. 모든 사람이 SP의 문법에 익숙해졌다고 가정하더라도, 예를 들어 오류가 발생해서 트랜잭션을 취소하기 전에 뭔가 세부적인 동작(로그 기록과 같은)을 해야 하는 경우, 깔끔하게 처리되지 않는다는 것이죠. try ~ catch를 사용하는 예외 처리 모델이 아닌 에러코드를 반환해서 호출자에서 이를 처리해야 하는 형태가 됩니다. 역시 디버깅 자체도 상당히 불편합니다.

넷째, 대부분 SP에 의존적인 사이트에서 많이 들리는 얘기 중 하나는 관리 문제입니다. 규모가 커질수록 SP를 관리하고, 재사용성을 확보하는 것이 쉽지가 않게 됩니다.

1-2. 로컬 트랜잭션 : ADO.NET 수동 트랜잭션

ADO.NET 수동 트랜잭션은 Connection 개체의 BeginTransaction()을 호출하여 Transaction 개체를 얻고, 작업을 수행한 후 Commit() 또는 Rollback()을 호출하여 트랜잭션을 처리하게 됩니다. 보통 다음과 같이 try~catch~finally를 조합해서 많이 작성하게 됩니다.

string connectionString = "..."; 
IDbConnection connection = new SqlConnection(connectionString); 
connection.Open(); 

IDbCommand command = new SqlCommand(); 
command.Connection = connection; 

IDbTransaction transaction; 
transaction = connection.BeginTransaction(); //Enlisting database 

command.Transaction = transaction; 
try { 
	// 작업 수행 
	... 
	transaction.Commit(); 
	// 트랜잭션 커밋 

} catch { 
	// 예외 처리 수행 
	(예: 로그 기록) 
	... 
	transaction.Rollback(); 
	//트랜잭션 취소 

} finally { 
	connection.Close(); 
}


SP를 사용할 때나 ADO.NET 트랜잭션을 쓸 때처럼, 트랜잭션의 시작과 끝을 명확하게 제어해주는 것을 명시적 트랜잭션(Explicit Transaction) 프로그래밍 모델이라고 부르기도 합니다.

이 모델은 우선 성능면에서는 SP에서 트랜잭션을 처리하는 것에 비해 떨어질 수 밖에 없습니다. SP 트랜잭션에 비해 명령을 수행하는 과정에서 더 많은 라운트 트립이 발생하고, DBMS와 ADO.NET 간에 Lock이 유지되기 때문입니다.

대신 성능을 제외하고는 SP를 사용할 때에 비해 모든 면이 낫습니다. 비즈니스 로직이 컴포넌트 내부로 들어오게 되므로 진정한 3-Tier 구조를 사용할 수 있고, 그에 따라 DB 서버의 부하가 줄어들어 확장성 면에서도 크게 개선이 됩니다. 프로그래밍 모델 자체도 SP에 비해 보다 나으며, 재사용성도 향상됩니다.

그러나 ADO.NET 수동 트랜잭션에서 생각해 봐야 할 점은 기본적으로 이 모델은 단일 개체-단일 리소스 트랜잭션에 적합하다는 것입니다.


무슨 말인가 하면.. 3-Tier 구조를 가정할 때, 프리젠테이션 개체와 비즈니스 로직 개체가 1:1로 대응되는 형태에 적합하다는 말이 됩니다. 즉, 프리젠테이션에서 비즈니스 로직 개체의 메서드 하나만을 호출하면 그 메서드 안에서 대부분의 작업이 다 이루어진다는 것을 가정하는 것입니다. 그런데, 다음과 같이 비즈니스 로직 개체가 다른 비즈니스 로직 개체를 호출하는 다중 개체-단일 리소스 트랜잭션의 경우에는 어떨까요?

이래저래 골치아픈 문제들이 생기기 시작합니다. 여러 개의 비즈니스 로직 개체가 단일 트랜잭션에 들어간다고 할 때, 어디서부터 트랜잭션을 시작해야 하며 나머지 개체들이 시작된 트랜잭션에 참여하려면 어떻게 해야 할까요? 트랜잭션이 종료되는 시점은 언제일까요? 그리고 만약 각 개체들이 개별적인 Connection 개체를 사용한다면?
이걸 좀 더 복잡하게 만들려면, 여기에다 만약 리소스(DB)도 여러 개를 사용한다면? (즉, 다음과 같이 다중 개체-다중 리소스 모델이라면..?)

이럴 경우에 필요한 것이 다음부터 나오는 분산 트랜잭션입니다.

1-3. 분산 트랜잭션 : COM+ (Enterprise Services)

위에서 언급했듯이 여러 개의 Connection을 사용하거나 여러 개의 DB를 사용할 경우에는 단일 리소스 관리자로 전체 트랜잭션을 처리하는 것이 불가능하며, 별도의 트랜잭션 관리자와 2 phase commit 프로토콜이 필요하게 됩니다.
Windows에는 DTC(Distributed Transaction Coordinater)라는 서비스가 트랜잭션 관리자의 역할을 합니다. msdtc.exe가 그것이며, 트랜잭션 프로토콜로는 OleTx(OLE 트랜잭션)을 사용하게 됩니다. 분산 트랜잭션을 사용하기 위해서는 일반적으로 COM+ 컴포넌트를 작성하게 되며, .NET에서는 Enterprise Service라고 불리게 됩니다.
Enterprise Service를 이용하려면, System.EnterpriseServices.ServicedComponent로부터 상속받도록 클래스를 작성하고, 여기에 필요한 attribute를 추가하면 됩니다. Enterprise Service 트랜잭션은 수동과 자동 트랜잭션이 존재합니다. 어느 경우든 트랜잭션의 시작은 Transaction 옵션이 Required로 지정된 클래스의 메서드가 호출되어 DB에 연결을 맺을 때부터입니다. 수동과 자동은 트랜잭션의 종료 처리에서 차이가 있는데, 수동 모델은 다음과 같이 ContextUtil의 SetComplete이나 SetAbort를 호출해줘야 합니다.

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
  public void TxMethod(...) {
    try {
      // 작업을 수행

      ContextUtil.SetComplete();
    }
    catch {
      ContextUtil.SetAbort();
    }
  }
}



자동 모델은 다음과 같이 메서드에 AutoComplete이란 attribute를 적용함으로써 명시적으로 SetComplete이나 SetAbort를 호출해주지 않아도 됩니다. 메서드 내부에서 예외가 발생하지 않으면 SetComplete이, 예외가 발생하면 SetAbort가 자동적으로 호출됩니다.

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
  [AutoComplete]
  public void TxMethod(...) {
     // 작업을 수행
  }
}



자동 모델은 위와 같이 트랜잭션의 시작뿐만 아니라 트랜잭션 커밋/롤백 여부에 대해서도 전혀 코드를 작성하지 않고 Attribute를 통해 선언하기만 하면 되므로, 선언적 트랜잭션(Declarative Transaction) 프로그래밍 모델이라고 불리기도 합니다.
아까 제기되었던 다중 개체-다중 리소스일 때의 문제는 어떻게 처리될까요? 위에서 언급했듯이 트랜잭션의 시작은 트랜잭션 옵션이 Required로 지정된 개체의 메서드가 최초 호출될 때부터라고 했습니다. 이 메서드 내에서 동일 개체 내의 다른 메서드, 또는 다른 개체의 메서드를 호출하면 자동적으로 이 트랜잭션에 편입되게 됩니다. 단, 개체(클래스)의 트랜잭션 옵션에 따라서 트랜잭션 동작과 편입 여부는 다소 달라지게 됩니다. ServicedComponent에는 다음과 같은 트랜잭션 옵션을 지정할 수 있습니다.
* Disabled - COM+ 트랜잭션을 사용하지 않음
* NotSupported - 트랜잭션에 참여하지 않음
* Supported - 기존에 트랜잭션이 존재하면 참여하고, 존재하지 않으면 참여하지 않는다.
* Required - 기존에 트랜잭션이 존재하면 참여하고, 존재하지 않으면 새 트랜잭션을 생성한다.
* RequiresNew - 기존 트랜잭션 존재여부에 관계없이 새로운 트랜잭션을 생성한다.

Enterprise Service를 사용하여 선언적 트랜잭션 프로그래밍 모델을 사용하면 이와 같이 트랜잭션의 범위 및 관리, 다중 개체, 다중 연결, 다중 DB 리소스 사용과 같은 문제점에서 완벽하게 해방이 될 수 있습니다. 프로그래머는 실제 로직에만 집중을 하면 되는 것이지요.

그러나, Enterprise Service가 만능은 아니며 다음과 같은 단점 역시 존재합니다.
첫째, Enterprise Service 트랜잭션은 심지어 단일 개체-단일 리소스를 사용하는 경우에도 분산 트랜잭션을 사용하게 됩니다. 이 때문에 다른 트랜잭션 처리 방법에 비해 상대적으로 성능이 낮습니다. 단일 로컬 트랜잭션 관리자가 처리하는게 아니라 DB의 리소스 관리자와 DTC 간에 통신이 필요하기 때문입니다. 이에 따라 DTC를 구동하는 서버(웹 또는 App)와 DB 서버 간에 DTC 프로토콜 통신이 가능하도록 보안 설정이 열려 있어야 합니다.
둘째, ServicedComponent로부터 상속을 받아야 한다는 제한이 문제가 됩니다. 이는 일반적으로 애플리케이션 모델링에서의 유연성을 떨어뜨리게 됩니다.
셋째, Enterprise Service를 쓸 경우, 개발자들이 COM+의 개념, 특성, 호스팅 모델에 대해서 이해하지 못하면 문제가 생길 소지가 있습니다. 예를 들어, 라이브러리 활성화/서버 활성화 간의 차이나 무상태 컴포넌트(Stateless Component)와 같은 것이 대표적인 예입니다.
넷째, Enterprise Service 트랜잭션은 항상 thread-safe이므로 동일 트랜잭션에 다중 쓰레드가 참여할 방법이 없습니다. 어찌보면 멀티쓰레드 환경에서 동기화 문제를 간단하게 만들기도 하지만, 이는 제한사항일 수도 있습니다.
다섯째, ServicedComponent는 COM+ 카탈로그에 등록되는 과정이 필요합니다. 등록을 하기 위해 치러야 하는 오버헤드도 문제지만, 나중에 카탈로그에 대한 관리부하 역시 만만치 않다는 것입니다.


1-4. 무엇을 쓸 것인가?

지금까지 .NET 1.x에서 선택 가능한 트랜잭션 방법들에 대해서 살펴보았습니다. 크게 정리해보자면 1.x에서는 다음과 같은 조합 중 하나만을 선택가능했습니다.

* 로컬 트랜잭션 + 명시적 트랜잭션 프로그래밍 모델
* 분산 트랜잭션 + 선언적 트랜잭션 프로그래밍 모델 (+ [COM+] + [DTC])

저희 회사의 경우에는 분산트랜잭션 + 선언적 프로그래밍 모델을 상당히 선호하는 편이었습니다. 왜냐하면 저 개인적으로도 분산 애플리케이션 아키텍쳐에서 가장 바람직한 것은 그 무엇보다도 유연성과 재사용성이라고 생각하기 때문입니다. 성능은 Scale-out을 통해 얼마든지 얻을 수 있다면 무시해도 되고, COM+에 대한 두려움은 개발자를 이해시키면 되고, 카탈로그 등록 오버헤드나 관리 문제는 무시해도 될만한 것이었기 때문입니다. 선언적 프로그래밍 모델의 장점이 이러한 단점들을 다 커버하고도 남는다고 본 것이죠.

여기서 이후에 소개할 System.Transactions의 개념이 시작됩니다. 위 조합과 장단점들을 떠올리면서 생각을 해보면 다음과 같은 조합도 가능했으면 좋겠다고 원하게 되었다는 것이죠.

* 로컬 트랜잭션 + 선언적 트랜잭션 프로그래밍 모델
* 분산 트랜잭션 + 명시적 트랜잭션 프로그래밍 모델

Reference
안재우님의 System.Transactions 소개

728x90