Tx.Propagation 에 NOT_SUPPORTED 를 추가하고, invoke() 에서 이 값이면 현재 트랜잭션을 잠시 멈추고(CURRENT.set(null)) 트랜잭션 없이 메서드를 실행한 뒤 원래 트랜잭션을 복원하세요.
public enum Propagation { REQUIRED, REQUIRES_NEW, NOT_SUPPORTED }
// invoke() 안, 분기 추가
if (tx.propagation() == Propagation.NOT_SUPPORTED) {
Ctx suspended = CURRENT.get();
CURRENT.set(null);
try {
return method.invoke(target, args);
} finally {
CURRENT.set(suspended);
}
}새 커넥션을 열지 않으므로 owner 판단이나 커밋·롤백 로직이 필요 없습니다.
account 테이블에 version INT 컬럼을 추가하고, addBalance 대신 버전을 함께 확인하는 갱신 메서드를 만드세요. 갱신 건수가 0이면 RuntimeException 을 던져 롤백을 유도합니다.
private void updateWithVersion(Connection c, String id, int delta, int seenVersion) throws SQLException {
try (PreparedStatement ps = c.prepareStatement(
"UPDATE account SET balance = balance + ?, version = version + 1 "
+ "WHERE id = ? AND version = ?")) {
ps.setInt(1, delta);
ps.setString(2, id);
ps.setInt(3, seenVersion);
if (ps.executeUpdate() == 0) {
throw new RuntimeException("버전 충돌: " + id);
}
}
}WHERE version = ? 조건이 핵심입니다. 조회 시점 버전과 다르면 다른 트랜잭션이 먼저 수정한 것입니다.
Tx.finish() 시작 부분에 System.out.println 으로 커밋인지 롤백인지, 어떤 스레드인지 출력하는 코드를 추가하세요. 실무의 TRACE 로그를 흉내 내는 문제입니다.
private static void finish(Ctx ctx, Throwable cause, Class<? extends Throwable> rollbackFor) {
boolean matches = cause != null && rollbackFor.isInstance(cause);
System.out.println("[tx] thread=" + Thread.currentThread().getName()
+ " action=" + (matches || ctx.rollbackOnly ? "ROLLBACK" : "COMMIT"));
// 이후 기존 로직 그대로
...
}실제 Spring 은 TransactionInterceptor 레벨에서 이런 로그를 TRACE 로 남깁니다.