트랜잭션 적용 문제 해결

바그다드·2023년 3월 25일
0

  • 앞서 트랜잭션을 적용하다보니 서비스 로직이 지저분해지는 문제가 발생했다.


위의 사진을 보면 프레젠테이션은 서블릿이나 MVC 같은 기술에 의존하고
데이터 접근 계층은 JDBC나 JPA같은 기술에 의존한다.

하지만 서비스 계층은 특정 기술에 의족하지 않고, 순수 자바 코드로 작성해야 한다.
컨트롤러나 리포지토리는 의존한느 기술의 트렌드가 바뀌면 수정해야 하나, 서비스 계층은 핵심 비즈니스 로직이 들어있어 최대한 변경 없이 유지해야 한다

  • 때문에 트랜잭션 추상화가 필요하다.

트랜잭션 추상화

  • 스프링에서는 트랜잭션 추상화를 트랜잭션 매니저가 해결해준다
  • 스프링은 트랜잭션 동기화 매니저를 제공하는데, 이것은 쓰레드 로컬을 사용해 커넥션을 동기화해준다.
    - 트랜잭션 매니저 내부에서 트랜잭션 동기화 매니저를 사용한다.

위의 동작 방식은 다음과 같은데
1. 트랜잭션 메니저가 커넥션 생성 후 트랜잭션 시작
2. 트랜잭션 매니저는 커넥션을 트랜잭션 동기화 매니저에 저장
3. 리포지토리는 트랜잭션 동기화 매니저에 있는 커넥션을 꺼내서 사용
4. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 있는 커넥션을 통해 트랜잭션을 종료하고, 커넥션을 닫음

1. 트랜잭션 매니저

// 리포지토리
    private void close(Connection con, Statement stmt, ResultSet rs) {

        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        DataSourceUtils.releaseConnection(con, dataSource);

    }

    private Connection getConnection() throws SQLException {
        // 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
        Connection con = DataSourceUtils.getConnection(dataSource);
        log.info("get connection={}, class={}", con, con.getClass());

        return con;
    }
  • 리포지토리에서는 DataSourceUtils를 사용하고
    getConnection()을 수행하면, 트랜잭션 동기화 매니저가 커넥션을 반환하고,
    커넥션이 없는 경우 새로 생성해서 반환한다.
  • releaseConnection()을 사용하더라도 트랜잭션을 위해 커넥션을 유지해준다.
    - 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우에는 커넥션을 닫는다.
// 서비스
	private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;

    public void accountTrancefer(String fromId, String toId, int money) throws SQLException {

        //트랜잭션 시작
        TransactionStatus status = transactionManager
        								.getTransaction(new DefaultTransactionDefinition());

        try {
            //비즈니스 로직
            bizLogic(fromId, toId, money);
            transactionManager.commit(status); // 성공시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status); // 실패시 롤백
            throw new IllegalStateException(e);
        }
    }
  1. 트랜잭션 매니저 주입
    • 위의 코드에서는 @RequiredArgsConstructor를 사용하고 있어 생성자가 없음
    • DataSourceTransactionManager은 구현체인데 현재는 JDBC를 사용하기 때문에 이것을 사용
    • JPA를 사용하는 경우 JPATransactionManager로 앞에 이름만 바뀜
  2. 트랜잭션 시작
    • transactionManager.getTransaction()
    • TransactionStatus status에는 트랜잭션의 상태 정보가 저장되어 있다.
  3. 비즈니스 로직 실행
  4. 성공 시 커밋
  5. 실패 시 롤백
// 컨트롤러
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,USERNAME, PASSWORD);
// 트랜잭션 매니저 생성
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberRepository = new MemberRepositoryV3(dataSource);
memberService = new MemberServiceV3_1(transactionManager, memberRepository);
  • 트랜잭션 매니저의 인자로 DataSource를 필요로 함

  • 그런데 위의 코드를 보면 getConnection, commit, rollback 등 반복되는 코드가 있음

2. 트랜잭션 템플릿

// 서비스
    private final TransactionTemplate txTemplate;
    private final MemberRepositoryV3 memberRepository;

    public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
        this.txTemplate = new TransactionTemplate(transactionManager);
        this.memberRepository = memberRepository;
    }


    public void accountTrancefer(String fromId, String toId, int money) throws SQLException {

        txTemplate.executeWithoutResult((status) ->{
            try {
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }
  • 트랜잭션 템플릿을 이용해 반복되는 코드가 줄어든걸 확인할 수 있다.
  • 하지만 서비스 로직에 비즈니스 로직뿐 아니라 트랜잭션 처리 로직이 포함되어 있다.
  • 이것을 관심사로 나눠보면 핵심 관점과 부가 관점이 한 곳에 모여있다.

3. 트랜잭션 AOP

프록시를 사용하여 트랜잭션 처리 로직과 비즈니스 로직을 처리하는 객체를 나눌 수 있다.

// 서비스
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
	bizLogic(fromId, toId, money);
}
  • @Transactional 어노테이션을 이용해 프록시를 간단하게 적용할 수 있다.
  • @Transactional은 메소드나 클래스에 붙일 수 있다.
  • 하지만 프록시를 스프링에서 지원하기 때문에 필요한 빈을 컨테이너에 등록해야 한다.
	@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());
        }
    }

DataSource, 트랜잭션 매니저 자동 등록

  • application.properties에 DataSource생성
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
  • 스프링 빈은 이를 참고해 datasource와 트랜잭션 매니저를 컨테이너에 등록
  • 어떤 트랜잭션 매니저를 컨테이너에 등록할지는 라이브러리를 보고 판단한다.
    - jdbc라면 DatasourceTransactionManager, jpa라면 JpaTransactionManager
	@Autowired
    private MemberRepositoryV3 memberRepository;
    @Autowired
    private MemberServiceV3_3 memberService;

    @TestConfiguration
    static class TestConfig{

        private final DataSource dataSource;

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

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

        @Bean
        MemberServiceV3_3 memberServiceV3_3() {
            return new MemberServiceV3_3(memberRepositoryV3());
        }
    }
  • DataSource와 트랜잭션 매니저를 빈으로 등록하는 코드가 생략되었다.
profile
꾸준히 하자!

0개의 댓글