트랜잭션(Transaction) 적용

Woo0·2024년 3월 25일
post-thumbnail

앞에서 트랜잭션(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));

MemberServiceV1memberRepository.update 는 트랜잭션이 없으면 기본적으로 오토 커밋이다. 예외가 발생하기 전의 update 메소드만 실행된다.

문제점

  • 이체 중 예외가 발생하게 되면 memberA의 금액은 10,000원에서 8,000원으로 2,000원 감소하지만 memberEx의 금액은 그대로 10,000으로 남아있다.
  • 결과적을 memberA의 돈만 2,000원 감소했다.

트랜잭션 적용

DB 트랜잭션을 사용해서 앞서 발생한 문제점을 해결해보자. 애플리케이션에서 트랜잭션을 어떤 계층에서 시작하고, 어디에서 커밋해야할까?

  • 트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작해야 한다.
  • 트랜잭션을 시작하려면 connection이 필요한데 서비스 계층에서 connection을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
  • 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 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);
        }
    }
}
  • 계좌이체 서비스 로직에서 트랜잭션 처리를 살펴보기 위해 findByIdupdate 코드를 변경했다.
  • 커넥션 유지가 필요한 두 메서드는 파라미터로 넘어온 커넥션을 사용했다.
  • 두 메서드는 커넥션을 닫으면 안되고 서비스 로직이 끝날 때 서비스 계층에서 트랜잭션을 종료하고 닫아야 한다.
@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 사용하고, 결과를 매핑하고..
-유사한 코드의 반복이 너무 많다.

스프링은 서비스 계층을 순수하게 유지하면서, 지금까지 이야기한 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다. 스프링을 사용해서 이런 문제들을 하나씩 해결해보자.


출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리 (김영한)

profile
실패를 두려워하지 않는 백엔드 개발자가 되기 위해 노력하고 있습니다.

0개의 댓글