Spring - Transactional AOP annotaion 직접 구현해보기

salgu·2022년 5월 14일
0

Spring

목록 보기
3/22
post-thumbnail
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTransactional {
}

MyTransactional annotaion을 선언해줍니다.

  • RetentionPolicy.CLASS : 컴파일러가 컴파일에서는 어노테이션의 메모리를 가져가지만 실질적으로 런타임시에는 사라지게 됩니다. 런타임시에 사라진다는 것은 리플렉션으로 선언된 어노테이션 데이터를 가져올 수 없게 됩니다. 디폴트값입니다.
  • RetentionPolicy.RUNTIME : 어노테이션을 런타임시에까지 사용할 수 있습니다. JVM이 자바 바이트코드가 담긴 class 파일에서 런타임환경을 구성하고 런타임을 종료할 때까지 메모리는 살아있습니다.
@Slf4j
@Aspect
@RequiredArgsConstructor
public class MyTransactionalAspect {

    private final PlatformTransactionManager transactionManager;

    @Around("@annotation(MyTransactional)")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[Transaction Start] {}", joinPoint.getSignature());
            final TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
            try {
                // 비지니스 로직
                joinPoint.proceed();
                // 트랜잭션 종료
                transactionManager.commit(status);
            } catch (Exception e) {
                // 실패시 롤백
                transactionManager.rollback(status);
                throw new IllegalStateException(e);
            }
            log.info("[Transaction End] {}", joinPoint.getSignature());
            return null;
        } catch (IllegalStateException e) {
            log.info("[Transaction Rollback] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[Resource Release] {}", joinPoint.getSignature());
        }
    }
}

MyTransactional annotaion이 붙은 메소드에 프록시 로직입니다.

  • TransactionManager : transaction에 이용되는 connection을 동기화 시켜주는 역할을 합니다.(ThreadLocal 사용)
    • transactionManager.commit(status) : target 로직에서 예외가 발생하지 않으면 commit이 동작하고 예외가 발생한다면 rollback이 동작하게 됩니다.
      • status를 파라미터로 주면 해당 커넥션이 pool로 반환되면서 트랜잭션이 종료됩니다.
  • @Around : Pointcut, 비지니스 메소드 실행 전과 실행 후 Advice 메소드 동작하는 형태입니다.
  • ("@annotation(MyTransactional)") : Joinpoint, 해당 어노테이션이 붙은 메소드에만 동작합니다.
  • joinPoint.proceed() : 실제 target을 호출하게 됩니다.
@Slf4j
@Component
public class MemberServiceV4 {

    private final MemberRepositoryV4 memberRepository;

    public MemberServiceV4(MemberRepositoryV4 memberRepository) {
        this.memberRepository = memberRepository;
    }

    @MyTransactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        accountTransferLogic(fromId, toId, money);
    }

    private void accountTransferLogic(String fromId, String toId, int money) throws SQLException {
        final Member fromMember = memberRepository.findById(fromId);
        final Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromMember.getMemberId(), fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toMember.getMemberId(), toMember.getMoney() + money);
    }

    private void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex"))
            throw new IllegalStateException("이체 중 예외발생");
    }

}

계좌이체 로직입니다.

@Slf4j
@Import(MyTransactionalAspect.class)	// Aspect를 import해줘야 테스트코드에서 작동하게 됩니다.
@SpringBootTest
class MemberServiceV4Test {

    @Autowired
    MemberServiceV4 memberService;

    @Autowired
    MemberRepositoryV3 memberRepository;

    private static final String toMemberId = "김다현";
    private static final String fromMemberId = "김사나";
    private static final String errorMemberId = "ex";
    private static final int money = 10000;

    @TestConfiguration
    static class TestConfig {

        // spring container 에 dataSource 가 자동으로 등록되어 주입받아 사용합니다.
        private final DataSource dataSource;

        TestConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource);
        }
    }

	// 테스트를 반복하기위해 테스트가 끝난 후 데이터를 제거해줍니다.
    @AfterEach
    void after() throws SQLException {
        memberRepository.delete(toMemberId);
        memberRepository.delete(fromMemberId);
        memberRepository.delete(errorMemberId);
    }

	// memberService에 MyTransactional AOP 어노테이션을 적용했기 때문에
    // 해당 메소드는 트랜잭션이 적용된 프록시가 주입되어야 됩니다.
    @Test
    void AopProxyCheck() {
        log.info("memberService class = {}", memberService.getClass());
        log.info("memberRepository class = {}", memberRepository.getClass());
        Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() throws SQLException {
        final Member toMember = new Member(toMemberId, money);
        final Member fromMember = new Member(fromMemberId, money);
        memberRepository.save(toMember);
        memberRepository.save(fromMember);

        memberService.accountTransfer(toMember.getMemberId(), fromMember.getMemberId(), money);

        final Member toMemberResult = memberRepository.findById(toMember.getMemberId());
        final Member fromMemberResult = memberRepository.findById(fromMember.getMemberId());
        assertThat(toMemberResult.getMoney()).isEqualTo(money-money);
        assertThat(fromMemberResult.getMoney()).isEqualTo(money+money);
    }

    @Test
    @DisplayName("이체 중 예외발생")
    void accountTransferEx() throws SQLException {
        final Member toMember = new Member(toMemberId, money);
        final Member fromMember = new Member(errorMemberId, money);
        memberRepository.save(toMember);
        memberRepository.save(fromMember);
        assertThatThrownBy(() -> memberService.accountTransfer(toMember.getMemberId(), fromMember.getMemberId(), money))
                .isInstanceOf(IllegalStateException.class);
        final Member toMemberResult = memberRepository.findById(toMember.getMemberId());
        final Member fromMemberResult = memberRepository.findById(fromMember.getMemberId());
        assertThat(toMemberResult.getMoney()).isEqualTo(money);
        assertThat(fromMemberResult.getMoney()).isEqualTo(money);
    }
} 

MyTransactional이 제대로 구현되었나 확인할 수 있는 테스트 코드입니다.
정상적으로 구현이 되었다면 아래 사진처럼 테스트 코드를 통과하게 됩니다.

해당 소스코드 : https://github.com/salgu1998/Spring-DB-Connection-Transaction/commit/57fd9518d10fc0723f30ce6f029ef5b59eeab64f?diff=unified

reference :
https://sas-study.tistory.com/329,
김영한 선생님,
https://developer-joe.tistory.com/221

profile
https://github.com/leeeesanggyu, leeeesanggyu@gmail.com

0개의 댓글