[Spring]트랜잭션 예외복구 시 주의사항
2개 이상의 트랜잭션을 1개의 트랜잭션으로 수행 시, 어느 한 개의 트랜잭션에서 롤백이 발생하면 모든 트랜잭션이 rollback 되는 것이 스프링 트랜잭션의 대원칙이다. 그런데 경우에 따라 어느 트랜잭션에서는 try… catch로 예외를 처리할 수 있다. 이 경우 개발자들은 대개 예외 처리한 트랜잭션에서는 rollback 되더라도, 다른 트랜잭션은 commit 될 것이라고 생각한다. 하지만 앞서 언급한대로 어느 하나의 트랜잭션에서 롤백이 발생하면 전체 트랜잭션은 롤백되는 것이 기본이다. 이를 해결하기 위한 가장 손쉬운 방법은 try…catch로 예외처리 할 트랜잭션을 신규 트랜잭션으로 분리하는 것이다. 이럴경우 기존 트랜잭션과 별도 commit, rollback이 이뤄지므로, 기존 트랜잭션에는 영향을 주지 않는다. 전파옵션은 REQUIRES_NEW
를 사용한다.
아래 예제를 살펴보자.
@Entity @Getter @Setter public class Member { @Id @GeneratedValue private Long id; private String username; public Member() { } public Member(String username) { this.username = username; } } @Entity @Getter @Setter public class Log { @Id @GeneratedValue private Long id; private String message; public Log() { } public Log(String message) { this.message = message; } } @Slf4j @Repository @RequiredArgsConstructor public class MemberRepository { private final EntityManager em; @Transactional public void save(Member member) { log.info("member 저장"); em.persist(member); } public Optional<Member> find(String username) { return em.createQuery("select m from Member m where m.username = :username", Member.class) .setParameter("username", username) .getResultList().stream() .findAny(); } } @Slf4j @Repository @RequiredArgsConstructor public class LogRepository { private final EntityManager em; @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Log message) { log.info("log 저장"); em.persist(message); if(message.getMessage().contains("로그예외")) { log.info("log 저장 시 예외 발생"); throw new RuntimeException("예외발생"); } } public Optional<Log> find(String logMessage) { return em.createQuery("select l from Log l where l.message = :message", Log.class) .setParameter("message", logMessage) .getResultList().stream() .findAny(); } } @Slf4j @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; private final LogRepository logRepository; @Transactional public void joinV2(String username) { Member member = new Member(username); Log logMessage = new Log(username); log.info("====================== memberRepository 호출 시작"); memberRepository.save(member); log.info("====================== memberRepository 호출 종료"); log.info("====================== logRepository 호출 시작"); try { logRepository.save(logMessage); } catch (RuntimeException e) { log.info("log 저장에 실패했습니다. logMessage = {}", logMessage); log.info("정상 흐름 반환"); } log.info("====================== logRepository 호출 종료"); } }
2개의 엔터티 Member, Log가 있다. 이를 저장하는 2개의 Repository도 있다.
2개의 엔터티를 DB에 저장하는 MemberService.joinV2()
메서드가 있다. 여기서 Log 저장에 실패하여도 전체 트랜잭션은 commit 하고 싶은 경우 LogRepository.save()
트랜잭션의 전파옵션에 REQUIRES_NEW
를 사용하면 된다.
@Slf4j @SpringBootTest class MemberServiceTest { @Autowired private MemberService memberService; @Autowired private MemberRepository memberRepository; @Autowired private LogRepository logRepository; @Test @DisplayName("memberService, memberRepository, logRepository 모두 tx가 있는 경우, 예외가 발생하는 경우") void recoverExceptionAndSuccess() { // given String username = "로그예외 test3"; // when memberService.joinV2(username); // then assertThat(memberRepository.find(username).isPresent()).isTrue(); assertThat(logRepository.find(username).isPresent()).isFalse(); } }
만약 LogRepository.save()
의 전파옵션 REQUIRES_NEW
를 제거하고 위 테스트 코드를 실행해보면 테스트가 실패하는 것을 알 수 있다. 또한 UnexpectedRollbackException.class
가 발생함을 알 수 있다. 이는 2개 이상의 트랜잭션이 1개의 트랜잭션으로 묶인 경우 논리 트랜잭션은 rollback이 발생하면 트랜잭션 동기화 매니저의 rollbackOnly=true
로 플래그 설정을 하기 때문이다. 마지막 단계의 물리트랜잭션에서 커밋을 하기전 위 rollbackOnley 플래그값을 참조하여 해당 값이 true인 경우 모든 트랜잭션을 rollback 한다. 그러므로 특정 트랜잭션에 try…catch하여 예외를 처리했다고, 해당 트랜잭션만 rollback 되고 전체 트랜잭션이 commit 되지 않음에 유의한다. 특정 트랜잭션의 commit/rollback에 상관없이 전체트랜잭션을 commit 처리하고 싶다면 해당 트랜잭션의 전파옵션에 REQUIRES_NEW
를 써주면 된다. 물론 예외처리(try… catch)를 해주는 것도 잊지 말아야 한다.
최신 댓글