caffeine-library / pro-spring-5

🌱 전문가를 위한 스프링5를 읽는 스터디
5 stars 0 forks source link

[miscellaneous] 트랜잭션 전파 유형과 예외 발생 위치에 따른 롤백 확인용 테스트 코드 #75

Closed binchoo closed 3 years ago

binchoo commented 3 years ago

연관 이슈: https://github.com/caffeine-library/pro-spring-5/issues/68

목적

(1) 트랜잭션 전파 유형 (2) 예외 발생 위치: 부모 / 자식 트랜잭셔널 메서드

위 두 조건에 따라 롤백 동작이 어디까지 수행되는지 확인하는 것입니다.

개요

비즈니스 요구사항 하나는, 하위에서 여러 서비스의 협업으로 구현되므로 이 요구사항이 온전히 달성되려면 각 서비스의 장애에 대한 대응이 필요합니다.

허나, 하위 서비스가 비즈니스 달성에 밀접한 연관을 갖지 않을 수도, 다른 장애에 면역을 갖고 모든 이력을 남겨야 할 수도 있습니다.

이렇듯 장애 처리(롤백)의 범위를 조정하는 개념이 트랜잭셔널 메서드의 '트랜잭션 전파'에 담겨 있는 것으로 생각됩니다.

PROPAGATION_REQUIRED: 이미 존재하는 트랜잭션을 지원합니다. 트랜잭션이 없으면 새 트랜잭션이 시작됩니다.

하지만 '트랜잭션 전파'의 스펙은 기술적인 수준에서 딱딱하게 설명되어 그 개념이 등장한 상위 배경이 파악되지가 않습니다.

따라서 테스트 코드를 작성하여 전파 유형에 따른 롤백 범위가 어떻게 되는지 살펴보려 합니다.

엔터티: SimpleData

두 개의 boolean 칼럼 OuterCommit, InnerCommit를 갖습니다.

image

서비스

OuterServiceInnerService가 있습니다.

public interface OuterService {
    void setInnerService(InnerService innerService);
    void updateColumn(Long id, boolean outerException, ExceptionLocation innerExceptionLocation);
}

public interface InnerService {
    void updateColumn(Long id, ExceptionLocation exLocation);
}

OuterService

엔터티의 OuterCommit과 InnerCommit을 TRUE로 갱신하는 비즈니스 로직을 갖춥니다.

OuterService는 예외 발생 지점을 설정할 수 있습니다.

OuterService는 트랜잭션 하에 수행됩니다. (전파 정책: PROPAGATION_REQUIRED)

OuterService는 InnerService가 뱉은 예외를 Consume해야 합니다.

@Service
public class OuterServiceImpl implements OuterService {

    @Autowired
    private SimpleDataRepository repository;

    private InnerService innerService = null;

    @Override
    public void setInnerService(InnerService innerService) {
        this.innerService = innerService;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void updateColumn(Long id, boolean outerException, ExceptionLocation innerExceptionLocation) {

        try {
            innerService.updateColumn(id, innerExceptionLocation);
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        updateOuterColumn(id);
        if (outerException) {
            throw new RuntimeException();
        }
    }

    private void updateOuterColumn(Long id) {
        SimpleData data = repository.findById(id).orElseGet(()->{ throw new IllegalArgumentException(); });

        data.setOuterCommit(true);

        repository.save(data);
    }
}

InnerService

InnerService는 엔터티의 InnerCommit을 TRUE로 갱신하는 로직을 수행합니다.

InnerService는 InnerCommit 갱신 직전 또는 직후에 에러를 발생시킬 수 있습니다.

InnerService는 트랜잭션 전파 정책이 변경되며 테스트됩니다.

전파 정책 별 InnerService

자식 트랜잭셔널 메서드의 전파 요구사항을 변경하며 테스트하기 위해

전파 정책별로 InnerService 인터페이스의 하위 구현체를 생성합니다. 그리고 @Transactional로 전파 요구사항을 기입합니다.

image

ex)

@Transactional(propagation = Propagation.MANDATORY)
@Service("innerServiceMandatory")
public class InnerServiceMandatory implements InnerService {

    @Autowired
    private InnerServiceImpl impl;

    @Override
    public void updateColumn(Long id, ExceptionLocation eLocation) {
        impl.updateColumn(id, eLocation);
    }
}

칼럼 갱신은 데이터에 직접 접근하는 또 다른 InnserSerivce 구현체에 위임합니다.

InnerServiceImpl

@Service
public class InnerServiceImpl implements InnerService {

    @Autowired
    private SimpleDataRepository repository;

    @Override
    public void updateColumn(Long id, ExceptionLocation exLocation) {
        SimpleData data = repository.findById(id).orElseGet(()->{ throw new IllegalArgumentException(); });

        data.setInnerCommit(true);

        if (ExceptionLocation.BEFORE_UPDATE == exLocation)
            exLocation.throwException();

        repository.save(data);

        if (ExceptionLocation.AFTER_UPDATE == exLocation)
            exLocation.throwException();
    }
}

테스트 케이스

image

테스트 실행

https://github.com/caffeine-library/pro-spring-5/blob/main/references/code/binchoo/TransactionPropagationTestProject/src/test/java/org/binchoo/env/propagation/test/PropagationCombinationsTest.java

테스트 오라클

칼럼 값이 TRUE이면, 해당 칼럼은 갱신된 것으로 봅니다. 칼럼 값이 FALSE이면, 아예 갱신 로직에 접근하지 못 했거나 롤백된 것입니다. assert~Rollback() 메서드는 두 용도로 활용되니 유의하여야 합니다.

private void assertOuterCommit() {
    assertThat(repository.findById(1L).get().getOuterCommit()).isTrue();
}

private void assertInnerCommit() {
    assertThat(repository.findById(1L).get().getInnerCommit()).isTrue();
}

private void assertOuterRollback() {
    assertThat(repository.findById(1L).get().getOuterCommit()).isFalse();
}

private void assertInnerRollback() {
    assertThat(repository.findById(1L).get().getInnerCommit()).isFalse();
}

결과 해석

https://github.com/caffeine-library/pro-spring-5/issues/68#issuecomment-948208664 해당 테스트 수행하여 정리해 본 Propagation 정책의 의미입니다.

@caffeine-library/readers-pro-spring-5

wooyounggggg commented 1 year ago

77