가장 과거의 방식부터 지금에 이르기까지 흐름을 따라가보자.
v0. 초기 트랜잭션 코드
트랜잭션 적용 방법의 예시
/**
* 트랜잭션 - 파라미터 연동, 풀을 고려한 종료
*/
@Slf4j
@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 validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); //커넥션 풀 고려
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
}
트랜잭션 시작 : con.setAutoCommit(false);
성공시 커밋 : con.commit();
실패시 롤백 : con.rollback();
그리고 con.close()를 사용하게 되면 커넥션 풀을 사용중일 때 커넥션이 종료되는 것이 아니라 풀에 반납된다.
따라서 이 전에 커밋모드를 다시 true로 돌려주어야 한다. con.setAutoCommit(true);
근데 트랜잭션을 사용하게 되면서 아래와 같은 문제점이 발생한다.
- JDBC 구현 기술이 서비스 계층에 누수됨. 즉 서비스 계층이 순수하지 못하게 된다. 변화에 대응 x
- 트랜잭션 동기화 문제: 같은 트랜잭션을 유지하기 위해 커넥션을 파라미터로 넘김(위에 bizLogic 참고). 이러면 레포지터리에서는 트랜잭션용 기능과 트랜잭션이 필요없는 기능으로 분리해야 함
- 트랜잭션 적용 반복 문제 : 트랜잭션 코드 자체에 반복이 너무 많다. ex) try, catch, finally ...
위의 문제점을 하나씩 해결해보자!
v1. 트랜잭션 매니저를 사용하여 트랜잭션을 추상화
우선 JDBC 구현 기술이 서비스 계층에 누수되어 있으면 향후 JPA 방식으로 변경될 때 코드 수정이 발생해야 된다.
(*JPA에서는 엔티티매니저를 사용하여 트랜잭션을 관리한다.)
따라서 트랜잭션을 추상화시켜야 한다.
스프링에선 PlatformTransactionManager라는 이름으로 인터페이스를 제공한다.
심지어 구현체도 대부분 만들어두었다.
게다가 이 트랜잭션매니저는 리소스 동기화, 즉 하나의 트랜잭션 내에 동일한 커넥션을 유지할 수 있도록 한다.
이전처럼 커넥션을 파라미터로 넘겨주지않아도 된다.
트랜잭션매니저를 사용한 코드
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_1 {
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 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("이체중 예외 발생");
}
}
}
v2. TransactionTemplate을 사용하여 반복 없애기
@Slf4j
public class MemberServiceV3_2 {
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 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("이체중 예외 발생");
}
}
}
트랜잭션 템플릿 덕분에 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 모두 제거되었다.
트랜잭션 템플릿의 기본 동작은 다음과 같다.
- 비즈니스 로직이 정상 수행되면 커밋한다.
- 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다. (체크 예외면 커밋)
코드에서 예외를 처리하기 위해 try~catch 가 들어갔는데, bizLogic() 메서드를 호출하면 SQLException 체크 예외를 넘겨준다. 해당 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크 예외로 바꾸어 던지도록 예외를 전환했다.
v3. 트랜잭션 AOP 이용 - @Transactional
@Transactional을 통해 서비스에는 비즈니스 로직만 남기고 트랜잭션 코드를 제거할 수 있게 되었다.
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_3 {
private final MemberRepositoryV3 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("이체중 예외 발생");
}
}
}
+ v4. 스프링 예외 변환기 사용
레포지토리 계층에서 스프링 예외 변환기를 사용하게 되면, 데이터베이스 접근 시 발생하는 오류(기술 종속적인 오류 ex) JDBC, JPA)를 스프링이 정의한 오류로 변환해준다.
이때 스프링이 정의한 오류는 RuntimeException을 상속받기 때문에 모두 언체크에러다.
그럼 이 경우 언체크에러이기 때문에 서비스 계층에서 throw를 사용해서 에러를 던져줄 필요가 없다.
즉 예외로부터 벗어난 순수 서비스 코드가 작성가능해진다.
/**
* 예외 누수 문제 해결
* SQLException 제거
*
* MemberRepository 인터페이스 의존
*/
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV4 {
private final MemberRepositoryV3 memberRepository;
@Transactional
public void accountTransfer(String fromId, String toId, int money) {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) {
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("이체중 예외 발생");
}
}
}
-----------------------------------------
참고: 김영한님 스프링 DB-1편 강의
댓글