
앞에서 트랜잭션(Transaction)에 대해 알아봤다. 트랜잭션을 적용하면서 생기는 문제점을 살펴보고 해결해 보자.
formId의 회원을 조회해서 toId의 회원에게 money만큰의 돈을 계좌이체 하는 로직이다. 먼저 트랜잭션 없이 계좌이체 비즈니스 로직만 구현해 보고 문제점을 살펴보자.
@RequiredArgsConstructor
public class MemberServiceV1 {
private final MemberRepositoryV1 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Member formMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, formMember.getMoney() - money); //트랜잭션 없으면 기본적으로 자동 커밋
validation(toMember);
memberRepository.update(toId, formMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
accountTransfer() : fromId의 회원을 조회해서 toId 회원에게 money만큼의 돈을 계좌이체 하는 로직
validation() : 예외 상황을 테스트해보기 위해 toId가 "ex"인 경우 예외를 발생
//이체 중 예외 발생
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_EX, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000));
MemberServiceV1의 memberRepository.update 는 트랜잭션이 없으면 기본적으로 오토 커밋이다. 예외가 발생하기 전의 update 메소드만 실행된다.
문제점
DB 트랜잭션을 사용해서 앞서 발생한 문제점을 해결해보자. 애플리케이션에서 트랜잭션을 어떤 계층에서 시작하고, 어디에서 커밋해야할까?

connection이 필요한데 서비스 계층에서 connection을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 트랜잭션 처리를 해보자
@Slf4j
public class MemberRepositoryV2 {
private final DataSource dataSource;
public MemberRepositoryV2(DataSource dataSource) {
this.dataSource = dataSource;
}
// 데이터 조회 - 커넥션을 파라미터로 전달받아 같은 커넥션이 사용되도록 유지
public Member findById(Connection con, String memberId) throws SQLException {
//...con을 파라미터로 전달받음
try {
//...
} catch (SQLException e) {
//...
} finally {
//connection 시작, 닫는 결정은 Service 계층에서
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
// 데이터 수정 - 커넥션을 파라미터로 전달받아 같은 커넥션이 사용되도록 유지
public void update(Connection con, String memberId, int money) throws SQLException {
//...con을 파라미터로 전달받음
try {
//...
} catch (SQLException e) {
//...
} finally {
//connection 시작, 닫는 결정은 Service 계층에서
JdbcUtils.closeStatement(pstmt);
}
}
}
@RequiredArgsConstructor
@Slf4j
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); //트랜잭션 시작
//...비지니스 로직 (toId가 "ex"인 경우 예외를 발생)
con.commit(); //로직 정상 수행시 커밋
} catch (Exception e) {
con.rollback(); //실패시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); //커넥션 풀 고려(기본이 auto 커밋)
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
con.setAutoCommit(false) : 자동 커밋 off, 트랜잭션 시작
release(con) : 커넥션 풀을 사용하는 경우 con.close() 호출시 커넥션이 종료되는 것이 아니라 풀에 반납된다. 그러므로 기본 값인 자동 커밋 모드로 변경하는 것이 안전하다.
문제점
1. JDBC 구현 기술이 서비스 계층에 누수되는 문제
-JDBC 구현 기술이 서비스 계층에 누수되는 문제 : 서비스 계층은 특정 기술에 종속되지 않아야 한다. 그렇기 때문에 데이터 접근 계층으로 JDBC 관련 코드를 모았는데, 트랜잭션을 적용하면서 결국 서비스 계층에 JDBC 구현 기술의 누수가 발생했다.
-트랜잭션 동기화 문제 : 같은 트랜잭션을 유지하기 위해 커넥션을 파라미터로 넘겨야 한다
-트랜잭션 적용 반복 문제 : try, catch, finally
2. 예외 누수
-데이터 접근 계층의 JDBC 구현 기술 예외가 서비스 계층으로 전파된다.
-SQLException은 JDBC 전용 기술이다. 향후 JPA나 다른 데이터 접근 기술을 사용하면, 그에 맞는 다른 예외로 변경해야 하고, 결국 서비스 코드도 수정해야 한다.
3. JDBC 반복 문제
-try, catch, finally..
-커넥션 열고, PreparedStatement 사용하고, 결과를 매핑하고..
-유사한 코드의 반복이 너무 많다.
스프링은 서비스 계층을 순수하게 유지하면서, 지금까지 이야기한 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다. 스프링을 사용해서 이런 문제들을 하나씩 해결해보자.