트랜잭션 추상화
트랜잭션 매니저 PlatformTransactionManager
스프링이 제공하는 트랜잭션 추상화 인터페이스이다.
데이터 접근 기술에 따른 트랜잭션 구현체도 대부분 만들어져서, 편리하게 사용하면 된다.
- 구현체 종류
`DataSourceTransactionManager` : JDBC 트랜잭션 관리
`JpaTransactionManager` : JPA 트랜잭션 관리
`HibernateTransactionManager` : 하이버네이트 트랜잭션 관리
`EtcTransactionManager` : 기타 트랜잭션 관리
> 참고 <
스프링 5.3부터는 JDBC 트랜잭션을 관리할 때 DataSourceTransactionManager 를 상속받아서 약간의 기능을 확장한 JdbcTransactionManager 를 제공하며, 기능은 비슷하다.
- 인터페이스
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()` : 트랜잭션 롤백
트랜잭션 동기화 매니저 TransactionSynchronizationManager
`쓰레드 로컬` 을 사용하여 커넥션을 동기화해주어 트랜잭션을 시작할 수 있다.
- 동작 방식
1. 서비스 계층에서 `transactionManager.getTransaction()` 을 호출해서 트랜잭션을 시작한다.
2. 트랜잭션을 시작하여 트랜잭션 매니저가 DataSource 를 통해 커넥션을 만든다.
3. 커넥션을 수동 커밋 모드로 변경하여 실제 데이터베이스 트랜잭션을 시작한다.
4. 트랜잭션 매니저가 내부에서 트랜잭션 동기화 매니저를 사용하여 쓰레드 로컬에 커넥션을 보관한다.
(이때, 쓰레드 로컬은 각각의 쓰레드마다 별도의 저장소가 부여되어 해당 쓰레드만 해당 데이터에 접근할 수 있다.)
5. 리포지토리는 `DataSourceUtils.getConnection()`를 사용하여 트랜잭션 동기화 매니저를 통해 쓰레드 로컬에 보관된 커넥션을 꺼내 사용한다.
6. 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 쓰레드 로컬에 보관된 커넥션을 통해 트랜잭션을 종료하고, 자동 커밋 모드로 변경한 뒤 커넥션을 닫는다.
JDBC 예시 코드
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DataSourceUtils.getConnection(dataSource);
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
throw e;
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, dataSource);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, dataSource);
}
}
트랜잭션 동기화 매니저를 사용하려면 DataSourceUtils 를 사용해야 한다.
`DataSourceUtils.getConnection()`
: 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환하고, 없으면 새로운 커넥션을 생성하여 반환한다.
`DataSourceUtils.releaseConnection()`
: 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지한다.
: 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.
public class MemberService {
private final PlatformTransactionManager transactionManager;
private final MemberRepository memberRepository;
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);
}
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
`private final PlatformTransactionManager transactionManager`
: 트랜잭션 매니저를 주입받는다.
: 구현체로 JDBC 구현체인 DataSourceTransacionManager 구현체를 외부에서 주입한다.
(JPA 기술이라면 JpaTransactionManager 를 주입한다.)
`TransactionStatus status`
: 현재 트랜잭션의 상태 정보가 포함되어 있다.
`new DefaultTransactionDefinition()`
: 트랜잭션과 관련된 옵션을 지정할 수 있다.
DataSourceTransacionManager 주입 방법
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberRepository = new MemberRepository(dataSource);
memberService = new MemberServiceV(transactionManager, memberRepository);
DataSource 인터페이스의 JDBC 구현체인 DriverManagerDataSource 를 통해 dataSource 를 생성하고,
PlatformTransactionManager 인터페이스의 JDBC 구현체인 DataSourceTransactionManger 를 생성하여
의존성 주입을 해준다.
문제점
하지만 아직도 서비스 계층에서 try - catch 와 commit, rollback 하는 구조가 반복되어 있다.
이를 트랜잭션 템플릿을 통해 해결할 수 있다.
트랜잭션 템플릿
TransactionTemplate
스프링은 템플릿 콜백 패턴을 적용을 위한 템플릿 클래스를 제공한다.
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action){..}
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
`execute()` : 응답 값이 있을 때 사용
`excuteWithoutResult()` : 응답 값이 없을 때 사용
예시 코드
public class MemberService {
private final TransactionTemplate txTemplate;
private final MemberRepository memberRepository;
public MemberService(PlatformTransactionManager transactionManager, MemberRepository 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);
}
});
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
`TransactionTemplate` 을 사용하려면 `transactionManager` 가 필요하므로, 생성자에서 주입받아 생성했다.
- 동작 방식
1. `txTemplate.excuteWithoutResult()` 를 통해 트랜잭션을 시작한다.
2. 비즈니스 로직이 정상 수행되면 커밋하고,
3. 언체크 예외가 발생하면 롤백한다.
(체크 예외가 발생하면 커밋한다.)
(이때, SQLException 이라는 체크 예외를 IllegalStateException 이라는 언체크 예외로 변경해 준다.)
문제점
아직도 서비스 로직이지만 비즈니스 로직에 트랜잭션 처리 기술 로직이 포함되어 있다.
이 문제를 트랜잭션 AOP 를 사용하면 트랜잭션 기술을 사용하지만, 비즈니스 로직과 트랜잭션 처리 기술 로직을 분리할 수 있다.
트랜잭션 AOP @Transactional
프록시를 통한 문제 해결
트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져가고, 트랜잭션을 시작한 후에 실제 서비스를 대신 호출한다.
따라서 서비스 계층에는 순수한 비즈니스 로직만 남길 수 있다.
@Transactional 예시 코드
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
이렇게 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션을 달아주면 된다.
public class TransactionProxy {
private MemberService target;
public void accountTransfer() {
TransactionStatus status = transactionManager.getTransaction(..);
try {
target.accountTransfer();
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new IllegalStateException(e);
}
}
}
내부적으로 이런 식으로 작동한다.
@Transactional
- 언체크 예외가 발생해야 롤백된다. (체크 예외가 발생하면 커밋)
- 클래스 단위에 붙이면, public 메서드가 AOP 적용 대상이 된다.
- @Transaction 이 하나라도 있으면 트랜잭션 프록시 객체가 만들어져서, 트랜잭션 프록시 객체가 대신 주입되어 사용된다.
데이터 소스와 트랜잭션 매니저 자동 등록
스프링 부트가 적용된 의존성과 설정 파일을 기반으로
`DataSource` 와 `PlatformTransactionManager` 를 자동으로 스프링 빈으로 등록해 준다.
JDBC or JPA 예시
`spring-boot-starter-jdbc` 또는 `spring-boot-starter-data-jpa` 의존성이 존재해야 하고,
// application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
위 설정이 있으면 스프링 부트는 DataSource 를 자동 생성 및 등록한다.
기술 | DataSource | TransactionManager | 빈 이름 |
JDBC | `HikariDataSource` | `DataSourceTransactionManager` | `dataSource`, `transactionManager` |
JPA | `HikariDataSource` | `JpaTransactionManager` | `dataSource`, `transactionManager` |
트랜잭션 매니저의 구현체는 사용 기술에 따라 구현체가 달라진다.
(빈 이름은 항상 dataSource, transactionManager 이다.)
이때, 데이터 소스와 트랜잭션 매니저를 직접 빈으로 등록하면, 스프링 부트는 자동으로 등록하지 않는다.
출처 | 스프링 DB 1(김영한) - 인프런
'💠프로그래밍 언어 > Spring' 카테고리의 다른 글
[Spring] JdbcTemplate 상세 사용법 정리 (1) | 2025.04.30 |
---|---|
[Spring] 에러 코드로 예외 처리하기, 스프링 예외 추상화 사용하기 (0) | 2025.04.23 |
[Spring] JDBC 와 트랜잭션, 커넥션 풀 (0) | 2025.04.22 |
[Spring] 서블릿 파일 업로드와 스프링 파일 업로드 (0) | 2025.04.06 |
[Spring] 컨버터와 포맷터 (컨버전 서비스, 스프링 기본 제공 포맷터) (0) | 2025.04.06 |