트랜잭션이 없는 비즈니스 로직

@RequiredArgsConstructor  
public class MemberServiceV1 {  

    private final MemberRepositoryV1 memberRepository;  

    public void accountTransfer(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 member) {  
        if (member.getMemberId().equals("ex")) {  
            throw new IllegalStateException("이체중 예외 발생");  
        }  
    }  
}

@Slf4j  
public class MemberRepositoryV1  
{  

    private final DataSource dataSource;  

    public MemberRepositoryV1(DataSource dataSource) {  
        this.dataSource = dataSource;  
    }  


    public Member save(Member member) throws SQLException {  
        String sql = "insert into member (member_id, money) values (?, ?)";  

        Connection con = null;  
        PreparedStatement pstmt = null;  


        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, member.getMemberId());  
            pstmt.setInt(2, member.getMoney());  

            pstmt.executeUpdate();  

            return member;  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, null);  
        }  

    }  

    public Member findById(String memberId) throws SQLException {  
        String sql = "select * from member where member_id = ?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            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 memberId=" + memberId);  
            }  


        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    public void update(String memberId, int money) throws SQLException {  
        String sql = "update member set money=? where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setInt(1, money);  
            pstmt.setString(2, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    public void delete(String memberId) throws SQLException {  
        String sql = "delete from member where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    private Connection getConnection() throws SQLException {  
        Connection conn = dataSource.getConnection();  
        log.info("conn = {}, conn.class = {}", conn, conn.getClass());  
        return conn;  
    }  

    private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {  
        JdbcUtils.closeResultSet(rs);  
        JdbcUtils.closeStatement(stmt);  
        JdbcUtils.closeConnection(con);  
    }  
}
  • formId 회원에서 toId 회원으로 money 만큼 계좌 이체하는 비즈니스 로직
  • toId 가 "ex"이면 예외가 발생하여 toId 회원의 잔고가 증가하는 로직은 실행되지 않음
  • 결국 fromId 회원의 잔고만 감소하는 문제가 발생

트랜잭션이 있는 비즈니스 로직

  • 앞서 발생한 문제는 트랜잭션을 통해 해결할 수 있음
  • 그렇다면 트랜잭션을 애플리케이션의 어느 계층에 걸어야 하는가? → 서비스 계층
  • 비즈니스 로직에서 문제가 발생하면 해당 비즈니스 로직에서 수행된 작업들을 롤백해야하기 때문

비즈니스 로직과 트랜잭션

  • 트랜잭션을 시작하기 위해 서비스 계층에서 커넥션을 생성하고 트랜잭션 커밋 후 종료해야 함
  • 애플리케이션에서 DB 트랜잭션을 사용하려면 같은 세션을 유지해야 하고, 이를 위해 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 함
  • 커넥션을 파라미터로 전달하여 사용함으로써 동일 커넥션을 유지할 수 있음

커넥션을 파라미터로 받는 Repostitory

public class MemberRepositoryV2 {  

    private final DataSource dataSource;  

    public MemberRepositoryV2(DataSource dataSource) {  
        this.dataSource = dataSource;  
    }  


    public Member save(Member member) throws SQLException {  
        String sql = "insert into member (member_id, money) values (?, ?)";  

        Connection con = null;  
        PreparedStatement pstmt = null;  


        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, member.getMemberId());  
            pstmt.setInt(2, member.getMoney());  

            pstmt.executeUpdate();  

            return member;  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, null);  
        }  

    }  

    public Member findById(String memberId) throws SQLException {  
        String sql = "select * from member where member_id = ?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            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 memberId=" + memberId);  
            }  


        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    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 memberId=" + memberId);  
            }  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            JdbcUtils.closeResultSet(rs);  
            JdbcUtils.closeStatement(pstmt);  
        }  
    }  

    public void update(String memberId, int money) throws SQLException {  
        String sql = "update member set money=? where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setInt(1, money);  
            pstmt.setString(2, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    public void update(Connection con, String memberId, int money) throws SQLException {  
        String sql = "update member set money=? where member_id=?";  

        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            pstmt = con.prepareStatement(sql);  
            pstmt.setInt(1, money);  
            pstmt.setString(2, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            JdbcUtils.closeStatement(pstmt);  
        }  
    }  

    public void delete(String memberId) throws SQLException {  
        String sql = "delete from member where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    private Connection getConnection() throws SQLException {  
        Connection conn = dataSource.getConnection();  
        log.info("conn = {}, conn.class = {}", conn, conn.getClass());  
        return conn;  
    }  

    private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {  
        JdbcUtils.closeResultSet(rs);  
        JdbcUtils.closeStatement(stmt);  
        JdbcUtils.closeConnection(con);  
    }  
}
  • 기존 MemberRepositoryV1과는 다르게 findById(), update() 에서 커넥션을 파라미터로 받음
    • 커넥션 유지가 필요하기 때문에 새로 커넥션을 획득하는 getConnection() 메서드를 사용하면 안 된다
    • 또한, 비즈니스로직 내 다른 작업에서 동일한 커넥션을 사용해야하기 때문에 close() 메서드로 커넥션을 닫으면 안 된다

Service 계층에서 트랜잭션 실행

@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); // 트랜잭션 시작  

            businessLogic(con, fromId, toId, money);  

            con.commit();   // 성공 시 커밋  
        } catch (Exception e) {  
            con.rollback();  
            throw new IllegalStateException(e);  
        } finally {  
            release(con);  
        }  
    }  

    private void businessLogic(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);  
                con.close();  
            } catch (Exception e) {  
                log.info("error", e);  
            }  
        }  
    }  

    private void validation(Member member) {  
        if (member.getMemberId().equals("ex")) {  
            throw new IllegalStateException("이체중 예외 발생");  
        }  
    }  
}
  • Connection con = dataSource.getConnection();
    • 트랜잭션을 시작하기 위해 비즈니스 로직 실행 전 커넥션 획득
  • con.setAutoCommit(false);
    • 트랜잭션 시작을 위해 수동 커밋 모드로 변경
    • 커넥션을 통해 DB 세션에 set autocommit false 가 전달
    • 트랜잭션 시작
  • businessLogic(con, fromId, toId, money);
    • 트랜잭션이 시작된 커넥션을 파라미터로 전달하면서 비즈니스 로직 수행
    • 비즈니스 로직 내부의 memberRepository.update(con, ... )에서 파라미터로 받은 커넥션을 사용
  • con.commit();
    • 비즈니스 로직이 정상적으로 완료되면 트랜잭션을 커밋
  • con.rollback();
    • 비즈니스 로직 실행 중 예외가 발생하면 트랜잭션을 롤백
  • release(con);
    • finally { ... } 구문을 사용하여 예외 발생과 상관없이 커넥션을 안전하게 종료
    • 단, 커넥션 풀을 사용하면 커넥션이 종료되는 것이 아니라 '반환'됨
    • 현재 수동 커밋 모드로 동작하기 떄문에 반환할 때는 기본값인 자동 커밋 모드로 되돌려 반환하는 것이 안전

+ Recent posts