REQUIRES_NEW와 독립적이라는 단어의 차이

Kevin·2025년 1월 5일
1

JPA

목록 보기
13/14
post-thumbnail

서론

최근 회사에서 개발을 하면서, 부모 트랜잭션간 Exception이 발생해도, 자식 트랜잭션은 Rollback 되지 않고 정상적으로 커밋 되게 구현 하고자 하는 일이 있었다.

이 때 부모와 자식간의 물리적 트랜잭션을 분리 해주는 REQUIRES_NEW 전파 옵션을 통해서 문제를 해결 하고자 하였으나, 내가 의도한대로 동작 하지 않아 문제를 살펴보았다.


본론

부모 트랜잭션

    @Override
    @Transactional(noRollbackFor = RuntimeException.class)
    public AuthenticationCodeResponseDto sendValidateMessage(AuthenticationCodeRequestDto requestDto) {

        List<AuthenticationCode> codes = authenticationCodeRepository.findByPhoneNum(requestDto.getPhoneNum());

        if (codes.size() >= 5) {
            deleteAuthenticationCodes(codes);

            throw new RuntimeException("인증 횟수가 초과 되었습니다. 처음부터 다시 시도해주세요.");
        }

        return responseDto;
    }

자식 트랜잭션

  	@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deleteAuthenticationCodes(List<AuthenticationCode> codes) {
        authenticationCodeRepository.deleteAllInBatch(codes);
    }

`위 코드는 각색 된 코드입니다.`

위 코드의 요구사항은 이렇다.

조회를 통해 가져온 List의 size가 5 이상인 경우 해당 데이터들을 모두 삭제 하고, RuntimeException을 발생 시켜라.

위 요구사항을 구현하던 중 부모에서 발생하는 RuntimeException으로 인해서 자식 트랜잭션의 delete가 롤백 되고 있다.

그리하여서 자식에게 REQUIRES_NEW 설정을 하여 트랜잭션을 분리 하고자 하였다.

REQUIRES_NEW 전파 옵션은 부모 트랜잭션에 참여하지 않고, 자식 트랜잭션은 새로운 물리적 트랜잭션을 시작하게 된다.

이를 통해 부모와 자식 트랜잭션을 분리하면 부모의 쿼리만 롤백 되고, 자식 트랜잭션의 쿼리는 롤백 되지 않을거라 생각 했다.

그러나 어쩐 일에서인지 자식 트랜잭션의 delete 문 또한 롤백이 되었다.


이유가 뭘까?

답은 부모 트랜잭션과 자식 트랜잭션은 동일 스레드 내에서 별도의 커넥션을 잡아 트랜잭션을 생성 하는 것이기 때문이다.

그렇기 때문에 부모 트랜잭션에서 RuntimeException이 발생 하게 된다면, 같은 쓰레드를 사용 하는 자식 트랜잭션 또한 RuntimeException의 영향을 받고 이에 롤백 되는 것이다.


REQUIRES_NEW에 대해서 대부분 “독립적”이라는 표현을 사용하여, 나는 환경 자체가 부모와 자식간 아예 분리가 되는 줄 알았다.


그러나 정확히는 “독립적”이 아닌 단순히 새로운 트랜잭션을 만드는 것일 뿐 스레드는 동일하다.

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

이에 따라 만약 자식 트랜잭션에서 예외가 발생 했다면, 부모에서 잡아주면 부모 트랜잭션의 롤백을 막을 수 있을 것이다.

물론 이 때 자식 트랜잭션의 전파 옵션은 REQUIRES_NEW 이어야 한다.

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

REQUIRES_NEW 전파 옵션은 자식 트랜잭션을 아예 별도의 물리적 트랜잭션으로 구분 하기에 롤백이 되지 >않게 된다.


그렇다면 어떻게 요구 사항을 만족 할 수 있을까?

바로 같은 스레드 안에서 동작 하여 문제가 발생 하였던 자식 트랜잭션을 아예 다른 스레드에서 비동기로 실행 하게 하면 되는 것이다.

그렇기위해서 이벤트 핸들링 방식을 사용할 수 있다.

아래는 이벤트 핸들링 방식을 사용한 부모, 자식 트랜잭션 코드이다.

import org.springframework.context.ApplicationEventPublisher;

@Service
@RequiredArgsConstructor
public class AuthenticationCodeService {

    private final AuthenticationCodeRepository authenticationCodeRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Override
    @Transactional(noRollbackFor = RuntimeException.class)
    public AuthenticationCodeResponseDto sendValidateMessage(AuthenticationCodeRequestDto requestDto) {
        List<AuthenticationCode> codes = authenticationCodeRepository.findByPhoneNum(requestDto.getPhoneNum());

        if (codes.size() >= 5) {
            eventPublisher.publishEvent(new AuthenticationCodeDeletionEvent(codes));

            throw new RuntimeException("인증 횟수가 초과 되었습니다. 처음부터 다시 시도해주세요.");
        }

        // 성공 응답 반환
        return new AuthenticationCodeResponseDto(/* ... */);
    }
}
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;

@Component
public class AuthenticationCodeDeletionListener {

    private final AuthenticationCodeRepository authenticationCodeRepository;

    public AuthenticationCodeDeletionListener(AuthenticationCodeRepository authenticationCodeRepository) {
        this.authenticationCodeRepository = authenticationCodeRepository;
    }

    @Async
    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleAuthenticationCodeDeletionEvent(AuthenticationCodeDeletionEvent event) {
        authenticationCodeRepository.deleteAllInBatch(event.getCodes());
    }
}
profile
Hello, World! \n

0개의 댓글