2020. 8. 3. 13:01ㆍJava
프로젝트를 진행하다가 알 수 없는 예외 때문에 고생한 경험을 바탕으로 이번 포스트를 쓴다. 해결하는 데 이 글이 정말 많은 도움이 됐다. 저 글보다 더 자세하고, 전문성 있는 설명을 할 수는 없어서 조금 더 간단하고, 직관적으로 풀어보려고 한다.
이번 포스트에서 사용한 로그 레벨은 다음과 같다.
logging.level.org.springframework.jdbc.core.JdbcTemplate=debug
logging.level.org.springframework.jdbc.core.StatementCreatorUtils=trace
logging.level.org.springframework.transaction=trace
문제의 시작
`@Transactional`이 걸려 있는 메서드 내부에서 `UnexpectedRollbackException`이 터지거나 예외 없이 DB에 값이 저장되지 않는 현상이 발생했다.
Transaction rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
테스트 코드를 모두 짠 코드를 실제 서버에 배포하니 위와 같은 `UnexpectedRollbackException` 예외가 나왔다. 디버그를 해보니 로직 진행은 모두 정상적으로 된다. 로직이 모두 정상적으로 진행되지만 `@Transactional`이 붙은 메서드가 끝남과 동시에 코드가 터지는 문제가 발생해서, 도대체 어떤 부분에서 예외가 나오는지 알 방법이 없었다. 아래는 상황을 비슷하게 재현한 코드다.
// TransactionalParent.java
@AllArgsConstructor
@Service
public class TransactionParent {
private final TransactionChild transactionChild;
@Transactional
public void savePersonAndCatchException() {
try {
transactionChild.savePersonAndThrowException();
} catch (RuntimeException e) {
System.out.println("부모가 예외를 처리");
}
System.out.println("로직이 계속 진행");
}
}
// TranscationalChild.java
@AllArgsConstructor
@Service
public class TransactionChild {
private final PersonRepository personRepository;
@Transactional
public void savePersonAndThrowException() {
personRepository.save(new Person(null, "장효혁", 20));
throw new RuntimeException("자식에게서 예외 발생!");
}
}
// TransactionalParentTest.java
@SpringBootTest
class TransactionParentTest {
@Autowired
private PersonRepository personRepository;
@Autowired
private TransactionParent transactionParent;
@DisplayName("Person 객체가 저장되는지 확인")
@Test
void transaction() {
transactionParent.savePersonAndCatchException();
assertThat(personRepository.findAll()).hasSize(1);
}
}
테스트 코드를 실행하면 위의 예외가 발생해서 테스트를 통과하지 못한다.
이유
1. 부모가 자식의 트랜잭션이 걸려있는 메서드인 `savePersonAndThrowException` 메서드를 호출한다.
2. 자식 메서드의 트랜잭션이 시작된다.
3. Person 객체를 저장하는 로직이 수행된다. 여기까지는 예외 없이 실제 db에 객체가 저장이 된다.
4. 여기서 자식이 unchecked 예외인 Runtime 예외를 발생시킨다.
5. 그리고 이와 동시에 트랜잭션이 끝난다.
6. 트랜잭션이 끝날 때, 예외가 발생했으므로 Rollback에 대한 규칙을 적용한다. 따로 설정한 규칙이 없으므로 기본 규칙인 롤백을 적용한다. 여기서는 롤백을 실행하지 않고, 단순히 Rollback을 하겠다고 표시(mark)만 해둔다. 이는 전역으로 관리되기 때문에 메서드가 끝난다고 사라지지 않는다.
7. 로직이 수행된다. 코드 상으로 로직은 문제 없이 수행된다.
8. 최초로 생성된 트랜잭션이 완료된다. 이 때, commit이 발생하는데(이전 트랜잭션들이 끝날 때는 commit이 발생하지 않는다. 자세한 이유는 관련된 글에서 Propagation.REQUIRED 를 살펴보시면 된다) commit을 수행할 때 아까 마킹해뒀던 Rollback 여부를 체크해서 이 값이 true라면 Rollback이 진행된다.
'Java' 카테고리의 다른 글
[객체지향] 캡슐화 - 객체의 값을 꺼내지 말고 메시지를 던져라 (0) | 2021.01.24 |
---|---|
[Spring Data Jdbc] 코틀린에서 wither를 인식하지 못하는 문제 (0) | 2020.12.06 |
[Jackson] Jackson 파싱 전략(불변 객체 활용) (1) | 2020.07.12 |
[Spring Data JDBC] 라이프사이클 이벤트와 콜백(LifeCycle Events & Callback) (1) | 2020.07.11 |
[WebClient] @RestClientTest를 WebFlux에서 사용하기 (0) | 2020.07.11 |