스프링/5. 스프링 DB-1

28. 트랜잭션 AOP 적용.

sdafdq 2023. 10. 1. 21:02

트랜잭션 AOP를 적용해 보겠다 (프록시)

 

@Slf4j
public class MemberServiceV3_3 {
    private final MemberRepositoryV3 memberRepository;

    public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
        this.memberRepository = memberRepository;
    }


    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

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


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

정말 간단해졌다.

그냥 트랜잭션 할 곳에 @Transactional 이것만 붙여주면 된다.

물론 클래스에다가 붙이면 그 클래스의 모든 메소드가 트랜잭션 적용이 된다.

 

 

@Slf4j
@SpringBootTest
public class MemberServiceV3_3Test {
    private static final String MEMBER_A = "memberA";
    private static final String MEMBER_B = "memberB";
    private static final String MEMBER_EX = "ex";

    @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 memberRepository(){
            return new MemberRepositoryV3(dataSource());
        }

        @Bean
        MemberServiceV3_3 memberSerivce(){
            return new MemberServiceV3_3(memberRepository());
        }
    }

    @AfterEach
    private void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("AOP 체크")
    public void aopCheck(){
        assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() throws SQLException {
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney() - transferMoney);
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney() + transferMoney);
    }

    @Test
    @DisplayName("이체 중 예외 발생")
    void accountTransferEx() throws SQLException {
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_EX, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        assertThatThrownBy(()->memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney))
                .isInstanceOf(IllegalStateException.class);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney());
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney());
    }
}

테스트 코드이다. 뭔가 덕지덕지 많이 붙었다.

우선 @SpringBootTest.

프록시는 스프링에서 제공해주는 기능이다. 따라서, 스프링이 올라와 있는 환경에서 써야 한다.

@SpringBootTest는 이렇게 테스트환경에서 스프링을 띄워주는 역할을 한다. 물론 필요한 기본 빈 들을 자동등록 해 주고.

 

@AutoWired로 자동 의존주입으로 지정해 주고,

@TestConfiguration 이거는 테스트에 관한 설정을 해 주는 곳인데, 주로 빈 등록할 때 쓰는 것 같다. 아니면 딱 빈 등록할 때 쓰는건가?

 

참고로, DataSourceTransactionManager는 우리 눈에 보이기에는 어떤 곳에도 의존관계를 주입시키는 곳이 보이지 않지만,

우리가 사용했던 @Transactional 이 프록시 기능은 결국 스프링 빈에 등록된 트랜잭션 매니저를 찾아서 사용하기 때문에 우리가 등록 해 주어야 한다.

 

그 외에는 다 비슷하고, 

AopCheck() 테스트 보면,

이게 프록시 객체인지 아닌 지 확인하는데, 

이게 프록시가 어떤 구조냐면,

이런 구조가 아니라,

 

 

어..

추상적으로 이런 구조와 유사하다.

 

그러니까, 스프링이 저 본 클래스 코드를 보고 @Transactional 이게 프록시이므로 프록시인걸 확인 하면,

본 클래스를 상속받은 프록시 + 본클래스인 클래스를 만든다.

 

그래서 저번에 얘기했던 

try{

    본로직

    commit()

}catch (e){

    rollback()

}

 

이런 것 처럼 감싸서 사용하는거다. 뭐 주위에 감싸는 내용은 프록시의 내용에 따라 당연히 다르다.

그래서 저 프록시 클래스를 출력해보면,

EnhancerBySpringCGLIB$$코드

직역해보면 강화된 스프링에 의해 CGLIB? 이런거고 코드는 아마 자동으로 저렇게 생성되는거니 중복을 피하기 위해서 한 것인듯?

 

CGLIB는 Code Generator Library

의 약자로, 직역하면 코드 생성기 라이브러리이고,

클래스의 바이트 코드를 조작하여 프록시 객체를 생성하도록 도와주는 라이브러리라고 한다.

 

바이트코드... ㅋㅋㅋㅋ

 

여튼 뭐 정상작동 된다.

 

 

 

 

 

 

전체 흐름이다. 클라이언트가 서버에 요청을 하면 요청에 따라 우리의 구현에 따라 그게 프록시 일 수도 있다. 그게 트랜잭션이라고 생각하고,

그럼 클라이언트 -> 서버 -> 컨트롤러 -> 서비스(프록시인) 이렇게 오게 되는데,

본 클래스를 상속받은 프록시 + 본 클래스의 로직을 실행한다. 내부는 프록시에 따라 프록시 로직 사이에 본 클래스의 로직이 적절하게 잘 들어가 있을 것이다. 이번 경우 트랜잭션이므로 트랜잭션의 로직 사이에 적절하게 본 클래스의 로직이 잘 들어가 있다.

 

여튼 @Transactional이므로 맨 처음 서버 자체를 실행 했을 때 저 @Transactional을 보고 프록시 + 본 클래스인 클래스를 만들어 빈에 등록해 놨을 것이다.

 

여튼 본클래스를 한번 감싼 그 프록시 + 본 클래스를 사용하게 되고, 적절히 잘 Transaction + 비즈니스 로직이 알아서 잘 실행이 된다.

 

클라이언트 요청이 오면 스프링 컨테이너에서 트랜잭션 매니저 빈을 획득하고,

그 트랜잭션 매니저를 이용해 (트랜잭션 매니저 생성할 때는 커넥션 생성을 담당하는 dataSource도 들어간다.)

커넥션을 생성하고, 

오토커밋모드를 꺼주고,

트랜잭션 동기화 매니저(쓰레드 로컬)에 그 커넥션을 보관해 준다.

 

이러면 이제 트랜잭션 할 준비가 끝난 것이다.

그 다음 실제 비즈니스 로직을 호출 하는데,

우리가 이때 커넥션 가져오는 걸

public Connection getConnection() throws SQLException {
    Connection con = DataSourceUtils.getConnection(dataSource);
    return con;
}

이걸로 해 놔서 DataSourceUtils.getConnection()은 쓰레드 로컬을 뒤져보고 거기서 커넥션을 가져오는 거기 때문에, 서로 간 커넥션을 공유할 수 있다. (마치 쫌 내부의 휘발성 DB비슷한 존재일려나.. ) 

비즈니스 로직에 따라, 리포지토리를 호출하여 그 리포지토리가 DB의 커넥션을 얻어 DB와 소통할 때, 이 커넥션은 쓰레드 로컬에서 가져오는 것 이고,

여기 트랜잭션 동기화 매니저에서 가져온 DB는 

private void close(Connection con, Statement stmt, ResultSet rs){
    JdbcUtils.closeResultSet(rs);
    JdbcUtils.closeStatement(stmt);
    DataSourceUtils.releaseConnection(con, dataSource);
}

DB close를 이렇게 해놨는데,

DataSourceUtils.releaseConnection()은 트랜잭션 동기화 매니저에서 가져온 DB는 close() 하지 않고 그냥 넘어간다.

다른 DB들은 그냥 close하던지, 풀에 반환한다.

 

그래서, 그냥 넘어가기 때문에 누군가는 close(혹은 풀에 반환)를 해 줘야 한다.

그걸 AOP 프록시에서

commit혹은 rollback 했을 시 close(혹은 반환) 한다.

에러없이 잘 수행됐으면 commit,

만약에 에러를 어디선가 내뿜었으면 catch로 에러를 잡고, rollback 수행.

 

 

'스프링 > 5. 스프링 DB-1' 카테고리의 다른 글

30. 자바 예외  (0) 2023.10.02
29. 스프링 부트 자동 리소스 등록  (0) 2023.10.01
27. 트랜잭션 AOP  (0) 2023.10.01
26. 트랜잭션 템플릿  (0) 2023.10.01
25. 트랜잭션 매니저  (0) 2023.10.01