티스토리 뷰
[Spring] JdbcTemplate과 TransactionTemplate, 그리고 @Transactional
heemang.dev 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 애너테이션을 사용하는 것이 좋습니다.