Spring DB : 트랜잭션 AOP

    이 글은 인프런 김영한님의 강의를 복습하며 정리한 글입니다.


    현재까지의 상태

    1. 트랜잭션이 없었다
      • DB 데이터 정합성 문제가 발생함.
    2. 트랜잭션을 도입함 → set autoCommit
      • 트랜잭션이 적용되어, 원자 단위로 일이 처리되었다.
    3. 각 DB 접근 기술마다 트랜잭션 사용 방법이 다름 → TransactionManager
      • TranscationManager는 트랜잭션 접근 방법을 추상화했다. 이를 통해 Jdbc / JPA 기술에 대한 의존성을 제거했다. 
    4. 서비스 계층에 Transcation 관련 반복 코드 다수 발생 → TranscationTemplate
      • TranscationTemplate은 트랜잭션의 시작 / Commit / Rollback / 자원회수등을 처리해준다.

    1~4번까지 점진적으로 코드의 개선이 있었다. TranscationTemplate에는 한 가지 문제가 남는다. 서비스 계층에 transcationTemplate.execute()라는 코드가 남는다는 것이다. 이것은 서비스 계층이 트랜잭션 기술에 의존하는 것을 의미한다. 바꿔 이야기하면 서비스 계층에 비즈니스 로직 외에 부가 관심사가 들어간다는 것을 의미한다.

    이런 코드는 유지 / 보수가 어렵다. 왜냐하면 서비스 계층에서 트랜잭션을 사용하지 않겠다고 할 경우, 코드 전체가 수정되어야 하기 때문이다. 이 부분의 해결이 필요한 상태다.

     

    트랜잭션 AOP 이해

    @Transactional를 사용하면 스프링은 트랜잭션 AOP를 제공해준다. 트랜잭션 AOP는 AOP Proxy를 이용해서 트랜잭션과 관련된 부가 관심사를 비즈니스 로직과 분리시켜준다. 

     

    트랜잭션 AOP 프록시 도입 전

    트랜잭션 AOP를 이용한 프록시 도입 전에는 서비스 계층에 다음과 같이 트랜잭션 시작 / 종료 코드가 포함되어있고, 그 사이에 비즈니스 로직이 존재하는 것을 볼 수 있다. 

    txTemplate.executeWithoutResult(transactionStatus ->{
        try {
            bizLogic(fromId, toId, money);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    } );

    실제 코드에서 볼 수 있듯이, 트랜잭션을 처리하는 코드들 사이에 bizLogic()으로 비즈니스 로직을 수행한다. 

     

    트랜잭션 AOP 프록시 도입 후

    트랜잭션 AOP를 이용한 프록시를 사용하면, 트랜잭션을 처리하는 객체와 비즈니스 로직 처리 객체를 명확하게 분리할 수 있다. 트랜잭션 AOP로 프록시를 생성하고, 프록시는 부가 관심사를 프록시 객체 내에서 처리해준다. 그리고 프록시는 부가 관심사 내에서 실제 대상이 되는 타겟 객체를 호출하면서 비즈니스 로직을 처리해준다. 즉, 핵심 관심사 / 부가 관심사에 따라 객체가 2개로 분리가 된다.

     

    트랜잭션 프록시 적용 후 서비스 코드 예시

    트랜잭션 AOP를 이용 / 이용하지 않았을 때의 코드를 비교하면, 트랜잭션 관심사의 분리를 좀 더 이해하기 쉽다.

    // AOP 트랜잭션 프록시 도입 전
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    
        transactionTemplate.executeWithoutResult(status -> {
            try {
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }
    • 프록시 도입 전
      • 서비스에 비즈니스 로직과 트랜잭션 처리 로직이 함께 섞여있다. 
    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(fromId, toId, money);
    }
    • 프록시 도입 후
      • 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다. 트랜잭션 프록시 덕분에 서비스 계층에는 순수한 비즈니스 로직만 남길 수 있게 된다.

     

    스프링이 제공하는 트랜잭션 AOP

    스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있다. 스프링 AOP를 직접 사용해서 트랜잭션 처리해도 된다. 그렇지만 트랜잭션은 매우 중요하고, 일반적인 기능이기 때문에 스프링에서 제공해주는 트랜잭션 AOP를 사용하는 것이 좋다.

    개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 어노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 어노테이션 기반 AOP가 적용되서 트랜잭션 프록시 객체를 만들어준다. 

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(fromId, toId, money);
    }
    • 스프링이 제공하는 트랜잭션 AOP를 적용하기 위해 @Transactional 어노테이션을 추가했다. 
    • 순수한 비즈니스 로직만 남기고, 트랜잭션 관련 코드는 모드 제거했다.

    @Transactional 어노테이션을 이용해서 트랜잭션의 관심사 분리가 되었다. 이를 통해 유지/보수가 용이한 코드를 만들 수 있게 되었다.

     

    테스트 코드 작성

    @Slf4j
    @SpringBootTest
    class MemberServiceV3Test_3 {
    
    ...
    
    }
    • @SpringBootTest
      • 스프링 AOP를 적용하려면 스프링 컨테이너가 필요하다. 왜나하면 스프링 AOP는 Bean PostProcessor를 이용해 프록시 객체를 바꿔치기 해 스프링 빈으로 등록해주기 때문이다.
      • 이 어노테이션이 있으면 테스트 시,스프링 부트를 통해 스프링 컨테이너를 생성한다. 그리고 테스트에서 @AutoWired등을 통해 스프링 컨테이너가 관리하는 빈들을 사용할 수 있다. 
    @Autowired
    private MemberRepositoryV3 memberRepository;
    @Autowired
    private MemberServiceV3_3 memberService;
    
    @TestConfiguration
    static class TestConfig {
    
        @Bean
        DataSource dataSource() {
            return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        }
    
        @Bean
        PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }
    
        @Bean
        MemberRepositoryV3 memberRepositoryV3() {
            return new MemberRepositoryV3(dataSource());
        }
    
        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    
    }
    • @TestConfiguration
      • 테스트 안에서 내부 설정 클래스를 만들어서 사용하면서 이 어노테이션을 붙이면, 스프링 부트가 자동으로 만들어 주는 빈들에 추가로 필요한 스프링 빈들을 등록하고 테스트를 수행할 수 있다. 

     

    AOP가 적용되었는지 확인하는 방법

    @Test
    void AopCheck() {
        System.out.println("MemberRepository : " +  memberRepository.getClass());
        System.out.println("MemberService : " +  memberService.getClass());
        Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isTrue();
    }

    다음과 같이 코드를 통해서 한번 확인해보자.

    • getClass() + AopUtils로 프록시 확인
      • 코드를 찍어보면 Service 계층은 CGLIB로 프록시가 적용된 것이 보인다.
      • Repository는 CGLIB가 찍히지 않은 것을 볼 수 있다. 또한, AopUtils를 활용해서 AOP가 적용된 객체인지를 확인하는 방법도 있다. 
    • @Transactional이 붙은 MemberService 계층만 프록시로 만들고, 그 프록시에서 MemberService를 호출한다. 그리고 그 MemberService는 내부적으로 MemberRepository를 가지고 있고, 이 때 MemberRepository를 호출한다.
      • 정리해보면 MemberService 프록시 → MemberService → MemberRepository 순으로 호출이 된다. 

     

     

    트랜잭션 AOP 적용 전체 흐름 정리

    1. 클라이언트가 비즈니스 로직을 호출한다. 
    2. 비즈니스 로직은 프록시 객체를 호출한다.
    3. 프록시 객체는 트랜잭션을 시작한다. 프록시 객체는 내부적으로 트랜잭션 매니저를 가지고 있다. 트랜잭션 매니저는 스프링 컨테이너를 통해 주입 받는다.
    4. 트랜잭션 매니저는 getTransaction()을 호출한다. 이 때, 트랜잭션 매니저는 데이터 소스로부터 커넥션을 가지고 온다. 가져온 커넥션에 setAutoCommit을 False로 설정해준다.
    5. 트랜잭션 매니저는 설정된 커넥션을 트랜잭션 동기화 매니저에게 저장해준다. 
    6. 프록시 객체는 실제 비즈니스 로직을 호출한다. 비즈니스 로직은 Repository를 호출한다.
    7. Repository는 트랜잭션이 있어야 DB와 통신을 할 수 있는데, 이 때 Repository는 내부적으로 가지고 있는 DataSourceUtils.getConnection()을 이용해 트랜잭션 동기화 매니저에게서 동기화 된 커넥션을 받아온다.
    8. 다 끝나면 반환되어서, 트랜잭션 프록시 객체에서 Commit / RollBack을 처리해준다. 

     

    선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리

    • 선언적 트랜잭션 관리
      • @Transactional 어노테이션 하나만 선언해서 트랜잭션을 처리하는 방법
    • 프로그래밍 방식의 트랜잭션 관리
      • TransactionManager / TransactionTemplate을 직접 작성해서 트랜잭션을 관리함.

    두 가지 방법에는 각각의 장점이 있다. @Transactional은 간편하게 이용할 수 있기 때문에 실무에서 많이 사용하는 방법이다. TranscationManager와 TransactionTemplate은 스프링 컨테이너가 없어도 사용할 수 있기 때문에 스프링 기술 의존성이 많이 줄어든다.

     

    정리

    • 스프링이 제공하는 @Transactional 덕분에 트랜잭션 관련 코드를 순수한 비즈니스 로직에서 제거할 수 있었다.
    • 개발자는 트랜잭션이 필요한 곳에 @Transactional 어노테이션을 하나 추가하기만 하면 된다.
    • @Transactional은 스프링 AOP 프록시 객체를 전달해준다. 이것은 Bean PostProcessor를 통해서 처리가 되기 때문에 스프링 컨테이너가 반드시 필요하다.

    댓글

    Designed by JB FACTORY