Spring Transaction
JDBC만으로 트랜잭션을 사용하면 몇가지 문제가 발생한다.
1. 서비스 계층에서 트랜잭션 코드로 인해 JDBC 코드가 작성된다.
- JDBC 코드에 의존하다보니 이후 JPA 등과 같이 다른 데이터 접근 기술을 사용한다면 코드 유지보수 하기가 힘들어진다.
- 또한 서비스 계층에는 다른 특정 구현 기술에 의존하면 안된다. 오직 순수한 자바 로직이 들어가야 한다.
2. 트랜잭션 동기화 문제
- 트랜잭션에 대한 커넥션을 유지하기 위해 리포지토리에 파라미터로 커넥션을 넘겨줬었다.
- 이렇게 되면 리포지토리 코드에 트랜잭션용 기능 코드와 일반 기능 코드를 나누어서 작성해줘야 한다.
3. 트랜잭션 적용 반복 문제
- 트랜잭션으로 인해 상당히 많은 반복 코드가 발생하게 된다.
- try, catch, finally, commit(), rollback(), close() 등...
Spring은 이러한 문제들을 해결하기 위해 다음과 같은 해결책을 제시한다.
1. 서비스 계층에서 트랜잭션 코드로 인해 JDBC 코드가 작성된다.
--> 트랜잭션 매니저(PlatformTransactionManager), 트랜잭션 AOP(@Transactional)
2. 트랜잭션 동기화 문제
--> 트랜잭션동기화 매니저(DataSourceUtils)
3. 트랜잭션 적용 반복 문제
--> 트랜잭션 템플릿(TransactionTemplate)
지금부터 하나하나 알아보도록 하겠다.
트랜잭션 매니저(PlatformTransactionManager)
현재 서비스 계층에서는 JDBC에 의존하고 있다.
이렇게 되면 이후 JPA 같은 다른 데이터 접근 기술을 도입하려고 할 때 코드를 수정하기가 매우 어려워진다.
이러한 점을 해결해주는 것이 바로 트랜잭션 매니저(PlatformTransactionManager)다.
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;
}
트랜잭션에 있어서 핵심적인 세가지 메서드를 제공한다.
- getTransaction(): 트랜잭션 시작 + 커넥션도 생성해줌
- commit(): 커밋
- rollback(): 롤백
이를 통해 서비스 계층에서는 특정 JDBC 코드에 의존하지 않고 트랜잭션 코드를 사용할 수 있다.
예시코드
private final MemberRepositoryV3 memberRepository;
private final PlatformTransactionManager transactionManager;
public void accountTransfer(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);
}
}
- transactionManager를 DI를 통해 주입을 받는다면 특정 데이터 접근 기술에 의존하지 않고 트랜잭션 코드를 사용할 수 있다.
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
- JDBC를 사용하는 경우 DataSourceTransactionManager가 주입된다.
- 생성할 때 파라미터로 데이터소스를 넣어줘야 한다.
트랜잭션 동기화 매니저(DataSourceUtils)
트랜잭션의 커넥션을 유지하기 위해 리포지토리 계층에 이 커넥션을 파라미터로 넘겨줬었다.
이렇게 되면 리포지토리에는 커넥션을 파라미터로 받는 트랜잭션 용 메서드, 받지 않는 일반 메서드를 모두 작성해야 했기에 코드가 매우 지저분했다.
이러한 점을 해결해주는 것이 트랜잭션 동기화 매니저이다.
- 트랜잭션 매니저가 전달받은 데이터소스를 통해 커넥션을 생성해 트랜잭션 동기화 매니저에 보관한다.
- 이후 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.
이렇게 되면 파라미터로 커넥션을 전달하지 않아도 일관된 커넥션을 사용할 수 있다. - 이후 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫는다.
예시코드
Connection con = DataSourceUtils.getConnection(dataSource);
- DataSourceUtils.getConnection()
- 트랜잭션 동기화 매니저에 보관된 커넥션이 있다면 꺼내서 사용.
- 없다면 커넥션을 새로 생성
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
- DataSourceUtils.releaseConnection()
- 트랜잭션을 위해 동기화된 커넥션은 바로 닫지 않고 유지한다.
- 트랜잭션을 위해 동기화된 커넥션이 없다면 닫는다.
- con.close() 혹은 JdbcUtils.closeConnection()을 통해 닫으면 안된다.
트랜잭션 템플릿(TransactionTemplate)
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
bizLogic(fromId, toId, money); // 비즈니스 로직
transactionManager.commit(status); // 성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); // 실패시 롤백
throw new IllegalStateException(e);
}
서비스 계층에 트랜잭션을 실행하는 코드를 보면 위의 코드가 반복된다.
이렇게 트랜잭션을 사용하는데 있어서 발생하는 반복을 해결해주는 것이 트랜잭션 템플릿이다.
TransactionTemplate 클래스
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action){..}
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
- TransactionManager를 주입받아야 한다.
- execute(): 트랜잭션의 결과로 반환할게 있는 경우
- executeWithoutResult(): 트랜잭션의 결과로 반환할게 없는 경우
예시코드
private final MemberRepositoryV3 memberRepository;
private final TransactionTemplate txTemplate;
public MemberServiceV3_2(MemberRepositoryV3 memberRepository, PlatformTransactionManager transactionManager) {
this.memberRepository = memberRepository;
this.txTemplate = new TransactionTemplate(transactionManager);
}
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);
}
});
}
- executeWithoutResult()
- 인자로 받은 람다식안에서 예외가 발생하지 않는다면 commit을 실행
- 인자로 받은 람다식안에서 언체크 예외가 발생한다면 rollback을 실행(체크예외인 경우는 commit)
트랜잭션 AOP(@Transactional)
트랜잭션 템플릿을 도입해도 여전히 서비스 계층에는 데이터관련 코드가 존재한다.
이러한 점을 해결하는 것이 트랜잭션 AOP이다.
서비스 계층에서 시작되던 트랜잭션 부분을 트랜잭션 프록시로 따로 떼어냈다.
이렇게 하면 서비스 계층은 완전하게 순수한 자바 비즈니스로직만이 남게된다.
예시코드
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
- @Transactional 어노테이션만 붙여주면 끝난다.
- 이 어노테이션이 붙어있으면 스프링이 알아서 트랜잭션 매니저를 찾고 커넥션을 생성하고 트랜잭션을 실행하고 종료하고 커넥션을 닫아준다.
- 트랜잭션이 예외없이 성공하면 Commit하고 런타임 예외가 발생하면 Rollback한다.
- 당연히 이 방법은 스프링 기술이므로 데이터소스, 트랜잭션 매니저등은 빈으로 등록되어 있어야 한다.
하지만 스프링부트는 이 부분까지 자동화해준다. application.properties에 다음부분을 추가하면 된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
이렇게 추가만 하면 스프링부트가 알아서 데이터소스를 생성하고 트랜잭션 매니저를 생성한다.
스프링부트는 기본적으로 데이터소스는 HikariDataSource를 주입하고
트랜잭션 매니저의 경우 JDBC를 사용하는 경우엔 DataSourceTransactionManager를 주입한다.
정리
지금까지 JDBC의 트랜잭션 코드로 인해 발생하는 문제점들을 스프링의 기술들로 어떻게 해결하는지 알아봤다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
'Spring > Spring Data' 카테고리의 다른 글
[Spring Data] SQL Mapper VS ORM (0) | 2024.06.05 |
---|---|
[Spring Data] 예외로 인한 의존성과 스프링의 예외 추상화 (0) | 2024.04.30 |
[Spring Data] JDBC 트랜잭션 (0) | 2024.04.29 |
[Spring Data] 커넥션 풀과 데이터 소스 (0) | 2024.04.29 |
[Spring Data] JDBC 이해하기 (0) | 2024.04.29 |