트랜잭션 템플릿

트랜잭션 매니저가 커넥션의 획득, 커밋 및 롤백, 커넥션의 반납을 추상화했다면, 트랜잭션 템플릿은 거기서 한발자국 더 나아가 비즈니스 로직을 제외한 트랜잭션의 시작과 끝을 숨겨놓은 객체를 의미한다.

인터페이스

public class TransactionTemplate extends DefaultTransactionDefinition {
    private PlatformTransactionManager transactionManager;
    public <T> T execute(TransactionCallback<T> action) { ... }
    void executeWithoutResult(Consumer<TransactionStatus> action) { ... }
}
  • 생성자를 통해 트랜잭션 매니저를 주입받는다.
  • execute()
    • action 인자로 넘긴 함수형 인터페이스가 반환값이 있는 경우 사용한다.
    • TransactionCallback
  • executeWithoutResult
    • action 인자로 넘긴 함수형 인터페이스가 반환값이 없는 경우 사용한다.

사용법

DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);

// 트랜잭션 시작
txTemplate.executeWithoutResult((status) -> {
    try {
        // 비즈니스 로직
        bizLogic(fromId, toId, money);
    } catch (SQLException e) {
        throw new IllegalStateException(e);
    }
});

함수형 인터페이스와 람다를 사용해서 비즈니스 로직을 템플릿의 인자에 전달하면, Callback을 이용해 구현된다.

트랜잭션 프록시

트랜잭션 템플릿이 함수형 인터페이스를 이용해서 트랜잭션의 시작과 끝을 숨겼다면, 트랜잭션 프록시는 비즈니스 로직을 대상으로 하는 Proxy 클래스를 이용해서 트랜잭션의 시작과 끝을 숨겼다.

프록시 패턴 적용 예시

public class TransactionProxy {

    private Service target;
    
    public TransactionProxy(Service target) {
        this.target = 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 interface Service {
    public void logic();
}

public class MemberService implements Service {
    @Override
    public void logic() {
        bizLogic();
    }
}

TransactionProxy serviceProxy = new TransactionProxy(new MemberService());

프록시를 도입하면 위와 같이 트랜잭션 처리 로직을 프록시가 담당하고 서비스 레이어는 비즈니스 로직만 남아있게 되었다. 이제 여기서 프록시를 스프링 빈에 등록하고 빈 후처리기를 이용해 서비스의 트랜잭션을 불러오면 스프링 방식의 @Transactional이 된다.

트랜잭션 AOP

스프링은 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공한다. 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.

이제 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.

@Transactional

org.springframework.transaction.annotation.Transactional

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Reflective
public @interface Transactional {

    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};

}

사용

@Slf4j
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository repository;

    /**
     * fromId -> toId
     * money 만큼의 돈을 전송
     */
    @Transactional
    public void accountTransfer(
            String fromId,
            String toId,
            int money
    ) throws SQLException {
        // 비즈니스 로직
        bizLogic(fromId, toId, money);
    }

    /**
     * fromId -> toId
     * money 만큼의 돈을 전송
     */
    private void bizLogic(
            String fromId,
            String toId,
            int money
    ) throws SQLException {
        Member fromMember = repository.findById(fromId);
        Member toMember = repository.findById(toId);

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

    /**
     * 대상 회원의 ID가 ex 인지 검증
     */
    private void validation(
            Member toMember
    ) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체중 예외 발생");
        }
    }
}

@Transactional 애노테이션은 메서드에 붙여도 되고, 클래스에 붙여도 된다. 클래스에 붙이면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다. 이제 MemberService는 프록시 빈(CGLIB)에 의해 관리된다.

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

@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 =

스프링 부트는 데이터 소스와 트랜잭션 매니저를 자동으로 스프링 빈으로 등록시켜준다.

  • DataSource dataSource() {}
    • URL, USERNAME, PASSWORD는 application.properties로 이관됐다.
    • 이에 해당하는 설정을 보고 싶다면 공식 홈페이지를 참고하자.
  • PlatformTransactionManager transactionManager() {}
    • 어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 스프링 부트가 판단한다.
    • JPA: JpaTransactionManager
    • JDBC: DataSourceTransactionManager
profile
백엔드 개발자 지망생

0개의 댓글