스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션11. 스프링 트랜잭션 전파2 - 활용
CS/김영한 스프링 강의

스프링 DB 2편 - 데이터 접근 핵심 원리 - 섹션11. 스프링 트랜잭션 전파2 - 활용

실제로 활용해보자.

회원 등록하는걸 예시로 만들어볼 거다. 원래 수정, 삭제 같은 경우도 로그로 남겨야 되지만 일단 등록할 때 실제 db에 회원 등록 및 로그도 등록하는걸 해볼거다.

 

@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final LogRepository logRepository;

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

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

함수에 @Transactional을 붙인게 아니라 이미 각각의 함수들 안에 @Transactional이 붙어있다.

다 만들었으니 테스트 코드를 만들자.

무난하게 잘 된다. 

 

그럼 멤버 등록은 성공하고 로그는 실패한 경우는

멤버는 정상이라 commit되서 db에 남지만 로그는 예외가 나와서 rollback되어 남지 않게 된다.

근데 만약 비즈니스 요구 사항이 둘은 항상 같이 다녀야 한다고 하면 이 두가지 트랜잭션을 하나로 묶어야 한다.

 

일단 다른곳들에 @Transactional 없애고 서비스에만 @Transactional 넣어서 아얘 하나로 묶는걸 사용할 수도 있다.

그럼 물리적으로 하나의 커넥션 가지고 왔다갔다 하는거라 잘 묶일 수밖에 없다..

근데 서비스에만 트랜잭션을 넣어놨으니 만약 서비스가 아닌 다른곳을 통해 레퍼지토리 함수를 실행하려고 하면 트랜잭션이 적용 안되는 문제가 발생한다. 또 새로 추가되면??

이렇게 복잡해지니 전파가 있는거임.

 

사실 그냥 해도 됨 ㅎㅎ 기본값이니 스프링이 알아서 해준다.

 

만약 로그부분에서 런타임 예외가 발생해서 롤백할 경우를 보자.

어차피 내부 트랜잭션은 가상이라 롤백 날려도 반영되지 않고 rollbackOnly를 true로 설정하지만, 외부 트랜잭션에서 롤백한다. 근데 이 롤백이 자기가 commit을 날리고 해서 롤백이 발생하는게 아니라 런타임 예외로써 롤백이 되는것이기 때문에 어차피 rollbackOnly를 참고하지 않고 롤백한다는 것이 조금 헷갈릴 수 있다.

 

이것까지 이해했다면 복구되는 부분에서 실무에 진짜 헷갈릴 수 있는 것이 있다. 중요한듯.

멤버 저장은 되지만 로그에서 에러 떴을 때 그냥 try catch해서 서비스에서 정상 흐름으로 바꾸면 제대로 되는것 아닌가? 생각할 수있다. 하지만 큰 실수다.

실패한 테스트

서비스에서 트랜잭션을 시작하여 서비스가 물리 트랜잭션을 가지고 있고 이 커넥션 하나 가지고 내부 트랜잭션들을 수행할 텐데 하나라도 예외가 났을 시 메니저에 rollbackOnly를 true를 바꾸기 때문에 try catch로 잡아서 정상 흐름으로 바꾸고 commit하려고 해도 rollbackOnly 때문에 커밋하지 못하고 예외를 발생시킨다. 결과 db에는 롤백되어 반영되지 않고 회원가입은 롤백되어 없던게 된다.

결국 의도한대로 아니니 적절한 대처 방법이 아니었다.

이래서 REQUIRES_NEW가 필요함.

 

로그 부분만 REQUIRES_NEW로 바꿔서 따로 커넥션을 만들어서 실행해주자. 그럼 여기서 에러가 떠도 멤버 등록까지 관리하는 서비스의 트랜잭션은 영향 없이 정상적으로 될거다.

그럼 예외가 생기더라도 다른 트랜잭션이기 때문에 영향이 없다.

 

이러면 주의할 점이 하나 실행하는데 커넥션이 2개라 낭비라고 생각할 수 있음.

그럴땐 아예 구조적으로 REQUIRES_NEW를 사용하지 않게 하는 방법을 사용해도 된다. 두 가지를 아예 따로 따로 실행하게 하던지 하면 된다. 어떻게 할지는 알아서 선택하자.