ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 트랜잭션 전파 - REQUIREDS_NEW (물리 트랜잭션 분리)
    스프링/스프링 기초DB 2023. 4. 2. 15:10

    외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하는 방법이 있다. 내부 트랜잭션에서 문제가 발생하여 롤백해도 외부 트랜잭션에 영향을 주지 않는다거나 반대로 외부 트랜잭션에 문제가 발생해도 내부 트랜잭션에 영향을 주지 않고 각각의 물리 트랜잭션을 사용하는 방법이다.

     

    @Test
    void inner_rollback_requires_new() {
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManger.getTransaction(new DefaultTransactionDefinition());
        log.info("outer.isNewTransaction()={}", outer.isNewTransaction());
    
        log.info("내부 트랜잭션 시작");
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        // 신규 트랜잭션을 만들어서 사용하는 것
        TransactionStatus inner = txManger.getTransaction(definition);
        log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // true
    
        log.info("내부 트랜잭션 롤백");
        txManger.rollback(inner); // 롤백
    
        log.info("외부 트랜잭션 커밋");
        txManger.commit(outer); // 커밋
    }

    외부 트랜잭션에서 커넥션1을 사용하다가 REQUIRES_NEW를 만나면 커넥션1을 보류하고 내부 트랜잭션에서 커넥션2를 만들어서 사용한다. 내부 트랜잭션에서 커넥션2를 다 사용하면 커넥션1을 다시 사용하게 된다.

     

    테스트 코드 실행 로그

    외부 트랜잭션에서 conn0을 사용하다가 내부 트랜잭션이 시작하면 외부 트랜잭션은 보류하고 내부 트랜잭션이 conn1을 사용한다. 내부에서 커밋, 롤백까지 다 이루어지고 나서 보류되었던 conn0이 사용되고 외부 트랜잭션도 커밋, 롤백을 하며 트랜잭션이 종료된다.

    이전과 다르게 내부 트랜잭션과 외부 트랜잭션은 아예 분리된 물리 트랜잭션이라는 것을 로그를 통해서 확인할 수 있었다.

    (물론 이때의 각각의 트랜잭션도 각각 rollback-only 설정을 체크한다.)

     

    로직 안에서의 별도의 로직은 별도의 트랜잭션을 사용하고 싶다면 이렇게 REQUIRES_NEW를 사용하면 된다.

     

    커넥션 풀 여러개를 동시에 사용하는 방식이기 때문에 실무에서는 db에서의 성능까지 고려해가며 조심히 사용해야된다.

     

     

     

     

     

     

    REQUIRES_NEW의 실제 활용 코드

    MemberRepository

    @Slf4j
    @Repository
    @RequiredArgsConstructor
    public class MemberRepository {
    
        private final EntityManager em;
    
        @Transactional // JPA 모든 데이터 변경은 트랜잭션 안에서 이루어져야함
        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();
        }
    }

    Member를 저장하는 MemberRepository이다. Transaction 애노테이션으로 트랜잭션이 가능하게 한다.

     

    LogRepository

    @Slf4j
    @Repository
    @RequiredArgsConstructor
    public class LogRepository {
    
        private final EntityManager em;
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void save(Log logMessage) {
            log.info("log 저장");
            em.persist(logMessage);
    
            if (logMessage.getMessage().contains("로그예외")) {
                log.info("log 저장시 예외 발생");
                throw new RuntimeException("예외 발생");
            }
        }
    
        public Optional<Log> find(String message) {
            return em.createQuery("select l from Log l where l.message = :message", Log.class)
                    .setParameter("message", message)
                    .getResultList().stream().findAny();
        }
    }

    트랜잭션 애노테이션에 (propagation = Propagation.REQUIRES_NEW)를 추가해줬다. 이러면 물리 트랜잭션이 분리가 되어서 별도의 물리 트랜잭션이 다른 커넥션을 추가로 받아서 사용하게 된다.

     

     

    MemberService에서의 join 메서드

    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.getMessage());
            log.info("정상 흐름 반환");
        }
    
        log.info("== logRepository 호출 종료 ==");
    }

    REQUIRES_NEW를 사용하려면 위와 같이 Exception을 try-catch 문으로 잡아야한다.

     

     

    /**
     * memberService     @Transactional:ON
     * memberRepository  @Transactional:ON
     * logRepository     @Transactional:ON(REQUIRES_NEW) Exception
     */
    @Test
    void recoverException_success() {
        //given
        String username = "로그예외_recoverException_success";
    
        //when
        memberService.joinV2(username);
    
        //then :  member 저장, log 롤백
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isEmpty());
    }

    테스트코드이다. 하나의 물리 트랜잭션이 아니라 logRepository는 별도의 물리 트랜잭션을 가지고 있으므로 전체가 롤백되는 것이 아니라 memberRepository와 memberService는 커밋되고 logRepository만 롤백된다.

     

    회원가입을 한다고 예를 들었을 때, 로그가 오류가 나도 나중에 복구 가능해서 일단 회원가입은 시키고 싶을 때의 예를 생각해보면 된다. 회원 데이터만 저장되고 로그 데이터만 롤백되는 것이다.

     

    사진 : 김영한님 스프링 강의

     

Designed by Tistory.