-
[Spring] JdbcTemplate과 TransactionTemplate, 그리고 @Transactionallegacy/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 애너테이션을 사용하는 것이 좋습니다.