티스토리 뷰
개발하고 있는 내용 중에 @Transactional을 중첩해서 사용하는 경우가 있어서 이 경우에 대해 실험을 해보았다.
실험을 하면서 알게 된 사실들이 있는데, 이 사실들을 먼저 알고 보면 도움이 될 것 같아 미리 적어둔다.
1. CheckedException은 예외 발생시 롤백하지 않는다.
2. 트랜잭션 전파(propagation)의 기본 속성은 REQUIRED다. 이 속성은 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
3. 동일한 클래스 내에서 @Transactional이 아닌 메서드에서 @Transactional 메서드를 호출하면 트랜잭션이 적용되지 않는다.
3. 트랜잭션 전파의 속성 중 REQUIRES_NEW는 동일한 클래스의 메서드들끼리 호출하면 작동하지 않고, 반드시 다른 클래스 메소드를 호출해야 한다.
Case#1 자식 메서드에서 RuntimeException, 부모 메서드에서 처리 X
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = childService.childMethod();
return result == null? "FAIL" : result;
}
// 다른 클래스의 자식메서드
@Transactional
public String childMethod() {
testRepository.save(new Post(2L, null));
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
결과적으로 두 Post 모두 저장되지 않는다.
Case#2 자식 메서드에서 RuntimeException, 부모 메서드에서 처리 O
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = null;
try {
result = childService.childMethod();
} catch (RuntimeException e) {
log.error("parentMethod 예외 발생");
}
return result == null? "FAIL" : result;
}
// 자식메서드
@Transactional
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
결과적으로 두 Post 모두 저장되지 않는다.
Case#3 자식메서드에 REQUIRES_NEW, RuntimeException 던지고 부모메서드에서 처리 X
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = childService.childMethod();
return result == null? "FAIL" : result;
}
// 자식메서드
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
REQUIRES_NEW는 항상 새로운 트랜잭션을 시작한다는 의미이다. 결과적으로 두 Post 모두 저장되지 않는다.
이때, parentMethod와 childMethod가 서로 독립적인 트랜잭션이기 때문에 childMethod에서 RuntimeException이 발생하여 childMethod는 저장되지 않더라도 parentMethod의 Post가 저장되지 않은 것은 의아할 수 있다.
이것은 parentMethod에서 예외처리를 해주지 않았기 때문이다. 예외가 발생했을 때 처리해주지 않으면 콜스택을 하나씩 제거하면서 최초 호출한 곳까지 예외가 전파되기 때문이다.(출처)
Case#4 자식메서드에 REQUIRES_NEW, RuntimeException 던지고 부모메서드에서 처리 O
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = null;
try {
result = childService.childMethod();
} catch (RuntimeException e) {
log.error("parentMethod 예외 발생");
}
return result == null? "FAIL" : result;
}
// 자식메서드
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
결과적으로 위와 같은 상황일 때 parentMethod의 Post는 저장되지만 childMethod의 Post는 저장되지 않는다. 위에서도 말했듯 이렇게 하려면 parentMethod와 childMethod가 서로 다른 클래스에 있어야 한다.(@Transactional의 기본 원리는 AOP이기 때문에 그렇다고 한다. 자세한 내용은 검색을.) REQUIRES_NEW와 마찬가지로 비슷한 역할을 하는 NESTED도 다른 클래스에 있어야 작동하는걸로 테스트되었다.
Case#5 자식메서드에 REQUIRES_NEW, 부모메서드에서 RuntimeException 던지기
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = childService.childMethod();
throw new RuntimeException();
}
// 자식메서드
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
return "SUCCESS";
}
결과적으로 위와 같은 상황일 때 parentMethod의 Post는 저장되지 않지만 childMethod의 Post는 저장된다.
추가로 자식메서드에 @Transactional이 존재하지 않을 경우는 어떤지 테스트해보았다.
Case#6 자식메서드에 @Transactional이 존재하지 않고, RuntimeException을 던지는데 부모메서드에서 처리 X
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = childService.childMethod();
return result;
}
// 자식메서드
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
결과적으로 두 Post 모두 저장되지 않는다.
Case#7 자식메서드에 @Transactional이 존재하지 않고, RuntimeException을 던지는데 부모메서드에서 처리 O
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = null;
try {
result = childService.childMethod();
} catch (RuntimeException e) {
log.error("parentMethod 예외 발생");
}
return result == null ? "FAIL" : "SUCCESS";
}
// 자식메서드
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
throw new RuntimeException();
}
결과적으로 두 Post 모두 저장된다.
Case#8 자식메서드에 @Transactional이 존재하지 않고, 부모메서드에서 RuntimeException 던지기
// 부모메서드
@Transactional
public String parentMethod() {
testRepository.save(new Post(1L, "테스트1"));
log.info("parentMethod={}", testRepository.count());
String result = childService.childMethod();
throw new RuntimeException();
}
// 자식메서드
public String childMethod() {
testRepository.save(new Post(2L, null));
List<Post> posts = testRepository.findAll();
log.info("childMethod={}", testRepository.count());
return "SUCCESS";
}
결과적으로 두 Post 모두 저장되지 않는다.
더 알아보려면 트랜잭션 전파, 트랜잭션 propagation으로 검색해보자. 또, 이와 관련해서 우아한 형제들 기술블로그에 올라온 글을 봤다.(응? 이게 왜 롤백되는거지?) 내가 개발중인 사례와 같아서 참고삼아 봐야겠다.
'공부흔적 > 스프링' 카테고리의 다른 글
Transaction 관련 내부 코드 뜯어보기 (0) | 2022.08.02 |
---|---|
파라미터 유효성 체크를 편하게 해보자 (0) | 2022.06.27 |
DispatcherServlet 뜯어보기 (0) | 2022.03.20 |
체크박스 객체로 받기 (0) | 2021.09.28 |
Spring mvc interceptor (0) | 2021.05.18 |