이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.
TransactionTemplate을 통한 코드 개선
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromId, toId, money);
transactionManager.commit(transaction);
} catch (Exception e) {
transactionManager.rollback(transaction); // 실패 시, 롤백
throw new IllegalStateException(e);
}
}
- Transaction을 사용하는 경우를 살펴보면 계속 반복되는 패턴이 있는 것을 확인할 수 있다.
- Try ~ Catch문을 처리한다.
- transactionManager.getTranscation()을 한다
- transcationManager.commit()을 한다.
- transactionManager.rollBack()을 한다.
위와 같이 반복되는 패턴의 문제점은 트랜잭션을 처리할 메서드 / 서비스 계층이 많을수록 동일한 코드가 늘어난다는 것이다. 반복되는 부분을 메서드로 일괄적으로 뽑아내면 좋을 것 같다. 그렇지만 위 코드에서 메서드로 뽑아내는 것은 쉽지 않다. 반복되는 부분 사이에 비즈니스 로직이 존재하기 때문이다.
이런 반복 문제를 해결할 때, 주로 템플릿 콜백 패턴(Strategy 패턴)이 사용된다. 스프링은 개발자들이 편리하게 이 부분을 처리할 수 있도록 Transaction을 위한 템플릿 콜백 패턴을 구현해두었고, 개발자는 이것을 가져와서 쓰기만 하면 된다. 이 때, 스프링이 제공해주는 것이 TranscationTemplate이다.
TranscationTemplate 구성
public class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action) throws TransactionException {}
void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException {}
}
스프링은 Transcation의 반복 코드 문제 해결을 위해 Transcation 처리를 위한 Template Callback 패턴을 제공한다. TranscationTemplate을 사용하면 된다.
- execute() : 응답값이 있을 때 사용한다.
- executeWithouResult() : 응답값이 없을 때 사용한다.
가장 핵심 코드는 위 두 가지다. 위 두 가지를 이용해서 복잡했던 코드를 단순하게 만들 수 있다. 위 코드들은 Try ~ Catch 문 + Commit / RollBack + 자원 회수를 적절히 처리해준다. 개발자는 비즈니스 로직에만 집중하면 된다.
트랜잭션 템플릿 사용 로직
@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_2 {
private final TransactionTemplate transactionTemplate;
private final MemberRepositoryV3 memberRepository;
// 계좌이체 로직
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
// 트랜잭션 시작
transactionTemplate.executeWithoutResult(status -> {
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
....
}
- TranscationTemplate을 사용하게 되면 TransactionTemplate을 DI해서 사용하면 된다.
- TransacionTemplate은 트랜잭션 시작 / Commit / RollBack / Try ~ Catch / Connection의 Close를 해결해준다.
- TranscationTemplate의 execute / executeWithoutResult로 비즈니스 로직을 트랜잭션 내에서 처리한다.
- execute를 할 때, 람다 함수로 시작하는데 이 때 TransactionStatus를 넘겨준다.
- 앞서 TranscationManager를 이용할 때, Commit / Rollback에 TranscationStatus가 필요했던 경우를 생각하면 된다.
트랜잭션 템플릿의 기본 동작은 다음과 같다.
- 정상 수행 시 트랜잭션을 커밋한다.
- 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다. 체크 예외는 커밋한다.
- 코드에서 예외를 처리하기 위해 Try ~ Catch가 들어갔다. bizLogic() 메서드를 호출하면, SQLException 체크 예외를 던진다. 해당 람다에서는 체크 예외를 밖으로 던질 수 없기 때문에 언체크 예외로 바꾸어 던지도록 예외를 전환했다.
코드 작성
MemberServiceV3_2
@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_2 {
private final TransactionTemplate transactionTemplate;
private final MemberRepositoryV3 memberRepository;
// 계좌이체 로직
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
// 트랜잭션 시작
transactionTemplate.executeWithoutResult(status -> {
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
...
}
- MemberServiceV3_2 코드를 다음과 같이 작성한다.
- TranscationManager를 사용했을 때는 Transcation에서 Connection을 얻어오고, Commit / RollBack / Resource Release를 모두 개발자가 반복적으로 구현해주어야 했다.
- 현재 코드에서는 TranscationTemplate을 주입받아 사용하고, TranscationTemplate은 내부적으로 TranscationManager를 사용한다. TransactionTemplate은 Connection과 관련된 일체의 작업을 처리해준다.
MemberServiceV3Test_2
@Slf4j
@SpringBootTest
class MemberServiceV3Test_2 {
private MemberRepositoryV3 memberRepository;
private MemberServiceV3_2 memberService;
@BeforeEach
void beforeEach() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
memberRepository = new MemberRepositoryV3(dataSource);
memberService = new MemberServiceV3_2(transactionTemplate, memberRepository);
}
...
}
- 테스트 코드는 TranscationManager → TransactionTemplate을 주입하도록 beforeEach 메서드만 변경한 것을 빼면 동일하다.
TransacionTemplate 정리
- TransacionTemplate는 트랜잭션을 사용할 때 반복하는 Try ~ Catch / Commit / Rollback 등을 일괄적으로 처리해준다.
- TransationTemplate은 한 가지 문제점을 남긴다.
- Service 계층이 Transaction에 의존한다는 것이다. → txTemplate.execute()
- 서비스 계층에서 트랜잭션을 쓰지 않는 것으로 변경된다면, 위 코드를 모드 수정해줘야한다.
- 어플리케이션을 구성하는 로직을 핵심 / 부가 기능으로 구분하면, Transcation은 부가 기능이다.
- 핵심 + 부가 기능이 함께 존재하면 코드의 복잡도가 올라가 유지 보수가 어렵다.
정리하면 TranscationTemplate은 코드 반복을 줄여주는 좋은 기술이지만, 여전히 서비스 계층이 트랜잭션 기술에 종속적이기 때문에 코드 유지보수 관점에서 어려움이 있다는 것이다. 어떻게 이 문제를 해결할 수 있을까?
'Spring > Spring DB' 카테고리의 다른 글
Spring DB : 스프링부트와 자동 리소스 등록 (0) | 2022.04.28 |
---|---|
Spring DB : 트랜잭션 AOP (0) | 2022.04.28 |
Spring DB : 트랜잭션 동기화 (0) | 2022.04.28 |
Spring DB : 트랜잭션 문제점 + 스프링의 트랜잭션 추상화 (0) | 2022.04.28 |
Spring DB : 트랜잭션 적용 (0) | 2022.04.28 |