롤백 규칙
밖으로 빠져나온 예외에 적용되는 기본 규칙과 rollbackFor·noRollbackFor의 실제 커밋 결과를 검증합니다.
이 글의 일반 동기식 메서드와 기본 설정에서는, 밖으로 빠져나온 RuntimeException과 Error에는 롤백하고, 밖으로 빠져나온 검사 예외에는 커밋을 시도합니다. 이 예제는 전역 기본 롤백 정책을 바꾸지 않으며, 이미 롤백 전용으로 표시되었거나 커밋 단계 자체가 실패한 경우까지 성공을 보장한다는 뜻은 아닙니다. 이 기본값은 Java의 예외 분류 관습일 뿐 업무의 불변식 자체가 아니므로, 어떤 데이터가 함께 남아야 하는지 먼저 정한 뒤 rollbackFor나 noRollbackFor로 정책을 좁혀야 합니다.
밖으로 나온 예외와 가장 구체적인 규칙이 완료 결정을 만든다
밖으로 나온 예외의 명시 규칙과 기본 규칙을 판정해 commit 시도 또는 rollback으로 갈라지는 순서가 있다.
EXCEPTION ESCAPE · STRONGEST RULE · COMPLETION
밖으로 나온 예외에 가장 구체적인 규칙을 적용한다
이 교재의 동기식 호출은 Spring 기본 롤백 설정을 사용합니다. 정상 반환과 예외 전달을 먼저 구분하고 예외에는 명시 규칙을 우선합니다.
- 정상 반환인가, 예외가 빠져나왔는가
내부에서 잡아 정상 값을 반환한 예외는 인터셉터에 도달하지 않습니다. 정상 반환은 커밋을 시도합니다.
- 예외와 일치하는 명시 규칙을 확인
rollbackFor / noRollbackFor 가운데 실제 타입에 가장 가까운 규칙이 이깁니다.
- 일치 규칙이 없을 때 기본값 적용
RuntimeException·Error는 롤백, 검사 예외는 커밋 시도입니다. 예외 전달 여부와 완료 결정은 별개입니다.
명시 규칙 · rollbackFor
검사 예외도 함께 롤백
예: 보고서 생성 실패와 현재 변경을 함께 무효화해야 할 때.
명시 규칙 · noRollbackFor
런타임 예외여도 커밋 시도
예: 핵심 상태가 유효한 알림 실패. 구체 규칙이 넓은 Exception 규칙보다 우선합니다.
기본값 · RuntimeException / Error
경계 밖으로 나오면 롤백
대상 메서드 안에서 처리한 예외에는 이 기본 판단을 적용하지 않습니다.
기본값 · 검사 예외 / 정상 반환
커밋 시도
이미 rollback-only인 참여 경계나 커밋 단계 오류까지 성공을 보장하는 뜻은 아닙니다.
규칙 입력과 최종 DB 행을 함께 검증합니다. 완료된 커밋 뒤 외부 전달 실패는 별도 전달 구조로 처리합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원래 일반 동기식 메서드와 기본 설정의 판단 순서다. 밖으로 예외가 없으면 commit을 시도하고, 있으면 가장 구체적인 명시 타입 규칙을 선택한다. rollbackFor면 rollback, noRollbackFor면 commit 시도다. 일치 규칙이 없으면 RuntimeException/Error에 rollback, checked에 commit을 시도한다. 이미 rollback-only인 공유 경계나 commit 실패까지 성공을 보장하지 않는다. 두 배치의 7노드와 6분기 의미가 같다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 밖으로 나온 예외와 가장 구체적인 규칙이 완료 결정을 만든다 |
| 6 | 원래 일반 동기식 메서드와 기본 설정의 판단 순서다. 밖으로 예외가 없으면 commit을 시도하고, 있으면 가장 구체적인 명시 타입 규칙을 선택한다. rollbackFor면 rollback, noRollbackFor면 commit 시도다. 일치 규칙이 없으면 RuntimeException/Error에 rollback, checked에 commit을 시도한다. 이미 rollback-only인 공유 경계나 commit 실패까지 성공을 보장하지 않는다. 두 배치의 7노드와 6분기 의미가 같다. |
| 10 | 가장 구체적인 타입 규칙 |
| 15 | 아니요 |
| 34 | 예: 검사 예외로 표현한 보고서 생성 실패와 현재 변경을 함께 무효화해야 할 때. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 밖으로 나온 결과 일반 동기 메서드 밖으로 예외가 나왔는지 확인합니다. | 예외 없음: commit 시도 · 예외 있음: 규칙 탐색 | 정상·예외 결과가 완료 판단 입력이 됩니다. |
| 가장 구체적 명시 규칙 일치하는 명시 타입 규칙을 선택합니다. | rollbackFor: rollback · noRollbackFor: commit 시도 | 구체적인 규칙이 기본 분류에 우선합니다. |
| 미일치 기본 규칙 명시 규칙이 없으면 RuntimeException·Error는 rollback이고 checked는 commit 시도입니다. | 기본: runtime rollback·checked commit 시도 · 완료 성공: 보장 아님 | 예외 타입에 따라 완료 시도가 갈립니다. |
밖으로 나온 예외와 가장 구체적인 규칙이 완료 결정을 만든다
밖으로 나온 예외의 명시 규칙과 기본 규칙을 판정해 commit 시도 또는 rollback으로 갈라지는 순서가 있다.
EXCEPTION ESCAPE · STRONGEST RULE · COMPLETION
밖으로 나온 예외에 가장 구체적인 규칙을 적용한다
이 교재의 동기식 호출은 Spring 기본 롤백 설정을 사용합니다. 정상 반환과 예외 전달을 먼저 구분하고 예외에는 명시 규칙을 우선합니다.
- 정상 반환인가, 예외가 빠져나왔는가
내부에서 잡아 정상 값을 반환한 예외는 인터셉터에 도달하지 않습니다. 정상 반환은 커밋을 시도합니다.
- 예외와 일치하는 명시 규칙을 확인
rollbackFor / noRollbackFor 가운데 실제 타입에 가장 가까운 규칙이 이깁니다.
- 일치 규칙이 없을 때 기본값 적용
RuntimeException·Error는 롤백, 검사 예외는 커밋 시도입니다. 예외 전달 여부와 완료 결정은 별개입니다.
명시 규칙 · rollbackFor
검사 예외도 함께 롤백
예: 보고서 생성 실패와 현재 변경을 함께 무효화해야 할 때.
명시 규칙 · noRollbackFor
런타임 예외여도 커밋 시도
예: 핵심 상태가 유효한 알림 실패. 구체 규칙이 넓은 Exception 규칙보다 우선합니다.
기본값 · RuntimeException / Error
경계 밖으로 나오면 롤백
대상 메서드 안에서 처리한 예외에는 이 기본 판단을 적용하지 않습니다.
기본값 · 검사 예외 / 정상 반환
커밋 시도
이미 rollback-only인 참여 경계나 커밋 단계 오류까지 성공을 보장하는 뜻은 아닙니다.
규칙 입력과 최종 DB 행을 함께 검증합니다. 완료된 커밋 뒤 외부 전달 실패는 별도 전달 구조로 처리합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 원래 일반 동기식 메서드와 기본 설정의 판단 순서다. 밖으로 예외가 없으면 commit을 시도하고, 있으면 가장 구체적인 명시 타입 규칙을 선택한다. rollbackFor면 rollback, noRollbackFor면 commit 시도다. 일치 규칙이 없으면 RuntimeException/Error에 rollback, checked에 commit을 시도한다. 이미 rollback-only인 공유 경계나 commit 실패까지 성공을 보장하지 않는다. 두 배치의 7노드와 6분기 의미가 같다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 밖으로 나온 예외와 가장 구체적인 규칙이 완료 결정을 만든다 |
| 6 | 원래 일반 동기식 메서드와 기본 설정의 판단 순서다. 밖으로 예외가 없으면 commit을 시도하고, 있으면 가장 구체적인 명시 타입 규칙을 선택한다. rollbackFor면 rollback, noRollbackFor면 commit 시도다. 일치 규칙이 없으면 RuntimeException/Error에 rollback, checked에 commit을 시도한다. 이미 rollback-only인 공유 경계나 commit 실패까지 성공을 보장하지 않는다. 두 배치의 7노드와 6분기 의미가 같다. |
| 10 | 가장 구체적인 타입 규칙 |
| 15 | 아니요 |
| 34 | 예: 검사 예외로 표현한 보고서 생성 실패와 현재 변경을 함께 무효화해야 할 때. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 밖으로 나온 결과 일반 동기 메서드 밖으로 예외가 나왔는지 확인합니다. | 예외 없음: commit 시도 · 예외 있음: 규칙 탐색 | 정상·예외 결과가 완료 판단 입력이 됩니다. |
| 가장 구체적 명시 규칙 일치하는 명시 타입 규칙을 선택합니다. | rollbackFor: rollback · noRollbackFor: commit 시도 | 구체적인 규칙이 기본 분류에 우선합니다. |
| 미일치 기본 규칙 명시 규칙이 없으면 RuntimeException·Error는 rollback이고 checked는 commit 시도입니다. | 기본: runtime rollback·checked commit 시도 · 완료 성공: 보장 아님 | 예외 타입에 따라 완료 시도가 갈립니다. |
그림은 일반 동기식 메서드에서 롤백을 판정하는 순서를 보여 줍니다. 밖으로 예외가 나오지 않았다면 커밋을 시도하고, 나왔다면 일치하는 명시 규칙 가운데 실제 타입에 가장 가까운 것을 고릅니다. rollbackFor면 롤백, noRollbackFor면 커밋을 시도하며, 일치하는 규칙이 없다면 RuntimeException과 Error는 롤백, 검사 예외는 커밋을 시도합니다. 이미 롤백 전용인 공유 트랜잭션이나 커밋의 실패까지 성공을 보장하지는 않습니다.
인터셉터가 본 종료 방식으로 판정합니다
트랜잭션 인터셉터는 대상 호출이 끝날 때 두 가지를 봅니다. 정상적으로 반환했다면 커밋을 시도하고, 예외가 빠져나왔다면 가장 구체적으로 일치하는 롤백 규칙을 적용합니다.
그래서 대상 메서드 안에서 예외를 잡고 정상 값을 돌려주면, 그 예외는 기본 롤백 판정에 닿지 않습니다. 반대로 호출한 쪽에서 예외를 HTTP 응답으로 바꾸는 것은 프록시가 먼저 예외를 보고 트랜잭션을 끝낸 뒤이므로 판정에 영향을 주지 않습니다. 기본 규칙은 다음과 같습니다.
| 밖으로 빠져나온 값 | 기본 완료 |
|---|---|
RuntimeException의 하위 타입 | 롤백 |
Error의 하위 타입 | 롤백 |
| 검사 예외 | 커밋 |
| 정상 반환 | 커밋 시도 |
예외 이름에 Business가 들어 있는지나 메시지에 특정 단어가 있는지는 기본 판정에 쓰이지 않습니다.
상태의 요구를 타입 규칙으로 명시합니다
다음 서비스는 같은 삽입 뒤에 서로 다른 실패를 밖으로 내보냅니다. 일곱 메서드의 차이는 예외가 실제로 빠져나오는 방식과 애노테이션의 규칙뿐입니다.
package board.tx.proxy.rollback;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
public class RollbackPolicyService {
private final JdbcTemplate jdbc;
public RollbackPolicyService(DataSource dataSource) {
this.jdbc = new JdbcTemplate(dataSource);
}
@Transactional
public void defaultChecked(long id) throws ReportExportFailure {
insert(id, "default-checked");
throw new ReportExportFailure();
}
@Transactional
public void defaultRuntime(long id) {
insert(id, "default-runtime");
throw new NotificationFailure();
}
@Transactional
public void defaultError(long id) {
insert(id, "default-error");
throw new AssertionError("broken invariant");
}
@Transactional(rollbackFor = ReportExportFailure.class)
public void rollbackChecked(long id) throws ReportExportFailure {
insert(id, "rollback-checked");
throw new ReportExportFailure();
}
@Transactional(noRollbackFor = NotificationFailure.class)
public void commitRuntime(long id) {
insert(id, "commit-runtime");
throw new NotificationFailure();
}
@Transactional(
rollbackFor = Exception.class,
noRollbackFor = NotificationFailure.class
)
public void specificNoRollbackWins(long id) {
insert(id, "specific-no-rollback");
throw new NotificationFailure();
}
@Transactional
public boolean catchRuntimeAndReturn(long id) {
insert(id, "caught-runtime");
try {
throw new NotificationFailure();
} catch (NotificationFailure handled) {
return false;
}
}
private void insert(long id, String scenario) {
jdbc.update(
"insert into rollback_note(id, scenario) values (?, ?)",
id,
scenario
);
}
public static final class ReportExportFailure extends Exception {
public ReportExportFailure() {
super("report export failed");
}
}
public static final class NotificationFailure extends RuntimeException {
public NotificationFailure() {
super("notification failed");
}
}
}rollbackFor는 검사 예외가 나도 지금의 데이터베이스 변경을 롤백해야 한다는 뜻입니다. noRollbackFor는 런타임 예외가 빠져나와도 지금의 데이터베이스 상태가 올바르므로 커밋하겠다는 강한 정책입니다. specificNoRollbackWins()에서는 Exception 규칙과 NotificationFailure 규칙이 모두 맞지만, 실제 타입에 더 가까운 noRollbackFor가 선택됩니다. 이것이 외부 시스템과의 원자성을 만들어 주는 것은 아닙니다.
예외와 행의 결과를 함께 테스트합니다
다음 테스트는 예외가 호출한 쪽에 전달되는지와 최종 행 수를 모두 고정합니다. 메서드가 실패했다는 것만으로는 커밋과 롤백을 구분할 수 없습니다.
package board.tx.proxy.rollback;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import javax.sql.DataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
class RollbackPolicyTest {
private AnnotationConfigApplicationContext context;
private JdbcTemplate jdbc;
private RollbackPolicyService service;
@BeforeEach
void openContext() {
context = new AnnotationConfigApplicationContext(RollbackConfig.class);
var dataSource = context.getBean(DataSource.class);
jdbc = new JdbcTemplate(dataSource);
jdbc.execute("create table if not exists rollback_note(id bigint primary key, scenario varchar(80) not null)");
jdbc.update("delete from rollback_note");
service = context.getBean(RollbackPolicyService.class);
}
@AfterEach
void closeContext() {
context.close();
}
@Test
void framework_default_is_runtime_and_error_rollback_checked_commit() {
var rule = new DefaultTransactionAttribute();
assertFalse(rule.rollbackOn(new RollbackPolicyService.ReportExportFailure()));
assertTrue(rule.rollbackOn(new RollbackPolicyService.NotificationFailure()));
assertTrue(rule.rollbackOn(new AssertionError("broken invariant")));
}
@Test
void escaping_checked_exception_commits_by_default() {
assertThrows(
RollbackPolicyService.ReportExportFailure.class,
() -> service.defaultChecked(11L)
);
assertEquals(1, rows());
}
@Test
void escaping_runtime_exception_rolls_back_by_default() {
assertThrows(
RollbackPolicyService.NotificationFailure.class,
() -> service.defaultRuntime(12L)
);
assertEquals(0, rows());
}
@Test
void escaping_error_rolls_back_through_the_transaction_proxy() {
assertTrue(AopUtils.isAopProxy(service));
assertThrows(
AssertionError.class,
() -> service.defaultError(16L)
);
assertEquals(0, rows());
}
@Test
void rollback_for_rolls_back_the_named_checked_exception() {
assertThrows(
RollbackPolicyService.ReportExportFailure.class,
() -> service.rollbackChecked(13L)
);
assertEquals(0, rows());
}
@Test
void no_rollback_for_commits_the_named_runtime_exception() {
assertThrows(
RollbackPolicyService.NotificationFailure.class,
() -> service.commitRuntime(14L)
);
assertEquals(1, rows());
}
@Test
void specific_no_rollback_rule_beats_the_matching_broad_rollback_rule() {
assertThrows(
RollbackPolicyService.NotificationFailure.class,
() -> service.specificNoRollbackWins(17L)
);
assertEquals(1, rows());
}
@Test
void caught_exception_becomes_normal_return_to_the_interceptor() {
assertFalse(service.catchRuntimeAndReturn(15L));
assertEquals(1, rows());
}
private int rows() {
return jdbc.queryForObject(
"select count(*) from rollback_note",
Integer.class
);
}
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
static class RollbackConfig {
@Bean
DataSource dataSource() {
var source = new JdbcDataSource();
source.setURL("jdbc:h2:mem:b92_proxy_rollback;DB_CLOSE_DELAY=-1");
source.setUser("sa");
source.setPassword("");
return source;
}
@Bean
PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
RollbackPolicyService rollbackPolicyService(DataSource dataSource) {
return new RollbackPolicyService(dataSource);
}
}
}검사 예외 테스트에서도 예외는 호출한 쪽에 그대로 전달됩니다. 차이는 전달되는지가 아니라 트랜잭션을 어떻게 끝내느냐입니다.
규칙은 좁고 구체적으로 둡니다
rollbackFor = Exception.class처럼 넓은 규칙은 프로그래밍 결함과 모든 검사 예외를 한 정책으로 묶습니다. rollbackForClassName 같은 문자열 규칙은 타입이 클래스 경로에 없을 때만 필요하며, 오타와 이름 충돌을 컴파일러가 잡지 못합니다.
컴파일되는 구체적인 타입을 먼저 쓰고, 한 메서드에 여러 규칙이 겹친다면 실제 예외 타입마다 행의 결과를 테스트합니다. 예외의 상속 구조를 바꾸면서 트랜잭션 정책이 몰래 바뀌지 않도록, 정책표와 테스트를 함께 고칩니다.
| 상황 | 예외가 밖으로 나가는가 | 의도한 DB 결과 | 표현 |
|---|---|---|---|
| 낙관적 충돌 | 예 | 롤백 | 런타임 기본값 |
| 검사 예외로 나타낸 보고서 생성 실패 | 예 | 함께 롤백 | 구체적인 rollbackFor |
| 핵심 상태는 올바른 알림 실패 | 예 | 커밋 | 구체적인 noRollbackFor |
| 안에서 회복해 정상 반환 | 아니요 | 커밋 시도 | 회복한 상태를 따로 검증 |
더 깊이: 문자열 규칙의 일치 방식과 6.2의 전역 기본값
문자열 규칙이 위험한 이유는 일치 방식에 있습니다. Spring 레퍼런스의 롤백 규칙에 따르면 rollbackFor처럼 타입으로 정한 규칙은 던져진 예외가 그 타입이거나 하위 타입일 때만 맞습니다. 반면 rollbackForClassName의 패턴은 던져진 예외의 이름에 그 문자열이 들어 있기만 하면 맞습니다. 그래서 "Exception"은 거의 모든 예외와 맞아 다른 규칙을 가리고, "com.example.CustomException"은 같은 패키지의 CustomExceptionV2나 중첩 클래스 CustomException$AnotherException과도 맞습니다. 여러 규칙이 함께 맞을 때는 가장 강하게 맞는 규칙 하나가 결과를 정합니다.
Spring Framework 6.2부터는 규칙이 없는 예외의 기본 처리 자체를 바꿀 수 있습니다. @EnableTransactionManagement(rollbackOn = RollbackOn.ALL_EXCEPTIONS)로 선언하면, 규칙 기반 트랜잭션은 검사 예외를 포함한 모든 예외에서 롤백합니다. API 문서는 메서드마다 적은 규칙이 이 기본값보다 우선하되, 규칙에 없는 예외에는 고른 기본값이 그대로 적용된다고 설명합니다. 또 커밋하는 EJB 방식의 업무 예외에 기대지 않는다면 이 설정으로 바꾸기를 권하고, 검사 예외를 강제하지 않는 Kotlin 애플리케이션에도 같은 권고를 합니다. 이 설정을 켜면 위 표의 기본 규칙에서 검사 예외의 결과가 롤백으로 바뀌므로, 검사 예외 뒤에도 커밋해야 하는 경우에만 noRollbackFor를 남기면 됩니다.
커밋 뒤의 실패는 롤백 규칙으로 풀지 않습니다
지연된 flush나 제약 조건 때문에, 대상 코드가 정상적으로 반환한 뒤 커밋 단계에서 실패할 수 있습니다. 대상 메서드 안의 try 블록은 프록시가 하는 커밋의 실패를 감싸지 못하므로, 호출한 쪽이 프록시 호출 전체의 실패를 처리해야 합니다.
이미 커밋된 뒤에 실행되는 콜백의 실패는 데이터베이스를 되돌리지 못합니다. 외부 전달이 핵심 저장과 독립적이라면, 로컬 데이터와 전달할 사실을 같은 트랜잭션에 기록하고 비동기로 발행하는 구조를 씁니다. 커밋 뒤에 드러난 프로그래밍 결함은 롤백 규칙으로 숨기지 말고 장애로 기록한 뒤, 필요한 보상을 별도의 작업으로 실행합니다. 그 구조의 원자성과 재시도, 멱등성은 뒤의 전파와 아웃박스 글에서 다룹니다.
정리
- 기본적으로 밖으로 나온
RuntimeException과Error는 롤백되고, 검사 예외와 정상 반환은 커밋을 시도합니다. - 인터셉터는 대상 밖으로 빠져나온 예외만 보므로, 안에서 잡고 정상 반환한 예외는 롤백을 일으키지 않습니다.
rollbackFor와noRollbackFor가 함께 맞으면 실제 타입에 가장 가까운 규칙이 이기며, 규칙은 구체적인 타입으로 좁게 둡니다.- 테스트는 예외가 전달되는지와 최종 행 수를 함께 확인해야 커밋과 롤백을 구분할 수 있습니다.
- 커밋 단계의 실패와 커밋 뒤 콜백의 실패는 롤백 규칙으로 풀리지 않으므로, 호출한 쪽의 처리와 별도의 전달 구조로 다룹니다.
연습 문제
1. 같은 catch가 다른 결과를 만드는 조건
같은 JDBC 관리자를 쓰는 두 호출을 비교합니다. A는 하나의 대상 메서드 안에서 행을 넣고, 일반 도우미가 던진 RuntimeException을 직접 잡은 뒤 false를 정상 반환합니다. 도우미는 프록시가 아니며 다른 실패나 롤백 전용 표시는 없습니다. B는 바깥 REQUIRED가 행을 넣고 별도 빈의 REQUIRED 프록시를 호출하는데, 안쪽의 런타임 실패로 공유 트랜잭션이 롤백 전용이 되었지만 바깥 대상은 예외를 잡고 false를 돌려줍니다. 최종 행과 호출한 쪽이 받는 결과가 각각 어떻게 다른가요?
해설 보기
A에서는 프록시가 정상 반환을 보므로 커밋을 시도합니다. 다른 완료 실패가 없다면 삽입한 행이 남고 false가 반환됩니다. B에서는 안쪽 프록시가 이미 공유 물리 트랜잭션을 롤백 전용으로 표시했습니다. 바깥 대상의 catch는 그 표시를 지우지 않으므로, 바깥이 끝날 때 전체가 롤백되고 UnexpectedRollbackException이 나서 false를 정상 결과로 전달하지 못합니다.
바깥의 noRollbackFor 규칙도 이미 표시된 공유 트랜잭션을 정상 상태로 되돌리지는 못합니다. 예외가 어디서 처리되었는지와 어느 프록시가 먼저 보았는지를 함께 기록해야 합니다.
2. 롤백 규칙을 점검하는 질문
새 서비스의 롤백 규칙을 검토하려고 합니다. 예외의 전달과 행의 결과, 규칙의 범위, 외부 시스템, 커밋 뒤의 실패라는 관점에서 확인할 질문을 다섯 가지로 정리해 보세요.
해설 보기
첫째, 롤백 규칙이 보는 예외가 실제로 프록시 밖으로 빠져나오는지 확인합니다. 둘째, 예외가 전달되는지와 최종 행의 결과를 함께 테스트했는지 봅니다. 셋째, rollbackFor와 noRollbackFor가 넓은 Exception이 아니라 구체적인 업무 상태를 설명하는 타입인지 봅니다. 넷째, 외부 시스템의 실패를 로컬 애노테이션 한 줄로 원자적으로 묶을 수 있다고 오해하지 않았는지 봅니다. 다섯째, 커밋 뒤의 실패를 롤백 규칙이 아니라 아웃박스 같은 별도의 전달 구조로 처리하는지 봅니다.
다음 글 트랜잭션 프록시 우회에서는 애노테이션이 있는데도 호출이 프록시를 지나지 않아 트랜잭션이 적용되지 않는 경우를 살펴봅니다.