서론

예전에 함께 스터디를 했던 스터디원이 트랜잭션에 관한 블로그 글을 공유하면서, 흥미로운 내용이라고 소개했다.

해당 글에서는 기존에 내가 알고있던 사실이 틀리다라고 얘기하는 내용이었다.

 

내가 잘못알고 있었던 건가??! 생각들때쯤 해당 코드에서 옥에 티 발견했다.

 

간단한 내용일 수도 있지만 개념을 확실하게 잡지 않으면 충분히 헷갈릴 수 있을 거 같다.  덕분에 나도 이번 기회에 다시 학습해보고 명확하게 정리를 해보는 시간을 갖게 되었다.

 


본문

 

글의 내용은 이런 것이었다.

@Transactional을 사용할 때 전파 속성을 "REQUIRES_NEW"로 지정해도, 부모 트랜잭션이랑 독립된 트랜잭션으로 실행되지 않는다 라는 내용이었다.

 

이 주장의 근거로 "자식 트랜잭션에서 예외가 발생하면, 부모 트랜잭션에서도 롤백이 발생한다"라는 예제를 보여주고 있었다. 다음 코드는 해당 글의 코드를 간결하게 재구성한 내용이다.

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 주목
    public void save() {
        childRepository.save(new Child());
        throw new IllegalStateException();
    }

}

 

@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional
    public void save(int age) {
        parentRepository.save(new Parent(age));
        childService.save();
    }
}

 

ParentService -> ChildService(save)를 호출하는데, ChildService에서 unchecked exception을 발생시킨다.

 

해당 글에선 ChildService는 REQUIRED_NEW 속성으로 트랜잭션을 설정했기 때문에, 두 서비스의 트랜잭션은 서로 독립적이어야하는데,

즉 Parent가 정상적으로 저장되어야 하는데, Parent가 저장되지 않았기 때문에 두 트랜잭션이 독립적이지 않다라는 얘기였다.

 

과연 두 트랜잭션은 독립적이지 않은걸까?

 


원인 진단

 

사실 원인은 매우 간단하다. Parent가 정상적으로 저장되지 않는 이유는 바로 예외 때문이다.

자바에서 기본적으로 예외가 발생했을때 처리해주지 않으면 콜스택을 하나씩 제거하면서 최초 호출한곳까지 예외가 전파된다.

 

ParentService에서 예외처리를 하지 않았기 때문에 예외가 전파되어서 ParentService도 롤백을 진행하게 된 것이다.

 

 


 

 

독립적 ??!

 

여기서  "독립적"이란 단어에 대해서 얘기해볼 필요가 있을 거 같다.

REQUIRES_NEW은 독립적인 것인가?

 

보통 독립적이라하면 어떤 대상 A가 있을때,  A는 다른 것으로 부터 영향을 받지 않는 것을 이야기한다.

즉 다른 존재에 의해서 영향을 받거나 주지 않는 것을 의미한다.

 

이를 비춰봤을때 REQUIRES_NEW는 독립적이라는 표현보다 별도의 트랜잭션을 생성하는 표현이 맞다고 생각한다.

 

왜냐면 스프링에서 트랜잭션은 thread-local을 기반으로 동작한다. TransactionSynchronizationManager는 관련 리소스들을 쓰레드 로컬에 보관하고있다. 트랜잭션 매니저는 이녀석을 이용해서 리소스를 사용한다.

REQUIRES_NEW별도의 새로운 트랜잭션(커넥션도 실제 다르다)을 만들 뿐, 쓰레드는 동일하다.

 

따라서 개인적으로 독립적이란 표현이 적합하지 않다고 생각한다. 독립적이려면 사실 위 코드에서 예외를 처리하지 않아도 Parent가 정상적으로 동작 했어야한다. Javadoc에서도 새로운  트랜잭션을 생성한다고 표현하고 있다. 독립적이란 용어는 사용하지 않는다.

 

REQUIRES_NEW속성을 이용하면 실제 두 트랜잭션이 서로 다른 커넥션을 이용하는 것을 확인할 수 있다.
REQUIRES_NEW에 대한 Javadoc 발췌 (독립적이란 표현을 쓰지 않는다) 새로운 트랜잭션을 만든다고 설명한다.

 


 

해결 방법

 

그럼 ParentService가 롤백되는 것은 어떻게 해결해야될까? 우선 해당  글에서는 @Async를 이용해서 해결했다고한다. 실제로 이 방법은 해결 방법은 맞지만, 문제 원인에 맞는 진단은 아니라고 생각한다.

 

왜냐면  @Async는 별도의 쓰레드로 작업을 처리하는 비동기 처리를 목적으로 하는 녀석이기 때문이다. 애초에 쓰레드 로컬이 다르기 때문에 독립적일 수 밖에 없다.

 

따라서 해당 해당 예제에서 문제를 해결하는 방법은 예외 처리만 해주면 끝이다.

    @Transactional
    public void save() {
        parentRepository.save(new Parent());
        try {
            childService.save();
        } catch (IllegalStateException e) {
            log.info(">>> child service 예외 발생");          
        }
    }

이런식으로 예외를 처리하면 ParentService는 정상적으로 커밋이 되고, Parent가 정상적으로 저장되는 것을 확인할 수 있다.

그럼 이런 의문이 들 수 있다. 그럼 REQUIRES 쓰지않고 REQUIRES_NEW 를 쓰면 뭐가 다른 것이지?

 

위 코드랑 똑같이 예외 처리를 한 상태에서 ChildService에서 REQUIRES_NEW를 빼버리고 REQUIRES로 바꾸면 어떻게 될까?

ParentService는 롤백된다. 이유는 하나의 물리적 트랜잭션에서 여러개의 논리적인 트랜잭션으로 구성되어 있을 경우, 하나의 논리적 트랜잭션에서 unchecked exception이 발생하면, 롤백 마크를하고 해당 물리적 트랜잭션은 전체 롤백이 되기 때문이다.

 

아무래도 용어 때문에 이런 오해가 충분히 생길 수 있을거 같다고 생각한다.

나도 다시 명확하게 정리하게 된 계기가 되었다.