이번 레벨2 미션들을 진행하면서 @Transactional 어노테이션을 별다른 생각 없이 DB 작업을 수행하는 기능들에 대해 선언해왔다. 그러던 중 리뷰어께서 @Transactional 어노테이션의 옵션들 중에서도 readOnly 옵션에 대해 알아보면 좋겠다는 피드백이 있었어서 이에 대해 알아보고자 한다.
일반적으로 검색하면 알 수 있는 내용은 @Transactional(readOnly=true)와 같이 어노테이션 선언 시 옵션을 주게 되면 해당 기능을 수행하는 중 Create, Update, Delete 와 같이 데이터의 변경이 발생하는 작업들이 수행되는 경우 예외를 발생시킨다는 것이다. 실제로 그러한지 알아보기 위해 바로 실험을 해봤다.
// 읽기 전용 트랜잭션으로 선언
@Transactional(readOnly = true)
public Long save(CartItem cartItem) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO cart_item (member_id, product_id, quantity, checked) VALUES (?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
ps.setLong(1, cartItem.getMember().getId());
ps.setLong(2, cartItem.getProduct().getId());
ps.setInt(3, cartItem.getQuantity());
ps.setBoolean(4, cartItem.isChecked());
return ps;
}, keyHolder);
return Objects.requireNonNull(keyHolder.getKey()).longValue();
}
앞에서 설명한 대로라면 해당 메서드가 실행되었을 때 읽기 전용 트랜잭션에서 update가 수행되려고 한다며 예외 처리 되어야 한다. 하지만 막상 위의 save 메서드를 포함하는 기능을 실행해본 결과 DB에 데이터가 Insert되고, 별도의 예외처리나 롤백 과정이 이뤄지지 않는다는 것을 확인했다.
이에 대해 원인파악을 해본 결과 다음과 같이 정리해볼 수 있었다.
@Transactional(readOnly=true) 옵션이 걸린 경우 DB 벤더에 따라 Create, Update, Delete 연산에 대해서 오류를 발생시킬 수도 있고 발생시키지 않을 수도 있다.
- 위에서 진행한 간단한 테스트의 경우 DB를 H2 인메모리 DB를 사용해 진행했다.
- 레퍼런스를 찾아본 결과 내부 H2 연결을 담당하는 JDBCConnection을 살펴보면 readOnly 옵션이 ignore된다고 나와있다. 즉 H2 DB의 경우 따로 읽기 전용 트랜잭션을 제공하지 않는다.
- 결과적으로 H2 DB를 사용하는 경우 스프링에서 @Transactional(readOnly = true)를 해도 Create, Update, Delete 연산이 예외처리 되지 않고 잘 수행된다.
따라서 해당 옵션을 줄 때 어떤 DB 벤더사와 드라이버를 사용하는지, 사용하는 버전에서 이러한 읽기 전용 트랜잭션을 제공하는지 확인해보는 작업이 반드시 필요하다고 결론지을 수 있었다.
그러면 사용하는 DB 벤더사가 읽기 전용 트랜잭션을 제공할 때 사용한다면 어떤 이점을 얻을 수 있을까? 정리해본 이점들은 다음과 같았다.
- 읽기 전용 트랜잭션을 제공하는 벤더사 DB를 사용하는 경우 의도치 않은 CUD 작업에 대한 예외처리를 해줄 수 있다.
- JPA를 사용하게 되면 데이터 변경을 감지하고 이를 반영하기 위한 더티 체킹, 플러시 호출, 스냅샷 생성등의 작업을 하지 않음으로써 성능적인 이점을 가져갈 수 있다.
- DB가 master-slave 구조를 가지는 Replication 구성이 되어 있는 경우 read는 slave DB로, Create, Update, Delete는 master DB로 요청되게 설정하여 트래픽을 분산시킬 수 있다. (단, 별도의 추가적인 설정이 필요하다고 한다.)
- DB 벤더사에 따른 read 쿼리 성능 최적화 + DB에 불필요한 Lock을 걸지 않음으로써 얻을 수 있는 성능적 이점이 존재한다.
- 동료 개발자에게 해당 메서드가 읽기 작업만을 수행한다는 것을 직관적으로 바로 알려줄 수 있다.
이러한 이점들을 취하기 위해 읽기 전용 트랜잭션과 아닌 트랜잭션을 구분해줄 수 있다. 단 DB 벤더사와 드라이버에 따른 차이가 존재하므로 항상 관련 가이드를 참고해서 트랜잭션 작업을 진행해야 한다.
(틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!)
Reference
'우아한테크코스 > 학습 정리' 카테고리의 다른 글
브랜치 전략에서 squash and merge 방식 사용에 대한 고찰 (0) | 2023.07.16 |
---|---|
Domain 그리고 Entity, VO에 대한 개인적인 고찰 (0) | 2023.06.07 |
스프링의 예외처리 과정 및 처리 방법에 대하여 (2) (2) | 2023.05.12 |
스프링의 예외처리 과정 및 처리 방법에 대하여 (1) (0) | 2023.05.12 |
[Level 2] Layered Architecture에 대한 개인적인 고찰 (0) | 2023.04.27 |