ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] JdbcTemplate과 TransactionTemplate, 그리고 @Transactional
    legacy/Spring 2023. 8. 20. 15:27

    본 글은 다크모드에 최적화되어 있습니다.

     

     

    스프링에는 데이터 접근 계층(Repository)에서 사용하는 JdbcTemplate과 서비스 계층(Service)에서 사용하는 TransactionTemplated 이 존재합니다. 특별한 기능을 제공하는 것은 아니고 템플릿 콜백 패턴을 사용하여 반복되는 코드를 제거해 줍니다.

     

    JdbcTemplate

    JdbcTemplate은 데이터 접근 계층에서 DB에 접근할 때 반복되는 코드를 제거해 줍니다. 반복되는 코드란 아래의 목록들을 말합니다.

    • 커넥션 조회 및 동기화
    • PreparedStatement 생성 및 파라미터 바인딩
    • 쿼리 실행
    • ResultSet으로 결과 바인딩
    • 예외 발생 시 스프링 예외 변환기(DataAccessException) 실행
    • 리소스 종료

    위의 목록들 중 일부를 알아봅시다.

    커넥션 조회 및 동기화

    서비스 계층에서 트랜잭션을 시작하면 DB에 접근하기 위한 커넥션을 생성하여 트랜잭션 동기화 매니저에 저장합니다. 데이터 접근 계층에서는 DataSourceUtils.getConnection()을 통해 트랜잭션 동기화 매니저에 저장된 커넥션을 가져와 DB에 접근합니다.

    public class Repository {
    	// ..생략
        public void 메서드() {
       	 // 동기화된 커넥션을 가져온다
    	Connection conn = DataSourceUtils.getConnection(dataSource);
        }
    }

    PreparedStatement 생성 및 파라미터 바인딩

    DB에 실행할 SQL문을 생성합니다.

    public class Repository {
    	// ..생략
        public void 메서드() {
        	String sql = "...";
            // PreparedStatement 생성
            PreparedStatement stmt = conn.prepareStatement(sql)
            // 파라미터 바인딩
            stmt.setString()
            stmt.setInt()
        }
    }

    스프링 예외 변환기

    체크 예외를 언체크 예외로 변환하여 서비스 계층에 예외를 넘겨주기 위해서 스프링에서 제공하는 스프링 예외 변환기를 사용합니다.

    public class Repository {
    	// ..생략
        public void 메서드() {
        	try{
           // ..생략
            } catch(SQLException ex) {
            SQLExceptionTranslator exTranslator 
            		= new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException exceptionTranslate 
            		= exTranslator.translate("save", sql, ex);
            throw exceptionTranslate;
            }
        }
    }

     

    여기까지 보았는데 하나의 로직을 작성하는데 코드가 상당히 길어집니다. 사용자 생성, 조회, 삭제 등등 로직이 많아질수록 위의 코드를 모두 작성하여야 하는데 상당히 힘들 것입니다. 

     

    이러한 문제를 해결하고자 JdbcTemplate을 사용할 수 있습니다. JdbcTemplate은 DB 접근 계층에서 반복되는 코드를 모두 삭제해 줍니다. 실행할 sql문만 작성하면 JdbcTemplate이 알아서 쿼리를 실행해 줍니다.

     

    JdbcTemplate 적용

    @Repository
    @Primary
    public class MemberRepositoryV5 implements MemberRepository{
    
        private final JdbcTemplate jdbcTemplate;
        @Autowired
        public MemberRepositoryV5(DataSource dataSource) {
            this.jdbcTemplate = new JdbcTemplate(dataSource);
        }
    
        @Override
        public Member save(Member member) {
            String sql = "insert into member(member_id, money) values(?, ?)";
            jdbcTemplate.update(sql, member.getMemberId(), member.getMoney());
            return member;
        }
    
        @Override
        public Member findById(String memberId) {
            String sql = "select * from member where member_id = ?";
    
            return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
                return new Member(rs.getString("member_id"), rs.getInt("money"));
            }, memberId);
        }
    
        @Override
        public void update(String memberId, int money) {
            String sql = "update member set money = ? where member_id = ?";
            jdbcTemplate.update(sql, money, memberId);
        }
    
        @Override
        public void delete(String memberId) {
            String sql = "delete from member where member_id = ?";
            jdbcTemplate.update(sql, memberId);
        }
    }

    JdbcTemplate 미적용

    @Repository
    @RequiredArgsConstructor
    public class MemberRepositoryV4_2 implements MemberRepository {
    
        private final DataSource dataSource;
    
        @Override
        public Member save(Member member) {
            String sql = "insert into member (member_id, money) values(?,?)";
    
            Connection conn = null;
            PreparedStatement stmt = null;
    
            try {
                conn = DataSourceUtils.getConnection(dataSource);
                stmt = conn.prepareStatement(sql);
                stmt.setString(1, member.getMemberId());
                stmt.setInt(2, member.getMoney());
                stmt.executeUpdate();
    
                return member;
            } catch (SQLException ex) {
                SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                DataAccessException dataAccessException = exceptionTranslator.translate("insert", sql, ex);
                throw dataAccessException;
            } finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(conn, dataSource);
            }
        }
    
        @Override
        public Member findById(String memberId) {
            String sql = "select * from member where member_id = ?";
    
            Connection conn = null;
            PreparedStatement stmt = null;
            ResultSet rs = null;
    
            try {
                conn = DataSourceUtils.getConnection(dataSource);
                stmt = conn.prepareStatement(sql);
                stmt.setString(1, memberId);
                rs = stmt.executeQuery();
                if (rs.next())
                    return new Member(rs.getString("member_id"), rs.getInt("money"));
                else
                    throw new NoSuchElementException("사용자가 존재하지 않습니다. id = " + memberId);
            } catch (SQLException ex) {
                SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                DataAccessException dataAccessException = exceptionTranslator.translate("select", sql, ex);
                throw dataAccessException;
            } finally {
                JdbcUtils.closeResultSet(rs);
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(conn, dataSource);
            }
        }
    
        @Override
        public void update(String memberId, int money) {
            String sql = "update member set money = ? where member_id = ?";
    
            Connection conn = null;
            PreparedStatement stmt = null;
    
            try {
                conn = DataSourceUtils.getConnection(dataSource);
                stmt = conn.prepareStatement(sql);
                stmt.setInt(1, money);
                stmt.setString(2, memberId);
                stmt.executeUpdate();
            } catch (SQLException ex) {
                SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                DataAccessException dataAccessException = exceptionTranslator.translate("update", sql, ex);
                throw dataAccessException;
            } finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(conn, dataSource);
            }
    
        }
    
        @Override
        public void delete(String memberId) {
            String sql = "delete from member where member_id = ?";
    
            Connection conn = null;
            PreparedStatement stmt = null;
    
            try {
                conn = DataSourceUtils.getConnection(dataSource);
                stmt = conn.prepareStatement(sql);
                stmt.setString(1, memberId);
                stmt.executeUpdate();
            } catch (SQLException ex) {
                SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                DataAccessException dataAccessException = exceptionTranslator.translate("delete", sql, ex);
                throw dataAccessException;
            } finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(conn, dataSource);
            }
    
        }
    }

     

    TransactionTemplate

    TransactionTemplate은 트랜잭션 매니저를 생성, 커밋, 롤백하는 과정을 제거해 줍니다. 즉 반복되는 코드를 삭제해 줍니다.

     

    TransactionTemplate 적용

    @RequiredArgsConstructor
    public class MemberServiceV3_2 {
    
        private final MemberRepositoryV3 memberRepository;
        private final TransactionTemplate transactionTemplate;
    
        public void accountTransfer(String fromId, String toId, int money)  {
    
            transactionTemplate.executeWithoutResult((status) -> {
                try {
                    Member fromMember = memberRepository.findById(fromId);
                    Member toMember = memberRepository.findById(toId);
    
                    memberRepository.update(fromId, fromMember.getMoney() - money);
                    if (toId.equals("ex"))
                        throw new IllegalStateException("계좌이체 오류");
                    memberRepository.update(toId, toMember.getMoney() + money);
                } catch (Exception ex) {
                    throw new IllegalStateException(ex);
                }
            });
        }
    }

    TransactionTemplate 미적용

    @RequiredArgsConstructor
    public class MemberServiceV3_1 {
    
        private final PlatformTransactionManager transactionManager;
        private final MemberRepositoryV3 memberRepository;
    
    
        public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    	// 트랜잭션 시작
            TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
    
            try {
                Member fromMember = memberRepository.findById(fromId);
                Member toMember = memberRepository.findById(toId);
    
                memberRepository.update(fromMember.getMemberId(), fromMember.getMoney() - money);
                if (toId.equals("ex"))
                    throw new IllegalStateException("계좌이체 오류");
                memberRepository.update(toMember.getMemberId(), toMember.getMoney() + money);
    		// 트랜잭션 커밋
                transactionManager.commit(transactionStatus);
            } catch (Exception ex) {
           	 // 트랜잭션 롤백
                transactionManager.rollback(transactionStatus);
                throw new IllegalStateException();
            }
        }
    }

     

     

    @Transactional

    위에서 트랜잭션 템플릿을 사용한 코드를 봅시다. 비즈니스 로직 안에 트랜잭션과 관련된 코드와 비즈니스 로직이 섞여 있습니다. 이는 좋은 코드라고 할 수가 없습니다. 비즈니스 로직에는 순수한 자바 코드만 담겨있어야 합니다. 

     

    따라서 이와 같은 문제를 해결하고자 @Transactional 애너테이션을 사용해야 합니다.

    아래 코드를 보면 비즈니스 로직에 순수한 자바 코드만 담겨 있습니다. 트랜잭션 시작, 커밋, 롤백을 @Transactional 애너테이션 하나로 해결해 준 것입니다.

    @Service
    @Slf4j
    @RequiredArgsConstructor
    public class MemberServiceV4 {
    
        private final MemberRepository memberRepository;
    
        @Transactional
        public void accountTransfer(String fromId, String toId, int money) {
            Member fromMember = memberRepository.findById(fromId);
            Member toMember = memberRepository.findById(toId);
    
            memberRepository.update(fromId, fromMember.getMoney() - money);
            validation(toId);
            memberRepository.update(toId, toMember.getMoney() + money);
        }
    
        private void validation(String memberId) {
            if(memberId.equals("ex"))
                throw new IllegalStateException("계좌이체 오류 발생");
        }
    }

    결론

    DB 접근 계층인 Repository에서는 JdbcTemplate을 사용하고, 서비스 계층인 Service에서는 @Transactional 애너테이션을 사용하는 것이 좋습니다.

Designed by Tistory.