본문 바로가기

Web Sever 개발과 CS 기초/스프링

JDBC를 사용한 Trasaction 처리와 이해

관련 내용

해당 프로젝트 깃허브

 

<JDBC, Spring - Transaction 처리에 대한 완벽한 이해>

  • (현재 글)순수 JDBC만을 사용한 Transaction 사용 방법 알아보기

JDBC를 사용한 Trasaction 처리와 이해

  • 순수 JDBC-Transaction 문제 Spring TransactionManger로 해결하기 - 트랜잭션 템플릿, 트랜잭션 AOP 사용

JDBC Trasaction문제 Spring으로 해결하기

  • Spring Transaction AOP 동작 과정과 주의 사항 알아보기

스프링 Trasaction AOP 동작 과정과 주의 사항

  • Spring Transaction 전파 원리 이해하기

Spring Transaction 전파 이해하기

  • Spring을 사용한 JDBC 예외 누수 문제 해결하기

Spring을 사용한 JDBC 예외 누수 문제 해결하기

  • 자바 예외에 대한 이해

자바 예외에 대한 이해

개요와 목적

이번 시간에는 Java의 JDBC 기술에서 단순히 DB만 사용하는 것이 아니라, 트랜잭션 처리를 하는 방법에 대해서 알아본다.

트랜잭션과 DB Lcok에 대한 이론적 이해는 아래 블로그 글에서 확인할 수 있다.

[백엔드/DB 지식] - Transaction과 ACID 쉽게 이해하기(+MySQL transaction 설정 방법)

[백엔드/DB 지식] - DB Lock에 대한 이해와 MySQL Lock의 특징

JDBC에서 Transaction 사용의 핵심

  • 동일한 커넥션(세션)을 유지하기 위해서, 서비스 메소드 내에서 커넥션을 획득한다.
  • con.setAutoCommit(false)으로 트랜잭션을 시작한다.
  • 모든 SQL을 실행하는 Repository 메소드에 동일한 커넥션을 파라미터로 전달한다.
  • 성공 시 con.commit()을 한다.
  • 실패 시 con.rollback()을 한다.
  • 마지막 서비스 계층에서 con.close()를 통해 커넥션을 종료한다.

코드를 통해 JDBC Transaction 처리 알아보기

transaction이 필요한 상황 가정하기

memberA와 memberEx는 각각 돈 10000원을 가지고 있다. memberA가 memberEx에게 2000원을 보내는 상황이다. 그런데 시스템 상 문제가 발생하여 해당 처리가 롤백되어야한다. 트랜잭션이 제대로 작동한다면, memberA와 memberEx 돈이 10000원이 그대로 남아있어야 한다.

transaction을 사용하려면 하나의 세션, 커넥션 안에서 이루어져야 한다.

트랜잭션을 적용하려면 하나의 세션을 유지해야 한다. 하나의 세션을 유지하려면 동일한 커넥션을 사용해서 SQL 쿼리를 보내야 한다. 결국 레포지토리 계층이 아니라, 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.

<Connection을 파라미터로 받는 방식으로 변경 - 서비스 계층에서 동일한 커넥션으로 SQL 요청을 날리기 위해서>

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\MemberRepositoryV1.java

KIMHWANG\jdbc\src\main\java\hello\jdbc\repository\MemberRepositoryV2.java

//MemberRepositoryV1.java
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 {
	    //메소드 내에서 새로운 connection을 획득한다.
	    //이러면 서비스 코드 내에서 동일한 connection을 유지할 수 없다.
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

//->
//MemberRepositoryV2.java
//동일한 connection을 유지하기 위해서 파라미터로 connection을 받는다.
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);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            //connection은 여기서 닫지 않는다. 서비스 계층에서 한번에 닫는다.
            JdbcUtils.closeStatement(pstmt);
        }
    }

transaction이 동작하는 계좌 이체 서비스 코드

트랜잭션이 적용되지 않은 서비스 코드는 깃허브 MemberServiceV0.java에서 볼 수 있다.

KIMHWANG\jdbc\src\main\java\hello\jdbc\service\MemberServiceV2.java

public class MemberServiceV2 {
    private final DataSource dataSource;
    private final MemberRepositoryV2 memberRepository;
    
		public void accountTransfer(String fromId, String toId, int money) throws
        SQLException {
        //동일한 connection을 사용하기 위해서 서비스 코드 내부에서 connection 획득
        //repository에 해당 connection 동일하게 전달
        Connection con = dataSource.getConnection();
        try {
            //setAutoCommit(false)로 트랜잭션 시작
            con.setAutoCommit(false); 
            
            //계좌 이체 비지니스 로직 
            Member fromMember = memberRepository.findById(con, fromId);
            Member toMember = memberRepository.findById(con, toId);
            memberRepository.update(con, fromId, fromMember.getMoney() - money);
            //에러 발생 상황 연출
            if (toMember.getMemberId().equals("ex")) {
                throw new IllegalStateException("이체중 예외 발생");
            }
            memberRepository.update(con, toId, toMember.getMoney() + money);
            
            //성공시 커밋
            con.commit(); 
        } catch (Exception e) {
            //에러발생 - 실패시 롤백
            con.rollback(); 
            throw new IllegalStateException(e);
        } finally {
	    //서비스 계층에서 connection 종료
            release(con);
        }
    }
    
    private void release(Connection con) {
        if (con != null) {
            try {
                //커넥션 풀 고려
                con.setAutoCommit(true); 
                con.close();
            } catch (Exception e) {
                log.info("error", e);
            }
        }
    }
}