본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 7 트랜젝션 매니저의 propagation - 패스트캠퍼스 챌린지 35일차

인증샷이 천편일률적이긴 한데요, 이 점은 이해를 부탁드립니다. 공부를 안 하고도 하는척 하는 뻥카를 못 치게 하려는 "인증샷" 이 아니었다면 굳이 인증샷을 올리진 않았을 거에요. 그 덕분에 공부도 열심히 할 수 밖에 없었구요. 보통이라면 필기는 안 하고 영상만 볼수도 있는데도...

트랜젝션 매니저의 마지막 영상입니다. 앞선 영상과 이어지는 @Transactional (Spring JPA) 의 두 가지 프로퍼티 알아보기 중 두 번째인 propagation 을 살펴보는 시간입니다. 다만 propagation 은 바꾸는 일이 정말 필요할 때 외에는 잘 없는 모양이라서, 이렇게 동작한다만 이해하면 될 것 같습니다.

@Transactional 의 Propagation

Spring JPA의 @Transactional 의 필드인 propagation 은 enum Propagation 이며, 7가지 종류의 값이 있습니다.

기본값은 REQUIRED 입니다.

강의 흐름에서 3종류로 묶였습니다. 묶어놓는 게 이해하기도 편하잖아요?

공식 문서도 이참에 같이 읽어봅시다.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html

문서에서 과거에 지배적이었던 (걸로 알고 있는) EJB 프레임워크에 대한 언급이 많은데 저흰 알 바 아니죠 (한국인의멋과얼이담긴케장콘)

  • REQUIRED 계열 3종 - 트랜젝션을 전파하거나 중첩시킵니다. (개별로 다뤘음)
    • REQUIRED (기본값):
      트랜젝션이 없으면 만들고, 트랜젝션이 이미 있으면 그걸 사용합니다.
      문서: "Support a current transaction, create a new one if none exists."
    • REQUIRED_NEW:
      트랜젝션을 반드시 만듭니다. 호출자가 이미 트랜젝션을 만들었는지 여부에 관계없이 말이죠.
      (지원하지 않을 수 있음)

      문서: "Create a new transaction, and suspend the current transaction if one exists."
    • NESTED:
      중첩된 트랜젝션을 만들어서 실행합니다. 피호출자의 트랜젝션이 호출자의 트랜젝션에 종속된 형태입니다.
      (지원하지 않을 수 있음)
      문서: "Execute within a nested transaction if a current transaction exists, otherwise behave like REQUIRED."
      (헷갈려서 otherwise 의 위치를 바꿈)
  • SUPPORTS / NOT_SUPPORTED 두 개 - 지원 여부만 지정
    • SUPPORTS:
      트랜젝션이 없으면 없는대로 실행하고, 있으면 그걸 사용합니다.
      문서: "Support a current transaction, execute non-transactionally if none exists."
    • NOT_SUPPORTED:
      트랜젝션이 없으면 없는대로 실행하고, 있으면 현재 트랜젝션을 일시정지하고(suspend) 트랜젝션이 없는 것처럼 실행합니다.
      문서: "Execute non-transactionally, suspend the current transaction if one exists."
  • MANDATORY / NEVER 두 개 - 트랜젝션이 있거나 없어야만 합니다. 그 외에는 오류.
    • MANDATORY:
      트랜젝션이 꼭 필요합니다. 현재 진행중인 트랜젝션이 없으면 오류를 일으킵니다.
      문서: "Support a current transaction, throw an exception if none exists."
    • NEVER:
      트랜젝션이 없어야 합니다. 현재 진행중인 트랜젝션이 있으면 오류를 일으킵니다.
      문서: "Execute non-transactionally, throw an exception if a transaction exists."

REQUIRED, REQUIRED_NEW, NESTED 알아보기

REQUIRED 계열 - 테스트 코드 셋업

강의에서는 앞의 세 개만 코드로 자세히 알아보고, 나머지 네 개는 테스트 코드로 확인해보지 않았습니다. 공통적인 테스트코드 세팅과 표로서 한 번에 확인이 가능하므로 그렇게 정리해보겠습니다.

이전 강의에서 썼던 BookService 의 putBookAndAuthor 에서 author 생성 부분을 별도 서비스로 빼서 별도 트랜젝션으로 만들어서 테스트할 것입니다.

두 가지 케이스를 봐야합니다.

  • 실험 1:
    • 내부 트랜젝션인 AuthorService 에서 throw를 해서
    • 외부 트랜젝션인 BookService 가 catch 하는 경우
  • 실험 2
    • 내부 트랜젝션인 AuthorService 는 정상적으로 실행되고
    • 외부 트랜젝션인 BookService 가 throw 하는 경우

첫 번째 코드는 외부 트랜젝션인 BookService 입니다. 기존의 코드를 리팩터해서 사용합니다. 내부 트랜젝션에서 RuntimeException 을 던졌을 때 롤백이 되는지 안 되는지를 보는 게 중요하므로, 외부 트랜젝션에서 예외를 트라이캐치 해줍니다.

@Service
@RequiredArgsConstructor
public class BookService {
    private final BookRepo bookRepo;
    private final AuthorRepo authorRepo;
    private final AuthorService authorService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void putBookAndAuthor(boolean throwOnAuthor) {
        Book book = new Book();
        book.setName("JPA 시작하기");

        bookRepo.save(book);

        // 실험 1: 내부 트랜젝션이 throw
        if (throwOnAuthor) {
            try {
                authorService.putAuthor(true); // exception
            } catch (Exception ex) {
                System.out.println(ex);
            }
        }
        // 실험 2: 외부 트랜젝션이 throw
        else {
            authorService.putAuthor(false); // no exception
            throw new RuntimeException();
        }
    }
    생략

내부 트랜젝션이 될 AuthorService 에서는 throw 를 해줘야 하구요. 내부 트랜젝션의 propagation 을 바꿔가며 확인해볼 겁니다.

@Service
@RequiredArgsConstructor
public class AuthorService {
    private final AuthorRepo authorRepo;

    @Transactional(propagation = Propagation.REQUIRED)
    public void putAuthor() {
        Author author = new Author();
        author.setName("marting");
        authorRepo.save(author);

        if (throwOnAuthor) {
            // 실험 1: 내부 트랜젝션이 throw
            throw new RuntimeException("asdf");
        }
        // 실험 2: 외부 트랜젝션이 throw
    }
}

테스트 확인은 트랜젝션이 다 끝난 바깥쪽에서 해줘야겠죠? 이전과 거의 동일합니다.

@SpringBootTest
class BookServiceTest {
    @Autowired
    private BookService bookService;
    @Autowired
    private BookRepo bookRepo;
    @Autowired
    private AuthorRepo authorRepo;

    @Test
    void transactionTestInnerException() {
        try {
            // 몇몇 상황에서 UnexpectedRollbackException 발생
            bookService.putBookAndAuthor(true);
        } catch (Exception ex) {
            System.out.println(ex);
        }
        System.out.println("books: " + bookRepo.findAll());
        System.out.println("autho: " + authorRepo.findAll());
    }

    @Test
    void transactionTestOuterException() {
        try {
            bookService.putBookAndAuthor(false);
        } catch (Exception ex) {
            System.out.println(ex);
        }
        System.out.println("books: " + bookRepo.findAll());
        System.out.println("autho: " + authorRepo.findAll());
    }

    생략

실행해보기

이제 표로 비교만 해보면 됩니다. 외부 트랜젝션은 REQUIRED 로 고정되어 있습니다. (뭐든 트랜젝션만 만들어준다면 상관 없죠.)

하는김에 전부... 사실 뒤의 네 가지는 실험 세팅이 적합하지 않습니다. 이 세팅에서는 외부에 트랜젝션이 있다고 강제하고 있으니까요. 참고삼아 같이 돌려봤습니다.

내부 트랜젝션의
propagation
(AuthorService)
실험 1:
내부 트랜젝션이 throw,
외부 트랜젝션이 catch
실험 2:
외부 트랜젝션이 throw,
테스트 코드가 catch
비고
Author 저장
(내부)
Book 저장
(외부)
Author 저장
(내부)
Book 저장
(외부)
REQUIRED / REQUIRED_NEW / NESTED
REQUIRED X X X X 하나의 트랜젝션을 사용하므로
어디서 예외가 터지던 둘 다 롤백
(외부에서 catch 필요)
REQUIRED_NEW X O O X 서로 다른 두 개의 트랜젝션이므로
터진 쪽 스스로에만 영향

NESTED X O X X 중첩된 트랜젝션이므로
바깥께 터지면 둘 다 롤백이지만
안쪽 트랜젝션만 오류면 걔만 롤백
SUPPORTS / NOT_SUPPORTED
SUPPORTS X X X X 바깥에 트랜젝션이 있으므로
그걸 사용합니다.
REQUIRED랑 같은 상황이네요
NOT_SUPPORTED O O O X 내부 트랜젝션인 Author 쪽은
트랜젝션이 없는 것처럼 동작하므로
어찌됐든 저장됩니다.
MANDATORY / NEVER
MANDATORY X X X X 바깥에 트랜젝션이 있으므로
그걸 사용합니다. 없으면 예외였겠죠
REQUIRED랑 같은 상황이네요
NEVER X O X X 바깥의 트랜젝션은 돌아가고,
내부에서 생긴 NEVER의 예외는
catch 되었습니다.

 

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

별 거 아닌데 열심히 정리해서 시간 아슬아슬하이? 거기다 오늘은 좀 느긋하게 딴 짓도 하면서 해서 말이죠.