Appendix C: Batch Processing and Transactions
C.1. Simple Batching with No Retry
재시도 없는 중첩된 배치에 대한 다음의 간단한 예를 살펴보자. 이는 배치에 대한 일반적인 상황을 보여준다. 입력 소스는 소진될 때까지 처리되고 처리 “청크”가 끝날 때 주기적으로 커밋된다.
1 | REPEAT(until=exhausted) {
|
2 | TX {
3 | REPEAT(size=5) {
3.1 | input;
3.2 | output;
| }
| }
|
| }
입력 작업(3.1)은 메시지 기반 수신(예: JMS에서) 또는 파일 기반 읽기일 수 있지만, 전체 잡을 완료할 가능성이 있는 처리를 복구하고 계속하려면 트랜잭션내에 있어야 한다.
3.2의 작업에도 동일하게 적용된다. 트랜잭션이거나 멱등적(idempotent)이어야 한다.
REPEAT
(3)의 청크가 3.2의 데이터베이스 예외로 인해 실패하면 TX(2)는 전체 청크를 롤백해야 한다.
C.2. Simple Stateless Retry
다음 예에서 볼 수 있듯이 웹 서비스나 기타 원격 리소스에 대한 호출과 같이 트랜잭션이 아닌 작업에 대해 재시도를 사용하는 것도 유용하다:
0 | TX {
1 | input;
1.1 | output;
2 | RETRY {
2.1 | remote access;
| }
| }
이는 실제로 재시도의 가장 유용한 애플리케이션 중 하나다. 원격 호출은 데이터베이스 업데이트보다 실패하고 재시도할 가능성이 훨씬 높기 때문이다. 원격 접근(2.1)가 결국 성공하면, 트랜잭션 TX(0)가 커밋된다. 원격 액세스(2.1)가 결국 실패하면 트랜잭션 TX(0)가 롤백된다.
C.3. Typical Repeat-Retry Pattern
가장 일반적인 배치 프로세싱 패턴은 다음 예제와 같이 청크의 내부 블록에 재시도를 추가하는 것이다:
1 | REPEAT(until=exhausted, exception=not critical) {
|
2 | TX {
3 | REPEAT(size=5) {
|
4 | RETRY(stateful, exception=deadlock loser) {
4.1 | input;
5 | } PROCESS {
5.1 | output;
6 | } SKIP and RECOVER {
| notify;
| }
|
| }
| }
|
| }
내부 RETRY
(4) 블록은 “상태”를 나타낸다. 상태 재시도에 대한 설명은 일반적인 사례를 참고하자. 즉, 재시도 PROCESS
(5) 블록이 실패하면 RETRY
(4)의 동작은 다음과 같다:
- 예외를 발생시켜, 청크 레벨에서 트랜잭션
TX
(2)를 롤백하고, 아이템이 입력 큐에 다시 표시되도록 한다. - 아이템이 다시 나타나면 재시도 정책에 따라 재시도하고
PROCESS
(5)를 재실행할 수 있다. 두 번째 및 후속 시도가 다시 실패하고 예외가 다시 발생할 수 있다. - 결국, 해당 아이템은 마지막으로 다시 나타난다. 재시도 정책은 더 이상 또 다른 시도를 허용하지 않으므로
PROCESS
(5)는 실행되지 않는다. 이 경우RECOVER
(6) 경로를 따르며 처리 중인 아이템을 효과적으로 “스킵”한다.
계획상에 RETRY
(4)에 사용된 표기는 입력 스텝(4.1)이 재시도의 일부임을 명시적으로 나타낸다. 또한 처리를 위한 두 가지 대체 경로가 있음을 분명히 한다. 즉, PROCESS
(5)로 표시되는 일반적인 경우와 RECOVER
(6)로 별도의 블록으로 표시되는 복구 경로이다. 두 개의 대체 경로는 완전히 다르다. 정상적인 상황에서는 하나만 동작한다.
특수한 경우(예: 특수 트랜젝션벨리드익셉션(TranscationValidException)
타입), 재시도 정책은 아이템이 다시 나타날 때까지 기다리는 대신 PROCESS
(5)가 실패한 후 마지막 시도에서 RECOVER
(6) 경로를 사용할 수 있는지 결정할 수 있다. 이는 일반적으로 사용할 수 없는 PROCESS
(5) 블록 내부에서 발생한 일에 대한 자세한 지식이 필요하기 때문에 일반적인 동작이 아니다. 예를 들어,오류가 발생하기 전에 출력에 쓰기 접근이 포함된 경우 트랜잭션 무결성을 보장하기 위해 예외가 다시 발생해야 한다.
외부 REPEAT
(1)의 완료 정책은 계획 성공에 매우 중요하다. 출력(5.1)이 실패하면 예외가 발생할 수 있으며(설명된 대로 일반적으로 발생함), 이 경우 트랜잭션 TX
(2)가 실패하고 예외가 외부 배치 REPEAT
(1)를 통해 전파될 수 있다. 재시도하면 RETRY
(4)가 여전히 성공할 수 있으며 전체 배치가 중지되는 것은 원하지 않으므로 외부 REPEAT
(1)에 exception=not critical
을 추가한다.
그러나 TX
(2)가 실패하고 재시도하는 경우 외부 완료 정책으로 인해 내부 REPEAT
(3)에서 다음에 처리되는 아이템이 방금 실패한 아이템이라는 보장은 없다. 그럴 수도 있지만, 입력 구현(4.1)에 따라 다르다. 따라서 새 아이템이나 이전 아이템에서 출력(5.1)이 다시 실패할 수 있다. 배치 프로세싱 클라이언트는 각 RETRY
(4) 시도가 실패한 마지막 시도와 동일한 아이템을 처리할 것이라고 가정해서는 안 된다. 예를 들어, REPEAT
(1)에 대한 종료 정책이 10번의 시도 후에 실패하는 경우 10번의 연속 시도 후에 실패하지만 반드시 동일한 아이템에 대한 실패는 아니다. 이는 전반적인 재시도 전략과 일치한다. 내부 RETRY
(4)는 각 아이템의 이력을 알고 있으며, 해당 아이템에 대해 재시도할지 여부를 결정할 수 있다.
C.4. Asynchronous Chunk Processing
일반적인 예제의 내부 배치 또는 청크는 에이싱크테스크익스큐터(AsyncTaskExecutor)
를 사용하도록 외부 배치를 구성하여 동시에 실행할 수 있다. 외부 배치는 완료되기 전 모든 청크가 완료될 때까지 기다린다. 다음 예에서는 비동기식 청크 처리를 보여준다:
1 | REPEAT(until=exhausted, concurrent, exception=not critical) {
|
2 | TX {
3 | REPEAT(size=5) {
|
4 | RETRY(stateful, exception=deadlock loser) {
4.1 | input;
5 } PROCESS {
| output;
6 | } RECOVER {
| recover;
| }
|
| }
| }
|
| }
C.5. Asynchronous Item Processing
일반적인 예에서 청크의 개별 아이템은 원칙적으로 동시에 처리될 수도 있다. 이 경우 다음 예와 같이 각 트랜잭션이 싱글 스레드에 있도록 트랜잭션 경계를 개별 아이템 레벨로 이동해야 한다:
1 | REPEAT(until=exhausted, exception=not critical) {
|
2 | REPEAT(size=5, concurrent) {
|
3 | TX {
4 | RETRY(stateful, exception=deadlock loser) {
4.1 | input;
5 | } PROCESS {
| output;
6 | } RECOVER {
| recover;
| }
| }
|
| }
|
| }
이 계획은 모든 트랜잭션 리소스를 함께 묶는 이점을 희생한다. 처리 비용(5)이 트랜젝션 관리 비용(3)보다 훨씬 높은 경우에만 유용하다.
C.6. Interactions Between Batching and Transaction Propagation
배치 재시도와 트랜잭션 관리 사이에는 우리가 이상적으로 원하는 것보다 더 긴밀하게 결합되어 있다. 특히 상태 비저장 재시도는 NESTED 전파를 지원하지 않는 트랜잭션 관리자로 데이터베이스 작업을 재시도하는 데 사용할 수 없다.
다음 예제에서 반복 없이 재시도 한다:
1 | TX {
|
1.1 | input;
2.2 | database access;
2 | RETRY {
3 | TX {
3.1 | database access;
| }
| }
|
| }
동일한 이유로, 내부 트랜잭션 TX
(3)로 인해 RETRY
(2)가 결국 성공하더라도 외부 트랜잭션 TX
(1)이 실패할 수 있다.
불행하게도, 다음 예에서 볼 수 있듯이 재시도(RETRY) 블록에서 주변 반복(REPEAT) 배치 프로세싱(있는 경우)까지 동일한 효과가 투과된다:
1 | TX {
|
2 | REPEAT(size=5) {
2.1 | input;
2.2 | database access;
3 | RETRY {
4 | TX {
4.1 | database access;
| }
| }
| }
|
| }
이제 TX
(3)가 롤백되면 TX
(1)의 전체 배치가 오염되어 마지막에 강제로 롤백될 수 있다. 기본이 아닌 전파는 어떨까?
- 위의 예에서
TX
(3)의PROPAGATION_REQUIRES_NEW
는 두 트랜잭션이 모두 성공할 경우 외부TX
(1)가 오염되는 것을 방지한다. 그러나TX
(3)이 커밋되고TX
(1)이 롤백되면TX
(3)는 커밋된 상태로 유지되므로TX
(1)에 대한 트랜잭션 기능에 대한 위반이다.TX
(3)가 롤백하는 경우TX
(1)이 반드시 롤백되는 것은 아니다(그러나 재시도에서 롤백 예외가 발생하므로 실제로는 롤백할 가능성이 높다). TX
(3)의PROPAGATION_NESTED
는 재시도 사례(및 스킵이 포함된 배치 프로세싱)에서 요구하는 대로 작동한다.TX
(3)는 커밋할 수 있지만 이후에 외부 트랜잭션인 TX(1)에 의해 롤백된다. TX(3)이 롤백되면 실제로는 TX(1)이 롤백된다. 이 옵션은 하이버네이트나 JTA를 제외한 일부 플랫폼에서만 사용할 수 있지만 지속적으로 작동하는 유일한 플랫폼이다.
결과적으로, 재시도(RETRY) 블록에 데이터베이스 접근이 포함된 경우 NESTED
패턴이 가장 좋다.
C.7. Special Case: Transactions with Orthogonal Resources
중첩된 데이터베이스 트랜잭션이 없는 간단한 경우에 기본 전파(propagation)는 항상 정상이다. SESSION
및 TX
가 전역 XA
리소스가 아니므로 해당 리소스가 직교(orthogonal)하는 다음 예를 생각해보자:
0 | SESSION {
1 | input;
2 | RETRY {
3 | TX {
3.1 | database access;
| }
| }
| }
여기에 SESSION
(0)이라는 트랜잭션 메시지가 있지만, 플랫폼트랜젝션매니저(PlatformTransactionManager)
와 다른 트랜잭션에 참여하지 않으므로 TX
(3)이 시작될 때 전파되지 않는다. RETRY
(2) 블록 외부에서는 데이터베이스 접근이 없다. TX
(3)가 실패하고 결국 재시도에 성공하면 SESSION
(0)이 TX
블록과 독립적으로 커밋될 수 있다. 이는 “best-efforts-one-phase-commit” 시나리오와 유사하다. 최악의 상황은 RETRY
(2)가 성공하고 SESSION
(0)이 커밋할 수 없는 경우(예: 메시지 시스템을 사용할 수 없는 경우) 중복된 메세지가 발생한다.
C.8. Stateless Retry Cannot Recover
앞서 나타난 일반적인 예에서 상태 비저장(stateless) 재시도와 상태 저장(stateful) 재시도 간의 차이는 중요하다. 실제로 구별을 강제하는 것은 궁극적으로 트랜잭션 제약(constraint)이며, 이 제약으로 인해 구별이 존재하는 이유도 분명해진다.
아이템 처리를 트랜잭션으로 래핑하지 않는 한 실패한 아이템을 스킵하고 나머지 청크를 성공적으로 커밋할 수 있는 방법이 없다는 관찰부터 시작한다. 결과적으로 일반적인 배치 실행 계획을 다음과 같이 단순화한다:
0 | REPEAT(until=exhausted) {
|
1 | TX {
2 | REPEAT(size=5) {
|
3 | RETRY(stateless) {
4 | TX {
4.1 | input;
4.2 | database access;
| }
5 | } RECOVER {
5.1 | skip;
| }
|
| }
| }
|
| }
위 예제에서 최종 시도가 실패한 후 시작되는 RECOVER
(5) 경로가 있는 상태 비저장 RETRY
(3)를 보여준다. 상태 비저장 레이블은 특정 한도까지 예외를 다시 발생시키지 않고 블록이 반복됨을 의미한다. 이는 트랜잭션 TX
(4)에 전파가 중첩된 경우에만 작동한다.
내부 TX
(4)에 기본 전파(propagation) 프로퍼티가 있고 롤백되면 외부 TX
(1)를 오염시킨다. 내부 트랜잭션은 트랜잭션 매니저에 의해 트랜잭션 리소스가 손상되었다고 가정하므로 다시 사용할 수 없다.
중첩된 전파에 대한 지원은 매우 드물기 때문에 현재 버전의 스프링 배치에서는 상태 비저장 재시도를 통한 복구를 지원하지 않기로 결정했다. 앞에서 설명한 일반적인 패턴을 사용하면 더 많은 처리를 반복하는 대신 항상 동일한 효과를 얻을 수 있다.