티스토리 뷰

공부흔적/스프링

중첩된 @Transactional을 실험해보자

주디 𝙹𝚞𝚍𝚢 2022. 7. 27. 12:32

 개발하고 있는 내용 중에 @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으로 검색해보자. 또, 이와 관련해서 우아한 형제들 기술블로그에 올라온 글을 봤다.(응? 이게 왜 롤백되는거지?) 내가 개발중인 사례와 같아서 참고삼아 봐야겠다.

300x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함