트랜잭션 전파 - 활용 예제

박찬우·2024년 2월 8일

스프링 DB

목록 보기
53/53

회원

  • domain
@Entity
@Getter @Setter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;

    public Member() {
    }

    public Member(String username) {
        this.username = username;
    }
}
  • repository
@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();
    }
}

DB로그

  • domain
@Entity
@Getter
@Setter
public class Log {
    @Id
    @GeneratedValue
    private Long id;
    private String message;
    public Log() {
    }
    public Log(String message) {
        this.message = message;
    }
}
  • repository
@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();
    }
}

Service

@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final LogRepository logRepository;

    @Transactional
    public void joinV1(String username) {
        Member member = new Member(username);
        Log logMessage = new Log(username);
        log.info("== memberRepository 호출 시작 ==");
        memberRepository.save(member);
        log.info("== memberRepository 호출 종료 ==");
        log.info("== logRepository 호출 시작 ==");
        logRepository.save(logMessage);
        log.info("== 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 호출 종료 ==");
    }
}

Test

@Slf4j
@SpringBootTest
class MemberServiceTest {
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;
    @Autowired
    LogRepository logRepository;

    /**
     * MemberService @Transactional:OFF
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 서로 다른 트랜잭션 사용 commit 테스트")
    void outerTxOff_success() {
        //given
        String username = "outerTxOff_success";
        //when
        memberService.joinV1(username);
        //then: 모든 데이터가 정상 저장된다.
        // 서로 다른 트랜잭션이므로 commit도 2번 호출됨
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService @Transactional:OFF
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON Exception
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 서로 다른 물리 트랜잭션 사용 rollback 테스트")
    void outerTxOff_fail() {
        //given
        String username = "로그예외_outerTxOff_fail";
        //when
        // joinV1()은 로그예외인 경우 RuntimeException을 던짐
        Assertions.assertThatThrownBy(() -> memberService.joinV1(username))
                        .isInstanceOf(RuntimeException.class);

        //then: 모든 데이터가 정상 저장된다.
        // 서로 다른 트랜잭션이므로 rollback, commit 각각 한 번씩 호출됨
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService @Transactional:ON
     * MemberRepository @Transactional:OFF
     * LogRepository @Transactional:OFF
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 같은 물리 트랜잭션 사용 commit 테스트")
    void singleTx() {
        //given
        String username = "singleTx_success";
        //when
        memberService.joinV1(username);
        //then: 모든 데이터가 정상 저장된다.
        // 같은 트랜잭션 안에서 처리됨 commit 한 번만 호출
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService @Transactional:ON
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 같은 물리 트랜잭션 사용 commit 테스트")
    void outerTxOn_success() {
        //given
        String username = "outerTxOn_success";
        //when
        memberService.joinV1(username);
        //then: 모든 데이터가 정상 저장된다.
        // 같은 트랜잭션 안에서 처리됨 commit 한 번만 호출
        // service에서 예외를 전파받았기 때문에 repository에 @Transactional이 있어도 작동하지 않음
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService @Transactional:ON
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON Exception
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 같은 물리 트랜잭션 사용 rollback 테스트")
    void outerTxOn_fail() {
        //given
        String username = "로그예외_outerTxOn_fail";
        //when
        // joinV1()은 로그예외인 경우 RuntimeException을 던짐
        Assertions.assertThatThrownBy(() -> memberService.joinV1(username))
                .isInstanceOf(RuntimeException.class);

        //then: 모든 데이터가 정상 저장된다.
        // 같은 트랜잭션 안에서 하나라도 rollback이면 rollback이 호출되고 예외를 던짐
        assertTrue(memberRepository.find(username).isEmpty());
        assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService @Transactional:ON
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON Exception
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRED - 같은 물리 트랜잭션 사용 rollback 테스트")
    void recoverException_fail() {
        //given
        String username = "로그예외_recoverException_fail";
        //when
        // joinV2()는 로그예외인 경우 RuntimeException을 잡아서 처리하여 예외가 테스트로 넘어오면 안된다고 생각이 듦
        // 처리가 될 것 같지만 내부 트랜잭션에 rollbackOnly가 설정되어 전체 롤백이 진행되고 예외를 던짐
        Assertions.assertThatThrownBy(() -> memberService.joinV2(username))
                .isInstanceOf(UnexpectedRollbackException.class);

        //then: 모든 데이터가 정상 저장된다.
        // 같은 트랜잭션 안에서 하나라도 rollback이면 rollback이 호출되고 예외를 던짐
        assertTrue(memberRepository.find(username).isEmpty());
        assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService @Transactional:ON
     * MemberRepository @Transactional:ON
     * LogRepository @Transactional:ON Exception Propagation.REQUIRES_NEW
     */
    @Test
    @DisplayName("propagation = Propagation.REQUIRES_NEW - 서로 다른 물리 트랜잭션 사용 rollback 테스트")
    void recoverException_success() {
        //given
        String username = "로그예외_recoverException_success";
        //when
        // 서로 다른 물리 트랜잭션이 적용되어 각각 실행되서 예외가 오지 않음
        memberService.joinV2(username);

        //then: 모든 데이터가 정상 저장된다.
        // 서로 다른 트랜잭션이므로 rollback, commit 각각 한 번씩 호출됨
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isEmpty());
    }


}
profile
진짜 개발자가 되어보자

0개의 댓글