트랜젝션 매니저의 마지막 영상입니다. 앞선 영상과 이어지는 @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 의 위치를 바꿈)
- REQUIRED (기본값):
- 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."
- SUPPORTS:
- MANDATORY / NEVER 두 개 - 트랜젝션이 있거나 없어야만 합니다. 그 외에는 오류.
- MANDATORY:
트랜젝션이 꼭 필요합니다. 현재 진행중인 트랜젝션이 없으면 오류를 일으킵니다.
문서: "Support a current transaction, throw an exception if none exists." - NEVER:
트랜젝션이 없어야 합니다. 현재 진행중인 트랜젝션이 있으면 오류를 일으킵니다.
문서: "Execute non-transactionally, throw an exception if a transaction exists."
- MANDATORY:
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
별 거 아닌데 열심히 정리해서 시간 아슬아슬하이? 거기다 오늘은 좀 느긋하게 딴 짓도 하면서 해서 말이죠.
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 8 remove cascade / soft delete - 패스트캠퍼스 챌린지 37일차 (0) | 2022.03.01 |
---|---|
JPA Ch 8 영속성 전이 (Cascade) - 패스트캠퍼스 챌린지 36일차 (0) | 2022.02.28 |
JPA Ch 7 영속성 - 트랜젝션 매니저 (5) - 패스트캠퍼스 챌린지 34일차 (0) | 2022.02.26 |
JPA Ch 7 영속성 - 트랜젝션 매니저 (3~4) - 패스트캠퍼스 챌린지 33일차 (0) | 2022.02.25 |
JPA Ch 7 영속성 - 트랜젝션 - 패스트캠퍼스 챌린지 32일차 (0) | 2022.02.24 |