트랜잭션(Transaction) 적용 2

Woo0·2024년 4월 1일
post-thumbnail

트랜잭션 매니저

트랜잭션 추상화

스프링은 트랜잭션 추상화 기술을 제공한다. 데이터 접근 기술에 따른 트랜잭션 구현체도 대부분 제공하기 때문에 사용하기만 하면 된다.

PlatformTransactionManager : 스프링 트랜잭션 추상화의 핵심 인터페이스
-> org.springframework.transaction.PlatformTransactionManager

public interface PlatformTransactionManager extends TransactionManager {
	TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

스프링이 구현체도 제공하기 때문에 구현체를 주입하여 사용하면된다. PlatformTransactionManager 인터페이스와 구현체를 포함해서 트랜잭션 매니저라고 줄여서 이야기 하도록 하자.

@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_1 {

    private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;


    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        //트랜잭션 시작 & release 자동
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            //비지니스 로직
            bizLogic(fromId, toId, money);
            transactionManager.commit(status);  //로직 정상 수행시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status);  //실패시 롤백
            throw new IllegalStateException(e);
        }
    }

    private void bizLogic(String fromId, String toId, int money) throws SQLException {
        //...
    }

    private void validation(Member toMember) {
        //...
    }

}

PlatformTransactionManager

  • JDBC에 의존하던 기존의 코드를 트랜잭션 추상화 인터페이스로 변경했음
  • 지금은 JDBC 기술을 사용하기 때문에 DataSourceTransactionManager 구현체를 외부에서 주입받아야 함

getTransaction

  • 트랜잭션 시작
  • TransactionStatus를 반환하는데 이후 트랜잭션을 커밋, 롤백할 때 필요

트랜잭션 동기화

트랜잭션 매니저는 트랜잭션 추상화 외에 리소스 동기화 역할도 수행한다. 트랜잭션을 유지하기 위해서는 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다. 이전에는 파라미터로 커넥션을 전달하는 방법을 사용했는데 많은 단점을 확인할 수 있었다.

TransactionSynchronizationManager : 트랜잭션 동기화 매니저
-> org.springframework.transaction.support.TransactionSynchronizationManager

1. 트랜잭션을 시작하려면 커넥션이 필요, 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작
2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용, 따라서 파라미터로 커넥션을 전달하지 않아도 됨
4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션도 닫음

@Slf4j
public class MemberRepositoryV3 {

    private final DataSource dataSource;

    public MemberRepositoryV3(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    // 1)데이터 저장
    // 2)데이터 조회
    // 3)데이터 수정
    // 4)데이터 삭제
    
    private void close(Connection con, Statement pstmt, ResultSet rs) {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(pstmt);
        //주의! 트랜잭션 동기화를 사용하려면 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()

  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환
  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환

DataSourceUtils.releaseConnection()

  • 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지
  • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫음

문제점

//트랜잭션 시작

try {
	//비지니스 로직
    //로직 정상 수행시 커밋
} catch (Exception e) {
	//실패시 롤백
}
  • 다른 서비스에서 트랜잭션을 시작하면 비즈니스 로직을 제외한 다른 부분이 반복된다.
  • 템플릿 콜백 패턴을 활용하여 반복 문제를 해결해보자.

트랜잭션 템플릿

public class TransactionTemplate {
	private PlatformTransactionManager transactionManager;
 
 	public <T> T execute(TransactionCallback<T> action){..}
 	void executeWithoutResult(Consumer<TransactionStatus> action){..}
}

execute() : 응답 값이 있을 때 사용
executeWithoutResult() : 응답 값이 없을 때 사용

템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 TransactionTemplate라는 템플릿 클래스를 제공한다.

@Slf4j
public class MemberServiceV3_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 accountTransfer(String fromId, String toId, int money) throws SQLException {
        
        txTemplate.executeWithoutResult((status) -> {
            try {
            	//비지니스 로직
                bizLogic(fromId, toId, money);
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            }
        });
    }
    //...
}
  • TransactionTemplate을 사용하기 위해서는 transactionManager가 필요하다.
  • 트랜잭션 템플릿은 비즈니스 로직이 정상 수행되면 커밋하고 언체크 예외가 발생하면 롤백한다.
  • 트랜잭션 템플릿를 이용하여 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드를 모두 제거했지만 여전히 문제점이 존재한다.

문제점

  • 서비스 로직에 트랜잭션을 처리하는 기술 로직(TransactionTemplate)이 함께 포함되어 있다.
  • 서비스 로직은 가급적 핵심 비즈니스 로직만 있어야 하는데 트랜잭션 기술을 사용하려면 어쩔 수 없이 트랜잭션 코드가 필요하다.

트랜잭션 AOP

  • 지금까지 트랜잭션을 편리하게 처리하기 위해서 트랜잭션 추상화를 도입하고, 추가로 반복적인 트랜잭션 로직을 해결하기 위해 트랜잭션 템플릿도 도입했다.
  • 트랜잭션 템플릿 덕분에 트랜잭션을 처리하는 반복 코드는 해결할 수 있었지만 서비스 계층에 순수한 비즈니스 로직만 남긴다는 목표는 달성하지 못했다.
  • 스프링 AOP를 통해 프록시를 도입하면 문제를 깔끔하게 해결할 수 있다.

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

try {
    //비즈니스 로직
    bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백
	throw new IllegalStateException(e);
}

프록시 도입 전 : 서비스에 비즈니스 로직과 트랜잭션 처리 로직이 함께 섞여있다.

//트랜잭션 프록시
public class TransactionProxy {

	private MemberService target;
 
 	public void logic() {
		//트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(..);
        try {
        	//실제 대상 호출
            target.logic();
            transactionManager.commit(status); //성공시 커밋
        } 
        catch (Exception e) {
        	transactionManager.rollback(status); //실패시 롤백
        	throw new IllegalStateException(e);
        }
	}
}

//서비스
public class Service {

	public void logic() {
		//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
		bizLogic(fromId, toId, money);
	}
}

프록시 도입 후 : 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져가고 서비스 계층에는 순수한 비즈니즈 로직만 남길 수 있다.

스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있다.
-> 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. org.springframework.transaction.annotation.Transactional

@Transactional 적용

@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 {
        bizLogic(fromId, toId, money);
    }
	//...
}
  • 스프링이 제공하는 트랜잭션 AOP를 적용하기 위해 @Transactional 추가하면서 순수한 비즈니스 로직은 남기고, 트랜잭션 관련 코드는 모두 제거했다.
  • @Transactional 애노테이션은 메서드에 붙여도 되고, 클래스에 붙여도 된다. 클래스에 붙이면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다.

트랜잭션 AOP 정리

  • 스프링이 제공하는 선언적 트랜잭션 관리 덕분에 트랜잭션 관련 코드를 순수한 비즈니스 로직에서 제거할 수 있다.
  • 개발자는 트랜잭션이 필요한 곳에 @Transactional 애노테이션만 추가하면 된다. 나머지는 스프링 트랜잭션 AOP가 자동으로 처리해준다.

스프링 부트 자동 리소스 등록

@Bean
DataSource dataSource() {
	return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
 
@Bean
PlatformTransactionManager transactionManager() {
	return new DataSourceTransactionManager(dataSource());
}

지금까지 데이터소스와 트랜잭션 매니저를 개발자가 직접 스프링 빈으로 등록해서 사용했다. 스프링을 통해 해당 부분이 자동화되었고 생략할 수 있다.

  • 데이터소스 자동 등록
# application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

스프링 부트가 기본으로 생성하는 데이터소스는 커넥션풀을 제공하는 HikariDataSource이다. 커넥션풀과 관련된 설정도 application.properties를 통해서 지정할 수 있다.

  • 트랜잭션 매니저 자동 등록

스프링 부트는 적절한 트랜잭션 매니저를 자동으로 스프링 빈에 등록한다. 어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단하는데, JDBC를 기술을 사용하면 DataSourceTransactionManager를 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager를 빈으로 등록한다.


출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리 (김영한)

profile
실패를 두려워하지 않는 백엔드 개발자가 되기 위해 노력하고 있습니다.

0개의 댓글