트랜잭션 전파가 필요한 이유

wangjh789·2022년 8월 22일
0

[Spring] 스프링-DB-2

목록 보기
21/21

다음과 같은 상황이 있다고 가정하자
클라이언트A는 MemberService 부터 트랜잭션을 물고 하나의 커넥션으로 통째로 수행하는 작업을 원하고,
클라이언트B는 MemberRepository만의 독립적인 커넥션을 원할 때 어떻게 해결해야 될까?

간단한 방법은 MemberRepository의 한 메서드를 트랜잭션이 있는 버전, 없는 버젼 이렇게 2가지로 만들어 필요한 것을 호출해 사용하는 것이다.

이떄 사용되는 것이 트랜잭션 전파 이다.
위의 상황은 REQUIRES 옵션으로 해결할 수 있다.

논리적 트랜잭션이 하나라도 롤백되면 전체가 롤백된다는 원칙덕분에 데이터 정합성을 지킬 수 있다.

복구 REQUIRES - 해결 X

회원과 로그를 하나의 트랜잭션으로 묶어 데이터 정합성 문제를 해결했다.
그런데 회원 이력로그를 DB에 남기는 작업에 가끔 문제가 발생해 회원 가입 자체가 안되는 경우가 발생한다고 가정한다.

그래서 정합성을 임시로 포기하고 회원가입을 시도한 로그를 남기는데 실패하더라도 회원가입은 유지가 되어야 한다.

단순하게 생각하면 예외가 터지면 MemberService에서 try/catch로 잡아 정상흐름으로 반환하면 문제가 없을 거라고 생각할 수 있다.

    @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 호출 종료 ==");
    }

하지만 실상은 LogRepository에서 예외가 발생했을 때 이미 글로벌 트랜잭션에 rollback-only 가 마크되어 결국 롤백된다.

    @Test
    void recoverException_fail() {
        //given
        String username = "로그예외outerTxOff_fail";

        //when
        assertThatThrownBy(() -> memberService.joinV2(username))
                .isInstanceOf(UnexpectedRollbackException.class);

        //then : 회원도 롤백, 로그도 롤백이 된다.
        assertTrue(memberRepository.find(username).isEmpty());
        assertTrue(logRepository.find(username).isEmpty());
    }

LogRepository에서 런타임 예외를 터뜨리면 동기화 트랜잭션 매니저에 rollback-only가 마크되고 그 예외가 MemberService에게 전달된다. MemberService는 그 예외를 try/catch로 잡았지만 트랜잭션 AOP 프록시에서 커밋하려는 개발자의 오류와 다르게 롤백을 수행하므로 UnExcpectedRollbackExcpetion 예외가 터지게 된다.

복구 REQUIRES_NEW - 해결

로그와 관련된 물리 트랜잭션을 별도로 분리하자

    @Test
    void recoverException_success() {
        //given
        String username = "로그예외outerTxOff_fail";

        //when
        memberService.joinV2(username);

        //then : 회원는 저장, 로그는 롤백이 된다.
        assertTrue(memberRepository.find(username).isPresent());
        assertTrue(logRepository.find(username).isEmpty());
    }

LogRepository는 REQUIRES_NEW 트랜잭션 전파를 사용해 별도의 물리 트랜잭션으로 분리되어 동작한다. 따라서 LogRepository가 롤백되든 커밋되든 기존의 MemberService의 트랜잭션은 영향을 받지 않는다.

주의할 것은 물리 트랜잭션이 분리되었다고 해도 런타임 예외가 처리된 것은 아니다. 만약 MemberService에서 예외를 처리하지 않는다면 언체크 예외가 터져 롤백이 된다.

이 방법은 요청 하나에 2개의 DB 커넥션을 사용하게 된다.

이 방법 말고도 구조를 변경하는 방법도 있다.
물리적으로 클라이언트를 만들어 다른 트랜잭션을 사용하게 하는 방법이다.

profile
기록

0개의 댓글