실행 방법입니다. H2 인메모리 DB 를 쓰므로 lib 의 H2 jar 가 필요합니다.
cd java-src\extension\15_transaction
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 -cp "..\..\lib\*" *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 -cp "..\..\lib\*;." Main # 약 1초Tx — 커스텀 어노테이션 + 동적 프록시 트랜잭션 매니저// Tx.invoke: 프록시가 실제 호출 앞뒤로 트랜잭션을 관리한다
Ctx outer = CURRENT.get();
boolean requiresNew = tx.propagation() == Propagation.REQUIRES_NEW;
Ctx ctx;
boolean owner; // 이 호출이 커밋·롤백을 책임지는 경계인가
if (outer == null || requiresNew) {
ctx = new Ctx(newConnection());
CURRENT.set(ctx);
owner = true;
} else {
ctx = outer; // REQUIRED: 기존 트랜잭션에 그대로 참여
owner = false;
}REQUIRED 는 이미 열린 트랜잭션이 있으면 그 커넥션을 그대로 쓰고, REQUIRES_NEW 는 무조건 새 커넥션을 열어 ThreadLocal 에 갈아 끼웁니다. 메서드가 끝나면 finally 에서 원래 커넥션으로 되돌립니다.
// 내부(owner=false) 호출이 실패하면 즉시 롤백하지 않고 표시만 한다
} else if (tx.rollbackFor().isInstance(cause)) {
ctx.rollbackOnly = true;
}바깥 메서드가 이 예외를 삼키고 정상 종료하면, finish() 는 rollbackOnly 를 보고 커밋 대신 롤백한 뒤 UnexpectedRollbackException 을 던집니다. 2.7 절에서 설명한 그대로입니다.
1) 정상 이체 -> 커밋
잔액: A=4000 B=1000
2) 잔액 부족 -> RuntimeException -> 전체 롤백
예외: 잔액 부족: A balance=4000
잔액: A=4000 B=1000
3-a) checked 예외 + 기본 rollbackFor -> 커밋됨
예외: 사후 감사 실패(기본 규칙이라 커밋됨)
잔액: A=3500 B=1500
3-b) checked 예외 + rollbackFor=Exception.class -> 롤백됨
예외: 사후 감사 실패(rollbackFor=Exception 이라 롤백)
잔액: A=3500 B=1500
4) REQUIRES_NEW 실패 로그는 바깥 롤백과 무관하게 남음
예외: 잔액 부족: A balance=3500
잔액: A=3500 B=1500
failure_log 건수: 1
5) 내부 실패를 바깥이 catch -> commit 시 UnexpectedRollbackException
예외: 내부 트랜잭션이 rollback-only 로 표시돼 커밋할 수 없습니다
잔액: A=3500 B=1500
6) self-invocation: 프록시 없이 impl 을 직접 호출
예외: 트랜잭션 밖에서는 DB 접근을 금지합니다(self-invocation?)3-a 와 3-b 는 같은 이체 로직에 어노테이션 설정만 다르게 붙인 결과입니다. rollbackFor 하나로 커밋과 롤백이 갈리는 것을 잔액으로 직접 확인할 수 있습니다.