| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- Dead Letter Queue
- 서블릿 컨테이너
- MSA
- kafka
- AWS
- DLQ
- JPA
- redis
- 페이징
- @ComponentScan
- JWT
- Spring Data JPA
- DI
- Spring
- 스프링 부트
- Routing Key
- dockerhub
- mybatis
- JPQL
- 컨테이너
- 지연 로딩
- docker compose
- @Transactional
- Spring Container
- CORS
- JdbcTemplate
- 쿠버네티스
- docker
- securitycontextholderfilter
- Web
- Today
- Total
look-forest
스프링과 문제 해결 - 트랜잭션 본문
애플리케이션 구조 - 순수한 서비스 계층

시간이 흘러서 UI(웹) 와 관련된 부분이 변하고, 데이터 저장 기술을 다른 기술로 변경해도, 핵심 비즈니스 로직이 들어있는 서비스 계층은 최대한 변경없이 유지되어야 한다. 서비스 계층은 가급적 비즈니스 로직만 구현하고 특정 구현 기술에 직접 의존해서는 안된다. 이렇게 하면 향후 구현 기술이 변경될 때 변경의 영향 범위를 최소화 할 수 있다.
순수 JDBC로 구현 시 문제점
- 트랜잭션을 사용하기 위해서 javax.sql.DataSource, java.sql.Connection, java.sql.SQLException 같은 JDBC 기술에 의존해야 한다. 향후 JDBC에서 JPA 같은 다른 기술로 바꾸어 사용하게 되면 서비스 코드도 모두 함께 변경해야 한다.
- 핵심 비즈니스 로직과 JDBC 기술이 섞여 있어서 역할 분리가 되지 않는다.
- 똑같은 기능도 트랜잭션용 기능과 트랜잭션을 유지하지 않아도 되는 기능으로 분리해야 한다.(커넥션 전달 O/X)
- 트랜잭션 적용 코드를 보면 반복이 많다. try , catch , finally ...
스프링은 서비스 계층을 순수하게 유지하면서, 이러한 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다.
스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 한다.
- 트랜잭션 추상화
- 리소스 동기화
트랜잭션 추상화
트랜잭션 기능을 추상화하면, 서비스는 특정 트랜잭션 기술에 직접 의존하는 것이 아니므로 코드를 전혀 변경하지 않고, 트랜잭션 기술을 마음껏 변경할 수 있다.
스프링이 제공하는 트랜잭션 추상화 기술을 사용하면 된다.

package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
//이름이 getTransaction() 인 이유는 기존에 이미 진행중인 트랜잭션이 있는 경우 해당 트랜잭션에 참여할 수 있기 때문
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
트랜잭션 동기화
트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야 한다.
스프링은 트랜잭션 동기화 매니저를 제공한다. 이것은 쓰레드 로컬( ThreadLocal )을 사용해서 커넥션을 동기화해준다.
트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용한다.
트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화 할 수 있다.
따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 된다.

동작 방식
- 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작한다. (DataSourceUtils)
- 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관한다. (쓰레드로컬)
- 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다.
- 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고,
커넥션도 닫는다.
참고
쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여된다. 따라서 해당 쓰레드만 해당 데이터에 접근 가능.
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
- DataSourceUtils.getConnection()
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.
- DataSourceUtils.releaseConnection()
- 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다
동작 흐름
트랜잭션 매니저 1 - 트랜잭션 시작

- 서비스 계층에서 transactionManager.getTransaction() 을 호출해서 트랜잭션을 시작한다.
- 트랜잭션을 시작하려면 먼저 커넥션이 필요하다. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성
- 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작한다.
- 커넥션을 트랜잭션 동기화 매니저에 보관한다.
- 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다. (멀티 쓰레드 환경에 안전하게 커넥션을 보관)
트랜잭션 매니저 2 - 로직 실행

- 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출한다.
- 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요하다. 리포지토리는 DataSourceUtils.getConnection() 을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. (같은 커넥션을 사용하고, 트랜잭션도 유지)
- 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.
트랜잭션 매니저 3 - 트랜잭션 종료

- 비즈니스 로직이 끝나고 트랜잭션을 종료한다. 트랜잭션은 커밋하거나 롤백하면 종료된다.
- 트랜잭션을 종료하려면 동기화된 커넥션이 필요하다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
- 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
- 전체 리소스를 정리한다.
- 트랜잭션 동기화 매니저를 정리한다. 쓰레드 로컬은 사용후 꼭 정리해야 한다.
- con.setAutoCommit(true) 로 되돌린다. 커넥션 풀을 고려해야 한다.
- con.close() 를 호출해셔 커넥션을 종료한다. 커넥션 풀을 사용하는 경우 con.close() 를 호출하면 커넥션 풀에 반환
트랜잭션 문제 해결 - 트랜잭션 템플릿
트랜잭션을 사용하는 로직을 살펴보면 다음과 같은 패턴이 반복되는 것을 확인할 수 있다.

다른 서비스에서 트랜잭션을 시작하려면 try , catch , finally 를 포함한 성공시 커밋, 실패 시 롤백 코드가 반복될 것이다.
이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비즈니스 로직 뿐이다.
이럴 때 템플릿 콜백 패턴을 활용하면 이런 반복 문제를 깔끔하게 해결할 수 있다. 템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 TransactionTemplate 이라는 템플릿 클래스를 제공한다.
public class TransactionTemplate {
private PlatformTransactionManager transactionManager; //내부에서 트랜잭션 매니저 사용
public <T> T execute(TransactionCallback<T> action){..} //응답값 있을때
void executeWithoutResult(Consumer<TransactionStatus> action){..} //응답값 없을때
}

트랜잭션 템플릿의 기본 동작
- 비즈니스 로직이 정상 수행되면 커밋.
- 언체크 예외가 발생하면 롤백. 그 외의 경우 커밋. (체크 예외의 경우에는 커밋. 관리가 되므로)
한계
트랜잭션 템플릿 덕분에, 트랜잭션을 사용할 때 반복하는 코드를 제거할 수 있었다.
하지만 이곳은 서비스 로직인데 비즈니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
이렇게 비즈니스 로직과 트랜잭션을 처리하는 기술 로직이 한 곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 코드를 유지보수하기 어려워진다.
이럴 때 스프링 AOP를 통해 프록시를 도입하면 문제를 깔끔하게 해결할 수 있다.
트랜잭션 문제 해결 - 트랜잭션 AOP
프록시를 사용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져간다. 그리고 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다.

스프링이 제공하는 트랜잭션 AOP
스프링은 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공한다. 스프링 부트를 사용하면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다. 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. 스프링의 트랜잭션 AOP는 이 애노테이션을 인식해서 트랜잭션 프록시를 적용해준다.
@Transactional 애노테이션은 메서드에 붙여도 되고, 클래스에 붙여도 된다. 클래스에 붙이면 외부에서 호출 가능한 public 메서드가 AOP 적용 대상이 된다


스프링 부트의 자동 리소스 등록
스프링 부트는 데이터소스와 트랜잭션 매니저를 스프링 빈에 자동으로 등록한다.
- 현재 등록된 라이브러리를 보고 적절한 트랜잭션 매니저 생성
- application.properties 에 있는 속성을 사용해서 DataSource( HikariDataSource )를 생성
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
참고로 개발자가 직접 데이터소스, 트랜잭션 매니저를 빈으로 등록하면 스프링 부트는 데이터소스 자동 등록 X
참고 자료 & 이미지 출처
스프링 DB 1편 - 데이터 접근 핵심 원리 (김영한 님)
'Spring > Spring 데이터 접근 - 핵심 원리' 카테고리의 다른 글
| 스프링과 문제 해결 - 예외 처리, 반복 (0) | 2024.08.16 |
|---|---|
| 자바 예외 이해 (0) | 2024.08.15 |
| 트랜잭션 이해 (0) | 2024.08.12 |
| 커넥션풀과 데이터소스 이해 (0) | 2024.08.12 |
| JDBC 이해 (0) | 2023.09.24 |