본문 바로가기

Dev.../플밍 관련 자료

[펌] JTS 이해하기 - 트랜잭션에 대한 소개

자바 트랜잭션 서비스는 J2EE 아키텍처의 핵심 요소이다. 자바 트랜잭션 API와 함께 자바 트랜잭션 서비스는 모든 종류의 시스템 및 네트워크 장애에 대해 강력한 분산 애플리케이션을 구축하도록 해준다. 트랜잭션은 신뢰성 있는 애플리케이션을 구축하기 위한 기초적인 구성 요소이다. 트랜잭션적인 지원 없이 신뢰성 있는 분산 애플리케이션을 작성하는 것이 터무니없이 어려운 일이다. 다행히도 JTS는 자신의 대부분의 작업을 개발자에게 투명하게 수행한다.; J2EE 컨테이너는 트랜잭션 구분과 자원 사용을 거의 보이지 않게 수행한다. 이번 3회로 구성된 연재물 중 첫 회에서는 트랜잭션이 무엇이고 왜 이들이 신뢰성 있는 분산 애플리케이션 구축에 필수적인지에 대한 기초적인 사항을 다루도록 하겠다.

 

J2EE에 대한 어떤 소개글이나 입문서를 보더라도 자바 트랜잭션 서비스 (JTS)나 자바 트랜잭션 API (JTA)에 대해서는 작은 부분만 할당되어 있음을 발견할 것이다. 이것은 JTS가 중요하지 않거나 J2EE의 선택적 부분이기 때문은 아니다. 사실은 그 반대라 할 수 있다. JTS는 EJB 기술보다 주목을 덜 받았는데, 이는 JTS가 애플리케이션에 제공하는 서비스가 대부분 투명하기 때문이다. 많은 개발자들은 자신의 애플리케이션에서 어디에서 트랜잭션이 시작되고 끝나는지도 알지 못한다. JTS가 알려져 있지 않은 것은 어떤 면에서 보면 그 성공 때문이다.; 트랜잭션 관리의 상세 사항을 너무나 효과적으로 숨기기 때문에 우리는 여기에 대해 많이 듣거나 말하지 않는 것이다. 그러나 여러분은 분명히 JTS가 여러분을 대신해서 무대 뒤에서 무슨 일을 하고 있는지 알고 싶을 것이다.

 

트랜잭션이 없으면 신뢰성 있는 분산 애플리케이션을 작성하는 것이 거의 불가능하다고 말해도 과장은 아닐 것이다. 트랜잭션은 애플리케이션의 영속적인 상태를 통제된 방식으로 수정할 수 있도록 하기 때문에, 우리 애플리케이션은 시스템 파손, 네트워크 장애, 전원 장애, 심지어는 자연 재해 등 모든 종류의 시스템 장애에 대해 강력해질 수 있다. 트랜잭션은 내결함성을 갖추고 신뢰성과 가용성이 높은 애플리케이션을 구축하는데 필요한 기초적인 구성 요소 중 하나이다.

 

 

트랜잭션을 사용하는 동기

 

한 계좌에서 다른 계좌로 돈을 이체한다고 상상해 보자. 각 계좌의 잔액은 데이터베이스 테이블의 행으로 표시된다. A 계좌에서 B 계좌로 돈을 이체하고 싶으면 아마도 다음과 같은 SQL 코드를 실행시킬 것이다.:

SELECT accountBalance INTO aBalance     FROM Accounts WHERE accountId=aId;IF (aBalance >= transferAmount) THEN     UPDATE Accounts         SET accountBalance = accountBalance - transferAmount        WHERE accountId = aId;    UPDATE Accounts         SET accountBalance = accountBalance + transferAmount        WHERE accountId = bId;    INSERT INTO AccountJournal (accountId, amount)        VALUES (aId, -transferAmount);    INSERT INTO AccountJournal (accountId, amount)        VALUES (bId, transferAmount);ELSE    FAIL "Insufficient funds in account";END IF

 

지금까지는 이 코드가 매우 직접적으로 보인다. A가 충분한 잔액을 가지고 있다면 한 계좌에서 돈이 빠져 나와 다른 계좌로 더해질 것이다. 그러나 시스템이 파손되거나 전원이 나간 경우에는 어떻게 될까? A 계좌와 B 계좌를 나타내는 행이 동일한 디스크 블록에 저장되지 않을 것이고, 이것은 이체를 완료하는데 한 개 이상의 디스크 입출력이 필요할 것이라는 뜻이다. 첫번째 것은 작성되었는데 두번째 것을 작성하기 전에 시스템 장애가 발생한다면 어떻게 될까? 그러면 돈이 A 계좌에서는 떠났는데 B 계좌에는 나타나지 않을 것이고 (A와 B 모두 이것을 좋아하지 않을 것이다), 혹은 돈이 B의 계좌에 나타난다 하더라도 A 계좌에서 빠져 나가지 않았을 수도 있다. (이것은 은행이 좋아하지 않을 것이다) 또는 계좌들은 제대로 업데이트되었는데, 계좌 분석 현황에는 업데이트가 이루어지지 않았다면 어떻게 될까 ? A와 B의 월별 계정 통지 작업이 계좌 잔고와 일치하지 않을 것이다.

 

여러 개의 데이터 블록을 디스크에 동시에 작성하는 것이 불가능할 뿐 아니라, 데이터의 일부가 변경되었을 때 모든 데이터 블록을 디스크에 작성하는 것도 시스템 성능에 좋지 않다. 디스크 작성을 보다 적절한 때로 미루면 애플리케이션 처리량을 크게 향상시킬 수 있지만, 데이터 무결성을 해치지 않도록 수행되어야 한다.

 

시스템 장애가 없는 경우라도 위의 코드에서 논의할만한 다른 위험이 있다. 바로 동시 처리 (concurrency)이다. A가 계좌에 $100를 가지고 있는데 두 개의 다른 계좌로 $100씩을 정확히 동시에 보냈다고 하면 어떻게 될까? 타이밍이 맞으면 적절한 잠금 메커니즘이 없을 경우 두 이체가 모두 성공할 수 있다. A의 잔고는 마이너스가 될 것이다.

 

이 시나리오들은 아주 그럴 듯해 보이고, 기업 데이터 시스템이 이들에 대해 대처해 줄 것을 기대하는 것이 당연하다. 우리는 은행이 화재, 홍수, 정전, 디스크 장애 및 시스템 장애 등의 상황에서 계좌 기록을 정확히 유지해 줄 것을 기대한다. 내결함성은 중복 (redundancy) - 중복 디스크, 중복 컴퓨터, 심지어는 데이터 센터까지 중복시킴-에 의해 제공될 수 있지만, 내결함성을 갖춘 소프트웨어 애플리케이션을 실질적으로 구축하도록 하는 것은 트랜잭션이다. 트랜잭션은 시스템이나 컴포넌트 장애시 데이터 일관성과 무결성을 지키기 위한 프레임워크를 제공한다.

 

트랜잭션이란 무엇인가?

 

그렇다면 과연 트랜잭션은 무엇인가? 이 용어를 정의하기 전에 먼저 애플리케이션 상태 (application state)라는 개념을 정의해 보자. 애플리케이션의 상태는 애플리케이션의 동작에 영향을 미치는 메모리 내, 그리고 디스크 상의 모든 데이터 아이템 -- 애플리케이션이 "알고 있는" 모든 것을 포함한다. 애플리케이션 상태는 메모리에 저장될 수도 있고 파일, 혹은 데이터베이스에 저장될 수도 있다. 시스템 장애가 발생했을 경우, 예를 들어 애플리케이션, 네트워크, 혹은 컴퓨터 시스템이 파손되었을 경우, 우리는 시스템이 재구동되었을 때 애플리케이션의 상태도 복구될 수 있음을 보증하고 싶다.

 

이제 우리는 트랜잭션을 애플리케이션 상태에 대한 관련 작업 모음으로 정의할 수 있는데, 원자성, 일관성, 격리성 및 영속성이라는 특성을 가진다. 이 특성들은 집합적으로 ACID 특성이라고 불린다.

원자성은 트랜잭션의 모든 작업이 애플리케이션 상태에 적용되든가 아니면 아무 것도 작용되지 않는다는 의미이다.; 트랜잭션은 더 이상 나누어지지 않는 작업 단위이다.

 

일관성은 트랜잭션이 애플리케이션 상태의 정확한 변형을 나타낸다는 뜻이다. 애플리케이션에 내포된 어떤 무결성 제약도 트랜잭션에 의해 침해되지 않는다. 실제로 일관성 개념은 애플리케이션마다 독자적인 것이다. 예를 들어, 회계 애플리케이션에서 일관성은 모든 자산 계정의 합은 모든 부채 계정의 합과 같다는 불변값을 포함할 것이다. 우리는 이 연재물의 3편에서 트랜잭션 구분을 논의할 때 이 요건에 대해 다시 살펴볼 것이다.

 

격리성은 한 트랜잭션의 결과가 동시에 실행되고 있는 다른 트랜잭션에 영향을 미치지 않는다는 뜻이다.; 트랜잭션 측면에서 보면, 트랜잭션은 병렬식보다는 순차적으로 실행되는 것으로 보인다. 데이터베이스 시스템에서 격리성은 보통 잠금 메커니즘을 사용해 구현된다. 격리성 요건은 때때로 애플리케이션 성능을 높이기 위해 특정 트랜잭션에 대해 완화되기도 한다.

 

영속성은 일단 한 트랜잭션이 성공적으로 완료되면 애플리케이션 상태에 대한 변경이 장애에도 삼아 남을 것이라는 뜻이다.

 

"장애에도 살아 남는다"는 것은 무슨 뜻일까? 장애에도 살아 남으려면 무엇으로 구성되어야 할까? 이것은 시스템에 달려 있고, 잘 설계된 시스템은 회복될 수 있는 장애를 명확하게 규정할 것이다. 내 데스크탑 워크스테이션에서 운영되는 트랜잭션 데이터베이스는 시스템 파손과 전원 장애에 대해서는 강하지만, 사무실 건물이 불타 버리는데는 강하지 않다. 은행은 자사 데이터 센터에 디스크, 네트워크 및 시스템을 중복하여 둘 뿐 아니라 아마도 다른 도시에 중복되는 데이터 센터를 만들어 중복 통신 링크로 연결시켜 자연 재해와 같은 심각한 장애로부터의 복구를 꾀할 것이다. 군용 데이터 시스템은 보다 엄격한 내결함성 요건을 가질 것이다.

 

트랜잭션 해부

 

일반적인 트랜잭션은 여러 참가자 -- 애플리케이션, 트랜잭션 처리 모니터 (TPM) 및 하나 이상의 자원 관리자 (RM)를 가지고 있다. RM은 애플리케이션 상태를 저장하는데, 대부분 데이터베이스이지만 메시지 큐 서버 (J2EE 애플리케이션에서는 JMS provider가 여기에 해당될 것이다)가 될 수도 있고 혹은 다른 트랜잭션 자원이 될 수도 있다. TPM은 트랜잭션의 "모두 처리하든지 아니면 아무것도 처리하지 않든지"라는 특성을 지키기 위해 RM들의 활동을 조율한다.

 

애플리케이션이 컨테이너나 트랜잭션 모니터에게 새로운 트랜잭션을 시작시키도록 요구하는 순간 트랜잭션은 시작된다. 애플리케이션이 다양한 RM들에 접근할 때 RM들은 트랜잭션 내에 등록된다. RM은 애플리케이션 상태에 대한 모든 변경 사항을 그 변경을 요청하는 트랜잭션과 결합시켜야 한다.

 

트랜잭션은 다음 둘 중 하나가 발생했을 때 종료된다. : 트랜잭션이 애플리케이션에 의해 확약 (commit)되거나 트랜잭션이 애플리케이션 혹은 RM 중 하나가 실패하여 롤백(roll back)될 때가 그것이다. 트랜잭션이 성공적으로 확약되면 그 트랜잭션과 관련된 변경 사항이 영속적인 기억 장치에 작성될 것이고 새로운 트랜잭션이 이를 볼 수 있다. 트랜잭션이 롤백되면 그 트랜잭션이 수행한 모든 변경 사항은 버려질 것이다. ; 즉, 트랜잭션이 일어나지 않은 것처럼 될 것이다.

 

트랜잭션 로그 - 영속성의 핵심

 

트랜잭션성 RM은 여러 트랜잭션의 결과를 하나의 트랜잭션 로그에 요약함으로써 적절한 성능으로 영속성을 성취한다. 트랜잭션 로그는 순차적인 디스크 파일로 (때로는 하나의 원시 파티션에) 저장되고 롤백이나 복구시를 제외하고는 보통 읽기는 안되고 쓰기만 가능할 것이다. 우리의 은행 계좌 예제에서, A와 B 계좌와 관련된 잔액은 메모리에 업데이트될 것이고, 차액은 트랜잭션 로그에 작성될 것이다. 업데이트 기록을 트랜잭션 로그에 작성하면 디스크에 작성되는 총 데이터의 수가 줄어들 것이고 (전체 디스크 블록 대신 변경된 데이터만 작성하면 됨) 디스크 필요량도 줄어들 것이다 (모든 변경 사항이 로그에 순차적 디스크 블록으로 들어갈 수 있기 때문이다). 더 나아가 동시에 일어나는 여러 개의 트랜잭션과 관련된 변경 사항들이 결합되어 한 번에 트랜잭션 로그로 작성될 수 있다. 이는 트랜잭션 별로 여러 개의 디스크 쓰기가 필요하지 않고 대신 여러 개의 트랜잭션을 하나의 디스크 쓰기로 처리할 수 있음을 의미한다. 그 후 RM은 변경된 데이터에 대응하는 실제 디스크 블록을 업데이트할 것이다.

 

재구동시 복구

 

시스템 장애시 시스템이 복구되면서 제일 먼저 하는 일은 로그에는 있으나 그 데이터 블록이 아직 업데이트되지 않은 확약된 트랜잭션의 결과를 재적용하는 것이다. 이런 방식으로 로그는 장애시 영속성을 보장하고, 우리가 수행하는 디스크 입출력 작업의 수를 줄일 수 있게 하고 혹은 적어도 시스템 성능에 영향을 덜 미칠 때로 이들을 연기하게 해 준다.

 

 

2단계 확약 (Two-phase commit)

 

많은 트랜잭션은 하나의 RM (보통 데이터베이스)만을 포함하고 있다. 이 경우 RM은 보통 트랜잭션을 확약하고 롤백하기 위한 대부분의 작업을 수행한다. (거의 모든 트랜잭션성 RM이 자체적인 트랜잭션 관리자를 내장하고 있는데, 이는 지역 트랜잭션, 즉 그 RM만이 참여한 트랜잭션을 처리할 수 있다.) 그러나 트랜잭션이 두 개, 혹은 그 이상의 RM (아마도 두 개의 개별적인 데이터베이스, 혹은 데이터베이스와 JMS 큐, 또는 두 개의 개별적인 JMS provider)을 포함하고 있다면 "모두 처리하든지 아니면 아무것도 처리하지 않는"다는 의미론이 그 RM 내에서 뿐 아니라 트랜잭션 내의 모든 RM에게 적용되도록 하고 싶을 것이다. 이런 경우 TPM은 2단계 확약을 전개할 것이다. 2단계 확약에서 TPM은 각 RM에게 "준비" 메시지를 보내어 그 RM이 준비가 되었고 트랜잭션을 수행할 수 있는지 물어본다. 모든 RM으로부터 긍정적인 답을 받으면 자신의 트랜잭션 로그에 트랜잭션이 확약되었다고 기록한 후 모든 RM에게 그 트랜잭션을 확약하라고 지시한다. 만일 어느 한 RM이라도 실패한다면, 재구동시 TMP에게 장애가 일어나는 시점에 미완으로 남아있던 트랜잭션의 상태에 대하여 질의하고, 그들을 확약하거나 혹은 롤백한다.

 

2단계 확약을 사회 생활에 비유해 보면 결혼식을 들 수 있다. 목사나 판사가 먼저 신랑, 신부에게 각각 "당신은 그/그녀를 당신의 남편/아내로 받아들이겠습니까?"하고 물어본다. 신랑, 신부 모두 "네"라고 대답하면 둘은 모두 결혼했음이 선언된다.; 그렇지 않다면 둘 모두 결혼하지 않은 상태로 남을 것이다. 누가 먼저 "네"라고 대답했는지에 상관없이, 한 사람은 결혼했는데 다른 쪽은 결혼하지 않은 상태로 남는 경우는 없다.

 

트랜잭션은 예외 처리 메커니즘이다.

 

여러분은 동기화된 블록이 메모리 내 데이터에 수행하는 것과 동일한 많은 기능을 트랜잭션이 애플리케이션 데이터에 제공함을 보았을 것이다. 즉 원자성, 변경사항의 가시성, 명확한 순서 등을 보장한다. 그러나 동기화가 주로 동시 처리 제어 메커니즘에 해당된다면, 트랜잭션은 주로 예외 처리 메커니즘이다. 디스크에 장애가 없고 시스템과 소프트웨어가 파손되지 않으며 전원도 100% 안전한 곳에서는 트랜잭션이 필요하지 않을 것이다. 트랜잭션은 사회에서 계약법이 수행하는 역할을 기업 애플리케이션에서 수행한다. 즉 계약 이행의 일부분에 대하여 어느 한 쪽이 이행에 실패할 경우 계약 이행 의무가 어떻게 풀리는지를 명시한다. 우리는 계약서를 작성할 때 보통 계약 내용이 여유 있기를 바라며, 고맙게도 대부분 그러하다.

 

간단한 자바 프로그램을 통해 유추해보면 트랜잭션은 catchfinally 블록이 메소드 레벨에 제공하는 것과 동일한 몇 가지 이점을 애플리케이션 레벨에 제공한다. ; 긴 에러 복구 코드를 작성하지 않고도 신뢰성 있는 에러 복구를 수행할 수 있도록 해준다. 한 파일을 다른 쪽에 복사하는 이 메소드를 검토해보자.:

public boolean copyFile(String inFile, String outFile) {  InputStream is = null;  OutputStream os = null;  byte[] buffer;  boolean success = true;  try {    is = new FileInputStream(inFile);    os = new FileOutputStream(outFile);    buffer = new byte[is.available()];    is.read(buffer);    os.write(buffer);  }  catch {IOException e) {    success = false;  }  catch (OutOfMemoryError e) {    success = false;  }  finally {    if (is != null)      is.close();    if (os != null)      os.close();  }  return success;}

 

전체 파일을 위해 하나의 버퍼를 할당하는 것이 그리 좋지 않은 생각이라는 사실을 무시하고 본다면, 이 메소드 내에서 잘못될 가능성이 있는 곳은 어디인가 ? 아주 많다. 입력 파일이 존재하지 않거나 이 사용자가 그것을 읽을 권한을 가지고 있지 않을 수 있다. 사용자가 출력 파일을 작성할 권한이 없을 수 있고 혹은 다른 사용자에 의해 잠겨 있을 수도 있다. 파일 작성 작업을 완료할만큼 충분한 디스크 공간이 없을 수도 있고, 혹은 가용 메모리가 충분하지 않을 경우 버퍼 할당이 실패할 수도 있다. 다행히도 이 모든 사항이 finally 절에 의해 처리되는데, 이 절은 copyFile()이 사용하는 모든 자원을 해제한다.

 

이 메소드를 구식 C 시절에 작성했다면 각 작업 (입력 개시, 출력 개시, malloc, 읽기, 쓰기)에 대해 반환 상태를 테스트해야 하고, 작업이 실패할 경우 이전의 모든 성공적인 작업을 취소하고 적절한 상태 코드를 반환해야 한다. 모든 에러 처리 코드 때문에 코드는 아주 커질 것이고 따라서 읽기가 더 어려워질 것이다. 자원 해제에 실패하거나 한 자원을 두 번 해제하거나 아직 할당되지 않은 자원을 해제하는 등 에러 처리 코드에서 실수를 하기도 쉽다 (또한 테스트하기 가장 어려운 부분이기도 하다). 두 개의 파일과 하나의 버퍼, 이러한 자원보다 훨씬 많은 자원을 처리해야 하는 더 큰 실제 메소드에서는 이것이 훨씬 더 복잡해질 것이다. 그 모든 오류 복구 코드에서 실제 프로그램 로직을 찾기란 더욱 더 어려워질 수 있다.

 

이제 여러분이 여러 데이터베이스에 여러 행을 삽입하거나 업데이트해야 하는 복잡한 작업을 수행하고 있고, 한 작업이 무결성 제약을 위반하여 실패했다고 상상해 보자. 여러분이 자체적으로 에러 복구를 관리한다면 어떤 작업들이 이미 수행되었는지를 추적하고 다음 작업이 실패했을 때 이들 각각을 취소하는 방법을 알고 있어야 한다. 작업 단위가 여러 메소드나 컴포넌트에 걸쳐 있을 때는 훨씬 더 어려워진다. 여러분 애플리케이션을 트랜잭션으로 체계화하면 이 모든 부기 작업을 데이터베이스에 위임할 수 있다. -- '롤백'이라고 말하기만 하면 트랜잭션 개시 이후에 여러분이 수행했던 모든 작업은 취소된다.

 

결론

 

애플리케이션을 트랜잭션으로 체계화함으로써 우리는 애플리케이션 상태의 정확한 변형 세트를 정의할 수 있고, 시스템이나 컴포넌트 장애 후에도 애플리케이션이 항상 올바른 상태에 있음을 보증할 수 있다. 트랜잭션은 예외 처리와 복구 작업의 많은 요소들을 TPM과 RM에 위임하도록 해주어, 코드를 단순화시키고 우리가 대신 애플케이션 로직에 대해 생각할 수 있도록 해준다.

 

이 시리즈의 2편에서는 J2EE 애플리케이션에서는 이것이 어떤 의미를 가지는지 살펴보겠다. -- J2EE가 트랜잭션 의미론을 J2EE 컴포넌트 (EJB 컴포넌트, servlet 및 JSP 페이지)에 전하게 하는 방법과, 심지어는 빈이 관리하는 트랜잭션에 대해서도 자원 사용을 애플리케이션에게 완전히 투명하게 만드는 방법, 그리고 한 개의 트랜잭션이 어떻게 여러 시스템 상에 있는 한 EJB 컴포넌트에서 다른 EJB 컴포넌트로의 제어 흐름, 또는 한 servlet에서 EJB 컴포넌트로의 제어 흐름을 투명하게 따라 갈 수 있는지에 대해 설명하겠다.

 

J2EE가 객체 트랜잭션 서비스를 비교적 투명하게 제공한다 하더라도 애플리케이션 설계자는 여전히 어디에서 트랜잭션을 구분할지와 애플리케이션에서 트랜잭션 자원을 어떻게 사용할 것인지에 관해 주의 깊게 생각해야 한다. 부정확한 트랜잭션 구분은 애플리케이션을 일관성 없는 상태로 둘 수 있고, 트랜잭션 자원을 잘못 사용하면 성능에 심각한 문제가 발생할 수 있다. 이 시리즈의 3편에서 이 문제들을 다루고 여러분의 트랜잭션을 체계화하는 방법에 관해 몇 가지 조언을 제공하도록 하겠다.

 

 

참고 자료

 

 

 

<출처 : IBM developerWorks>

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