- 이번 포스팅에서는 스프링에서 트랜잭션을 적용하기 위해서 어떠한 과정을 거쳐왔는지를 예제를 통해 학습해보자.
- 예제는 간단하게 계좌이체를 하는 코드이다.
@RequiredArgsConstructor
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false);
bizLogic(con, fromId, toId, money);
con.commit(); //성공시 커밋
} catch (Exception e) {
con.rollback();
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
//비즈니스 로직
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); //connection pool을 고려
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
- 첫 번쨰 함수는 accountTransfer() 이다.
- 이 함수는 파라미터로 보내는 사람, 받는 사람, 금액 등을 받아서 계좌이체를 실행한다.
- 계좌이체를 하는 로직은 bizLogic 으로 넘기고 호출한다.
- 중요한 bizLogic 보다는 데이터베이스 커넥션과 트랜잭션과 관련된 코드가 훨씬 많음을 알 수 있다.
- SQLException 체크 예외가 발생할 수 있어서 선언되어있다.
- 다음은 Repository 함수이다.
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from Member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
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 member_id=" + memberId);
}
} catch (SQLException e) {
log.error("error", e);
throw e;
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
// JdbcUtils.closeConnection(con);
}
}
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate(); //영향받은 row 수를 반환
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("DB error");
throw e;
} finally {
JdbcUtils.closeStatement(pstmt);
}
}
- 여기서도 dataSource 로부터 커넥션을 가져오고 이 커넥션으로 PreparedStatement 를 각각 만들어서 이를 실행한다.
문제점들
- 간단하게 만들어 보았는데 코드에는 여러 문제가 있음을 알 수 있다.
- 트랜잭션문제
- JDBC 구현 기술이 서비스에 누수 : 서비스 계층은 순수해야한다. JDBC 관련 코드가 서비스에까지 전파되었는데 만약 JDBC가 JPA로 변경되면 대부분의 코드를 수정해야한다.
- 트랜잭션 동기화 문제 : 계좌이체 로직은 하나의 트랜잭션으로 실행되어야 한다. 따라서 같은 트랜잭션을 유지하기 위해 같은 Connection을 유지해야하는데 그러기 위해 Connection을 매개변수로 전달한다.
- 트랜잭션 적용 반복 문제 : 트랜잭션과 관련된 코드가 너무 많다. try, catch, finally ...
- 예외 누수 문제 : SQLException은 JDBC 전용 체크 예외이다. JDBC => JPA로 변경되면 예외도 변경된다. 기술이 변경 되면 서비스코드까지 수정해야하는 문제가 발생한다.
- JDBC 반복 문제 : 데이터 접근계층에서도 try, catch, finally 등 JDBC 관련 코드가 너무 중복된다.
스프링이 위의 문제들을 어떻게 해결해나가는지 예제 코드를 통해서 살펴보자
트랜잭션 추상화
- 현재 서비스 계층에서 트랜잭션을 사용할 때 JDBC 기술에 의존하고 있다. 이 때 JDBC => JPA로 기술변경이 발생하면 트랜잭션 관련 코드를 모두 수정해야한다.
- 이 방법을 해결하기 위해 서비스가 의존하는 대상을 트랜잭션의 추상화(인터페이스)에만 의존하게 만든다.
public interface TxManager{
begin();
commit();
rollback();
}
- 의존성 주입으로 트랜잭션 관리 기술을 넣어주면 서비스 코드이 변경없이 트랜잭션 기술을 변경할 수 있다.
스프링의 트랜잭션 추상화
- 스프링은 이미 트랜잭션 추상화 기술을 만들어두었다.
- PlatformTransactionManager
- 그리고 이에 대한 구현체도 각 데이터 접근 기술마다 제공한다.
- 우리는 그냥 가져다 사용하면 된다!!
트랜잭션 동기화
- 다음은 트랜잭션 동기화 문제이다. 이전 코드에서는 하나의 커넥션을 유지하기 위해 커넥션을 매개변수로 전달했고 그 커넥션으로 트랜잭션을 관리했다.
- 스프링이 제공해주는 트랜잭션 메니저는 트랜잭션 추상화 기능 뿐만아니라 트랜잭션 리소스 동기화까지 해준다.
- 리소스동기화
- 커넥션을 유지하여 하나의 트랜잭션을 유지해준다.
- 트랜잭션 동기화 매니저는 쓰레드로컬을 사용해서 커넥션을 동기화한다.
- 트랜잭션이 시작되면 트랜잭션 매니저는 커넥션을 생성하고 해당 커넥션을 동기화매니저(TransactionSynchronizationManager)에 보관한다.
- 필요할떄 동기화 매니저르 통해서 꺼내서 커넥션을 가져와서 사용한다.
트랜잭션 동기화 - 리소스 획득과 반환 예제 코드
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
//주의 트랜잭션 동기화를 사용하려면 DatasourceUtils르 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() throws SQLException {
// transactinosynch
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
- DataSourceUtils.getConnection()
- 트랜잭션 동기화 매니저에 커넥션이 있으면 해당 커넥션을 반환
- 없으면 만들어서 반환
- DataSourceUtils.releaseConnection()
- 커넥션을 close 하면 커넥션을 유지할 수 없다.
- releaseConnection()을 사용하면 커넥션을 닫지 않는다.
- transactionManager.commit() or rollback 할때 커넥션으로 커밋 롤백하고
- 동기화매니저의 리소스를 정리한다.
트랜잭션 문제 해결 - 트랜잭션 템플릿
- 트랜잭션 로직은 반복되는 패턴이 있음을 볼 수 있다.
- 트랜잭션 시작
- try catch
- commit rollback 등등..
- 달라지는 부분은 비즈니스 로직뿐이다.
이러한 문제는 템플릿 콜백 패턴으로 해결할 수 있다.
txTemplate.executeWithoutResult((status)-> {
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
- 트랜잭션 템플릿 덕분에 트랜잭션을 시작하고 커밋 롤백코드르 제거할 수 있다.
- 트랜잭션 템플릿의 기본 동작
- 비즈니스 로직이 정상 수행되면 커밋한다.
- 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다.
- 코드를 많이 제거할 수 있었지만 그럼에도 트랜잭션이라는 부가기능을 제공하는 코드가 아직 남아있다.
- 이 문제를 어떻게 해결할 수 있을까?
트랜잭션 문제 해결 - 트랜잭션 AOP 이해
- 프록시 도입 전
- 서비스에 비즈니스 로직과 트랜잭션 로직이 섞여있다.
- 프록시 도입 후
- 트랜잭션 프록시가 트랜잭션 처리로직을 가져간다.
- 트랜잭션을 시작한 후에 비즈니스 로직을 실행한다.
트랜잭션 문제 해결 - 트랜잭션 AOP 적용
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
트랜잭션 문제 해결 - 트랜잭션 AOP 정리
![[Pasted image 20220617225521.png]]
- 선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리
- @Transactional vs 트랜잭션 코드 작성
- 선언적 트랜잭션 관리가 프로그래밍 방식에 비해 매우 편하고 실용적이다.
- 프로그래밍 방식의 트랜잭션 관리는 스프링 컨테이너나 AOP 기술이 없이 간단히 사용할 수 있지만 실무에서는 대부분 선언적 트랜잭션 관리를 사용한다.
스프링 부트의 자동 리소스 등록
- 스프링 부트는 데이터 접근과 트랜잭션 관련하여 리소스를 자동등록한다.
- 데이터 소스 자동등록
- 스프링부트는 다음과 같은 application.properties(yml) 파일의 정보를 읽어서 데이터소스를 자동으로 등록한다.
- spring.datasource.url=jdbc:h2:tcp://localhost/~/test
- spring.datasource.username=sa
- spring.datasource.password=
- 디폴트는 히카리
- 스프링부트는 다음과 같은 application.properties(yml) 파일의 정보를 읽어서 데이터소스를 자동으로 등록한다.
- 트랜잭션 매니저 자동등록
- 스프링 부트는 적절한 트랜잭션 매니저(PlatformTransactionManager)를 자동으로 스프링 빈에 등록해준다
- 어떤 트랜잭션 매니저를 사용할지는 등록된 라이브러리를 보고 판단하게된다.
Reference
- 스프링 DB 1편 - 데이터 접근 핵심 원리 by 김영한
'Spring' 카테고리의 다른 글
스프링 부트 웹 애플리케이션 만들기 with Spring Reactive Web (0) | 2022.08.11 |
---|---|
스프링 영속성 관리 (영속성 컨텍스트) (0) | 2022.07.08 |
스프링 AOP 내부호출 문제와 한계 (0) | 2022.06.16 |
스프링 AOP (0) | 2022.06.12 |
빈 후처리기, BeanPostProcessor (0) | 2022.06.11 |