본문 바로가기

Dev.../플밍 관련 자료

[펌] JTS 이해하기 -- 보안과 퍼포먼스 균형맞추기

 

JTS 시리즈를 통해 Brian은 트랜잭션의 기초와 J2EE 컨테이너가 트랜잭션 서비스를 EJB 컴포넌트에 투명하게 할 수 있는지를 설명했다. 이번에는 트랜잭션 경계화와 고립화 관리에 필요한 J2EE 기능을 설명한다. 또한 그들을 효과적으로 사용할 수 있는 가이드라인도 제시한다.

 

Part 1 ("JTS 이해하기 - 트랜잭션에 대한 소개")과 Part 2 ("JTS 이해하기 - 장막 뒤의 마법 ")에서 트랜잭션이 무엇인지를 정의했고 트랜잭션의 기본 속성, Java Transaction Service와 J2EE 콘테이너가 함께 작동하여 J2EE 컴포넌트를 지원하는 방법을 설명했다. 이제는 트랜잭션 경계화(demarcation)와 고립(isolation)에 대해 알아보자.

 

EJB 컴포넌트의 트랜잭션 경계화와 고립 속성을 정의하는 책임은 애플리케이션 어셈블러에 있다. 이들을 부적절하게 설정하면 퍼포먼스, 확장성, 애플리케이션의 내구성에 심각한 결과를 초래할 수 있다. 불행히도 이러한 속성들을 적절히 설정하는 데에 정해진 규칙 같은 것은 없다. 다만 병행성 위험과 퍼포먼스 위험 사이에서 균형을 찾도록 도와주는 가이드라인이 있을 뿐이다.

 

Part 1에서 다루었듯이 트랜잭션은 기본적으로 예외 처리(exception-handling) 매커니즘이다. 무엇인가 잘못되었다면 복구할 수 있도록 도와준다. 실제로 나쁜 상태가 되는 것은 거의 없기 때문에 비용과 시간 할애를 최소화 할 수 있다. 애플리케이션에서 트랜잭션을 사용하는 방법에 따라 애플리케이션 퍼포먼스와 확장성에 큰 영향을 미칠 수 있다.

 

 

트랜잭션 경계화


J2EE 콘테이너는 트랜잭션이 어디에서 시작하고 끝나는지를 정의하는 두 가지 매커니즘 (빈 관리(bean-managed) 트랜잭션과 콘테이너 관리(container-managed) 트랜잭션)을 제공한다. 빈 관리 트랜잭션의 경우 UserTransaction.begin()UserTransaction.commit()을 이용하여 빈(bean) 메소드에서 트랜잭션을 시작하고 끝낸다. 반면, 콘테이너 관리 트랜잭션의 경우 좀 더 많은 유연성을 제공한다. 어셈블리 디스크립터에 각 EJB 메소드를 위한 트랜잭션 속성을 정의함으로서 각 메소드 별로 트랜잭션 요구사항이 무엇인지를 지정할 수 있고 콘테이너가 트랜잭션을 언제 시작하고 끝내야 하는지를 결정하도록 할 수 있다. 두 경우 모두 트랜잭션을 설계하는 기본 가이드라인은 같다.

 

전략


트랜잭션 경계화의 첫 번째 규칙은 "짧게 유지할것!" 트랜잭션은 병행 제어를 제공한다. 이것은 리소스 매니저가 트랜잭션 과정 중에 액세스한 데이터 아이템을 대신하여 lock을 얻어 트랜잭션이 끝날때까지 그들을 보유하고 있어야 한다는 것을 의미한다. 여러분이 lock을 보유하고 있는 동안에 다른 트랜잭션은 lock을 해제할 때까지 기다린다. 트랜잭션이 매우 길어지면 모든 다른 트랜잭션들은 저지당하고 애플리케이션 쓰루풋은 내려가기 마련이다.

Rule 1: 트랜잭션을 가능한 한 짧게 유지하라!

트랜잭션을 짧게 유지함으로서 애플리케이션의 확장성을 향상시킨다. 트랜잭션을 가능한 짧게 유지하는 최상의 방법은 트랜잭션의 중간에 불필요하게 시간을 허비하는 어떤 작업도 수행하지 않고 특별히 트랜잭션의 중간에 사용자 인풋을 기다리지 않는다.

 

트랜잭션을 시작하고 데이터베이스에서 데이터를 도출하여 데이터를 디스플레이하고 사용자에게 트랜잭션 동안 선택을 하도록 요청하고 싶을 것이다. 하지만 이렇게 하지 말기를 바란다. 사용자가 주의를 기울이더라도 응답하기 까지는 수 초가 걸리고 이는 데이터베이스에서 lock을 보유하기엔 긴 시간이다. 사용자가 점심을 먹거나 집에 가기위해 컴퓨터에서 물러나기라도 한다면 어떻게 될까? 애플리케이션은 간단히 멈추게 될 것이다. 트랜잭션 도중에 I/O를 수행하는 것은 재앙으로 가는 비결이다.

Rule 2: 트랜잭션 도중 사용자 인풋을 기다리지 말것!

 

관련 오퍼레이션을 함께 그룹핑 할 것


각 트랜잭션은 중대한 오버헤드를 갖고있기 때문에 오퍼레이션 당 오버헤드를 최소화하기위해 단일 트랜잭션에서 가능한 많은 오퍼레이션을 수행하는 것이 최상의 방법이라고 생각할 수도 있다. 하지만 Rule 1에 따르면 긴 트랜잭션은 확장성에 나쁘다고 한다. 그렇다면 오퍼레이션당 오버헤드를 줄이는 것과 확장성 사이의 균형을 어떻게 맞추면 되는가?

 

Rule 1--트랜잭션 당 한 오퍼레이션--을 논리적 마지막 수단으로 취한다면 추가적인 오버헤드를 가져올 뿐 아니라 애플리케이션 상태의 일관성도 타협하게 된다. 트랜잭션 리소스 매니저는 애플리케이션 상태의 일관성을 관리하도록 되어있다. 하지만 그들은 일관성을 정의하기 위해 애플리케이션에 의존한다. 사실 트랜잭션을 설명할 때 우리가 사용하는 일관성의 정의는 다소 모호하다. 일관성은 애플리케이션이 말하는 모든것을 의미한다.

 

Part 1에서는 한 어카운트에서 다른 어카운트로 자금을 이동하는 뱅킹 애플리케이션 예제를 보았다. Listing 1은 SQL로 할 수 있는 구현을 보여준다. 다섯 개의 SQL 작동이 포함되어있다:

 

Listing 1. 자금 이체용 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

 

다섯 개의 개별 트랜잭션으로서 이 오퍼레이션을 실행한다면 어떤 일이 발생할까? 느려지는 것은 물론 일관성도 잃게된다. 예를들어 누군가가 첫 번째 SELECT 실행과 연속적인 차변 UPDATE 사이에 개별 트랜잭션으로 일부로서 A 어카운트에서 돈을 인출한다면 어떻게 될까? 이것은 이 코드에 의해 시행되어야 하는 비지니스 규칙을 위반한 것이 된다. 시스템이 첫 번째 UPDATE와 두번째 UPDATE 사이에서 고장난다면 어떻게 될까? 시스템이 복구되면 돈은 A 어카운트에 남아있게 되지만 B 어카운트에 기입되지 않게되고 이유는 기록되지 않는다. 이것은 두 계정의 소유자에게 안좋은 일이다.

 

Listing 1의 다섯 개의 SQL 오퍼레이션은 하나의 관련된 오퍼레이션으로 볼 수 있다. 즉 돈을 한 어카운트에서 다른 어카운트로 전송하는 것을 의미한다. 그러므로 우리는 모든것이 실행되거나 아예 실행되지 않거나 하기를 바라는 것이다. 그들이 단일 트랜잭션에서 모두 실행되어야 하는 이유가 바로 이것이다.

Rule 3: 관련 오퍼레이션을 단일 트랜잭션으로 그룹핑 할것!

 

이상적인 균형


Rule 1은 트랜잭션이 되도록이면 짧아져야 할 것을 요구하고 있다. Listing 1의 예제에서는 일관성을 유지하기 위해서 가끔은 오퍼레이션들을 하나의 트랜잭션으로 묶어야 한다는 것을 보여주었다. 물론 이 모든 것은 어떤 구성요소들이 "관련 오퍼레이션"인지를 결정하는 애플리케이션에 의존한다. Rule 1과 3을 조합하여 트랜잭션의 범위를 설명하는 일반적인 가이드라인을 만들수 있다. 이것이 Rule 4 이다:

Rule 4: 관련 오퍼레이션을 단일 트랜잭션으로 그룹핑하라. 단, 연관성 없는 오퍼레이션은 개별 트랜잭션으로 놓을것!

 

콘테이너 관리 트랜잭션


콘테이너 관리 트랜잭션을 사용할 때, 트랜잭션이 어디서 시작하여 어디에서 끝나는지를 명확히 설명하는 대신 각 EJB 메소드를 위한 트랜잭션 요구사항을 정의한다. 트랜잭션 모드는 빈의 assembly-descriptorcontainer-transaction 섹션의 trans-attribute 엘리먼트에 정의되어 있다. 메소드의 트랜잭션 모드는 호출 메소드가 트랜잭션이 이미 포함되어 있는지의 여부를 나타내는 상태와 더불어 EJB 메소드가 호출 될 때 콘테이너가 어떤 액션을 취할지 결정한다:

  • 기존 트랜잭션에서 메소드를 추가시킬것!
  • 새로운 트랜잭션을 만들고 여기에 메소드를 추가시킬것!
  • 모든 트랜잭션에 메소드를 추가하지 말것!
  • 예외를 줄것!

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>logError</method-name>    </method>    <trans-attribute>RequiresNew</trans-attribute>  </container-transaction>  ...</assembly-descriptor>

 

J2EE 스팩은 여섯 개의 트랜잭션 모드를 정의한다: Required, RequiresNew, Mandatory, Supports, NotSupported, Never. Table 1은 각 모드의 작동을 요약해 놓은 것이다. 기존 트랜잭션에 있을 때 호출될 때와 트랜잭션에 없을 때 호출 될 경우로 분류했다. 각 모드를 지원하는 EJB 컴포넌트 유형도 설명했다

 

Table 1. 트랜잭션 모드

트랜잭션 모드

빈 유형T 트랜잭션에 있을 때 호출될 경우의 액션트랜잭션 밖에서 호출될 때의 액션
RequiredSession, Entity, Message-drivenT에 포함새로운 트랜잭션
RequiresNewSession, Entity새로운 트랜잭션새로운 트랜잭션
SupportsSession, Message-drivenT에 포함트랜잭션 없이 실행
MandatorySession, EntityT에 포함Error
NotSupportedSession, Message-driven트랜잭션 없이 실행트랜잭션 없이 실행
NeverSession, Message-drivenError트랜잭션 없이 실행

 

콘테이너 관리 트랜잭션만을 사용하는 애플리케이션에서, 트랜잭션을 시작할 수 있는 유일한 방법은 트랜잭션 모드가 Required 또는 RequiresNew인 EJB 메소드를 컴포넌트가 호출할 경우이다. 콘테이너가 호출한 결과로서 트랜잭션을 만들 때, 그 트랜잭션은 메소드가 완료될 때 닫힌다. 메소드가 정상적으로 리턴되면 콘테이너는 트랜잭션을 발생시킨다. 예외를 주어 메소드가 종료하면 콘테이너는 트랜잭션을 롤백(roll back)하고 예외를 선언한다. 메소드가 기존 T 트랜잭션에 있을 때 호출되고 그 트랜잭션 모드는 메소드가 트랜잭션 없이 실행되거나 새로운 트랜잭션에서 실행되어야 한다는 것을 지정하면, T 트랜잭션은 메소드가 완료될 때 까지 중지하고 이전의 T 트랜잭션은 시작된다.

 

 

트랜잭션 모드 선택하기


빈 메소드를 위해 어떤 모드를 선택해야 할까? 세션과 message-driven bean의 경우 모든 호출이 트랜잭션의 일부로서 실행될 것을 확인하기 위해 Required를 사용해야 하지만 메소드가 더 큰 트랜잭션의 컴포넌트가 되도록 할 수 있다. RequiresNew로 시도해보자. 이것은 메소드의 작동이 호출된 메소드의 행동과 개별적으로 이루어진다는 것을 확신할 때만 사용될 수 있다. RequiresNew는 전형적으로 시스템에서 다른 객체들과 관련이 없는 객체를 가지고 사용된다.

 

부적절한 방법으로 RequiresNew 를 사용하면 위에 설명한 것과 비슷한 결과가 될 수 있다. Listing 1의 코드는 하나가 아닌 다섯 개의 개별 트랜잭션에서 실행되었고 이는 애플리케이션을 일관성 없는 상태로 만들 수 있다.

 

CMP (container-managed persistence) 엔터티 빈의 경우, Required를 사용해야 할 것이다. Mandatory 또한 합당한 옵션이 될 수 있는데 특별히 초기 개발에 맞다. CMP 엔터티빈에 RequiresNew 를 사용할 수는 없다. NotSupportedNever는 비 트랜잭션 리소스용이다.

 

EJB 애플리케이션이 올바르게 디자인될 때 트랜잭션 모드에 대한 위 가이드라인은 Rule 4의 트랜잭션 경계화를 만들어낸다. J2EE 아키텍쳐는 애플리케이션을 가장 작은 프로세싱 청크로 분해되는 것을 장려한다.

 

 

고립


Part 1에서, 고립(isolation)은 한 트랜잭션의 효과가 동시에 실행하고 있는 다른 트랜잭션에 보이지않는 것이라고 정의했다. 트랜잭션의 관점에서 트랜잭션은 병렬보다는 순차적으로 실행한다. 트랜잭션 리소스 매니저가 많은 트랜잭션들을 동시에 프로세스 하면서 "고립의 환상"을 제공한다. 가끔씩 고립의 제약조건은 새로운 트랜잭션 시작을 기존 트랜잭션이 완료될 때까지 미루도록 한다. 트랜잭션을 완료하는 것에는 적어도 하나의 동기식(synchronous)의 디스크 I/O가 포함되어있기 때문에 초당 트랜잭션의 수를 초당 디스크 쓰기의 수로 제한할 수 있다. 이것은 확장성에 좋지 않다.

 

실제로 고립 요구조건을 풀어서 더 많은 트랜잭션들이 동시에 실행될 수 있도록 하고 시스템 응답과 더 나은 확장성으로 향상시킬 수 있다. 거의 모든 데이터베이스는 네 개의 표준 고립 레벨을 지원한다: Read Uncommitted, Read Committed, Repeatable Read, Serializable.

 

콘테이너 관리 트랜잭션을 위한 고립 관리는 현재 J2EE 스팩의 범위를 벗어나있다. 하지만 IBM WebSphere와 BEA WebLogic 같은 많은 J2EE 콘테이너는 콘테이너 스팩의 확장을 제공하여 트랜잭션 모드가 어셈블리 디스크립터에 설정되는 것과 같은 방식으로 메소드 기반으로 트랜잭션 고립 레벨을 설정할 수 있다. 빈 관리 트랜잭션의 경우 JDBC 또는 다른 리소스 매니저 커넥션을 통해 고립 레벨을 설정할 수 있다.

 

고립 레벨 간 차이점을 보기위해 여러가지 병행성 위험을 분류해 보겠다. 다음 위험요소들 모두는 두 번째 트랜잭션이 이미 시작한 후에도 첫 번째 트랜잭션이 두 번째 트랜잭션에 보여진 결과와 연관되어있다:

  • Dirty Read: 한 트랜잭션의 중간 결과가 다른 트랜잭션에 보여질 때 발생.

  • Unrepeatable Read: 한 트랜잭션이 데이터 아이템을 읽고 그 다음에 같은 아이템을 다시 읽었을 때 다른 값이 보일 때 발생.

  • Phantom Read: 한 트랜잭션이 다중 열(row)을 리턴하는 질의를 수행하고 후에 같은 질의를 다시 수행할 때, 첫 번째 질의 수행에서는 보이지 않던 추가 열이 생기는 경우 발생.

네 개의 표준 고립 레벨은 이러한 세 개의 고립 위험과 관련되어있다. (Table 2). 가장 낮은 고립 레벨은 Read Uncommitted 로서 다른 트랜잭션에서 이루어진 변경을 막지 못한다. 하지만 읽기 lock에 대한 경쟁(contention)을 요구하지 않기 때문에 가장 빠르다. 가장 높은 고립 레벨인 Serializable은 위에 제사한 고립의 정의와 일치한다. 각 트랜잭션은 다른 트랜잭션의 효과로부터 완전히 고립된다.

 

Table 2. 트랜잭션 고립 레벨

고립 레벨 Dirty readUnrepeatable readPhantom read
Read UncommittedYesYesYes
Read CommittedNoYesYes
Repeatable ReadNoNoYes
SerializableNoNoNo

 

대부분의 데이터베이스 경우 기본 고립 레벨은 Read Committed 인데 트랜잭션의 어떤 지점에서도 애플리케이션 데이터가 보이는 것을 방지해주기 때문에 유용하다. Read Committed는 가장 전형적인 짧은 트랜잭션에 사용하기 알맞은 고립 레벨이다.

 

높은 수준의 고립인 Repeatable Read와 Serializable은 트랜잭션 전체에서 높은 수준의 일관성이 필요할 때 알맞다. 데이터 일관성이 절대적으로 필요한 경우 새로운 열 생성을 방어할 필요가 있다. 이때 Serializable을 사용해야 한다.

 

가장 낮은 고립 레벨인 Read Uncommitted는 거의 쓰이지 않는다. 정확하지 않는 값을 얻고자 할 때 쓰이며 질의는 예기치 않은 퍼포먼스 오버헤드를 만들게 된다. 주문 수량 또는 당일 주문받은 달러 같이, 빠르게 수량을 계산할 때에는 Read Uncommitted가 전형적으로 사용된다.

 

고립과 확장성 사이에는 모순이 존재하기 때문에 트랜잭션에 고립 레벨을 선택할 때에는 특별한 주의를 기울여야 한다. 너무 낮은 레벨을 선택하면 데이터에 위험부담이 생긴다. 또, 너무 높은 레벨을 선택하면 퍼포먼스에 좋지 않다. 일반적으로 데이터 일관성 문제는 퍼포먼스 문제보다 더 심각하다:

Rule 5: 데이터를 안전하게 하는 가장 낮은 고립 레벨을 사용하되 불확실할 경우 Serializable을 사용할것!

컴포넌트를 개발 할 때 고립의 요구 사항에 대해 신중하게 생각해봐야 한다. 만약 퍼포먼스가 문제가 된다면 나중에 당황하지 않도록 낮은 고립 레벨도 견딜 수 있는 트랜잭션을 작성해야 한다. 어떤 메소드가 작동하고 고립 레벨을 정확히 설정하기 위한 일관성 전제조건이 무엇인지를 알 필요가 있으므로 개발하면서 일관성 요구사항과 전제조건을 세심하게 문서화 하는 것이 좋다.

 

 

결론


이 글에 제시된 가이드라인들이 다소 모순처럼 보인다. 트랜잭션 경계화와 고립은 근본적으로 모순 그 자체이기 때문이다. 안전성과 툴의 퍼포먼스 오버헤드사이의 균형을 유지해야 한다.

참고자료

 

 

 

<출처 : IBM developerWorks>

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