스프링/5. 스프링 DB-1

22. 기존 트랜잭션의 문제

sdafdq 2023. 9. 30. 13:29

먼저, 스프링의 애플리케이션 구조

Controller에서 사용자와 상호작용 하고

Controller에서 사용자의 요청에 따라 알맞은 서비스를 호출하고

서비스에서는 필요에 따라 리포지토리(DB)에 접근한다.

 

Controller

UI 관련 처리

웹 요청, 응답

사용자 요청 검증

 

Service

비즈니스 로직

가급적 특정 기술에 의존하지 않고 순수 자바 코드로 작성

 

Repository

DB와 소통하는 코드

 

 

여기서 Service는,

특정 기술에 종속적이지 않아야 한다.

웹, REST API 등 여러 통신 방법에도 같아야 한다.

그래서 거의 순수 자바 코드로 작성되어야 한다.

 

 

Controller에서는 웹, REST API 등 다양한 요청 및 응답은 이 친구가 처리를 해 주고, 그래서 요청마다 약간 다를 수 있고,

 

Repository는 DB와의 소통을 위한 친구 이므로 DB따라 조금 다를 수 있다.

 

근데 Service는 똑같아야 한다.

 

그런데, 기존에 우리가 작성했던 Service 부분의 로직은 문제가 있다.

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {
    private final MemberRepositoryV2 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Connection con = memberRepository.getConnection();
        try{
            con.setAutoCommit(false);
            transferLogic(con, fromId, toId, money);
            con.commit();
        }catch(Exception e){
            con.rollback();
            throw new IllegalStateException();
        }finally {
            release(con);
        }
    }

    private void transferLogic(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 static void release(Connection con) {
        if(con != null){
            try{
                con.setAutoCommit(true);
                con.close();
            }catch (Exception e){
                log.info("close error = ",e);
            }
        }
    }

    private void validation(Member toMember) {
        if(toMember.getMemberId().equals("ex")){
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}

우선 SQLException도 jdbc 에러이고,

connection에 관련된 것도 jdbc의 기술을 사용하는 것이다.

거의 순수 자바만 쓴 비즈니스 로직은 transferLogic 밖에 없다. 

 

만약 jdbc에서 jpc같은 걸로 바꾸게 된다면, 저 커넥션 관련해서 코드를 수정해야 할 부분이 상당히 많아진다.

 

Controller와 Repository는 요청의 종류에 따라, 혹은 DB의 종류에 따라 확장해도 괜찮지만,

 

핵심 비즈니스 로직은 어떤 종류의 요청이든 어떤 종류의 DB든간에 호응이 되는 로직이여야 한다. 절대적이여야 한다.

 

 

 

위의 문제는

JDBC 구현기술이 서비스 계층까지 누수가 된 것이다.

트랜잭션을 적용하기 위해 결국 서비스계층에서 까지 JDBC 기술에 의존하게 된 것이다.

 

또, 트랜잭션을 적용하기 위한 코드가 반복적이게 될 것이다.

지금은 우리가 서비스로직 하나만 구현해 놔서 그런데, 

다른 서비스 로직도 트랜잭션이 필요할 수도 있다.

그러면 그 로직 안에 똑같이 저 트랜잭션을 적용하기 위한 코드를 그대로 써야 한다.

 

예외도 그렇다.

SQLExceptio은 Jdbc 전용 기술이기에 향후 JPA등 다른 리포지토리를 개발하면 그에 맞는 예외로 변경해 한다.

 

 

또 사실 Repository에서도 문제가 있다.

@Slf4j
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(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){
            throw e;
        }finally {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }
    }

    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){
            throw e;
        }finally {
            close(con, pstmt, rs);
        }
    }

    private void close(Connection con, Statement stmt, ResultSet rs){
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }

    public void update(Connection con, String memberId, int money){
        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);
            int result = pstmt.executeUpdate();
            log.info("result = {}",result);
        }catch(SQLException e){
            log.info("error",e);
        }finally {
            JdbcUtils.closeStatement(pstmt);
        }
    }

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

        Connection con = null;
        PreparedStatement pstmt = null;

        try{
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int result = pstmt.executeUpdate();
            log.info("result = {}",result);
        }catch(SQLException e){
            log.info("error",e);
        }finally {
            close(con, pstmt,null);
        }
    }

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

        Connection con = null;
        PreparedStatement pstmt = null;
        try{
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1,memberId);
            pstmt.executeUpdate();
        }catch(SQLException e){
            throw e;
        }finally {
            close(con, pstmt, null);
        }
    }

    public Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        return con;
    }
}

반복되는 코드가 많다. try ~~ catch

커넥션 얻고, 쿼리문에 바인딩 해 주고, 쿼리 실행하고,

에러 있으면 던져주고, 마지막에 커넥션 닫아주고..

 

 

'스프링 > 5. 스프링 DB-1' 카테고리의 다른 글

24. 트랜잭션 동기화  (0) 2023.09.30
23. 트랜잭션 추상화  (0) 2023.09.30
21. 이체 트랜잭션 적용  (0) 2023.09.30
20. 이체 테스트  (0) 2023.09.30
19. 조회할 때 락 가져오기  (0) 2023.09.30