알아보게된 이유
수정을 하는 로직이 있다면 무조건 Transactional을 붙여야하는가?
언제 Transactional을 붙여야하는건지
Transactional에 대한 이해 부족
@Transactional이란?
- spring에서 메서드의 원자성을 보장하기 위해 정의된 annotation interface입니다.
- Spring으로 원자성을 보장하기 위해서는 persistence layer를 구성하여 수행하는데요, 이는 보통 DB 연결로 수행 하기 때문에 구현체로 DB 관련 TransactionManager를 많이 사용하게 됩니다.
- 하지만 @Transactional은 DB에 한정되서 사용하는 것이 아니라 기능 동작에 관한 원자성을 보장하는 interface입니다. 하지만 Spring은 데이터 저장소가 아니라서 어떤 솔루션으로도 원자성을 보장하는것에 한계가 있기 때문에 자연스레 DB조회&업데이트 관련 원자성 보장에 많이 활용하는 편입니다.
@Transactional 사용법
- 메서드 레벨 또는 클래스 레벨에서 사용할 수 있다.
- 반드시 public 메서드에 트랜잭션을 적용
- 해당하는 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 트랜잭션을 commit하고, 예외가 발생하면 트랜잭션을 rollback한다. ⇒ 데이터 일관성을 유지
- [동작과정] 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성
- 프록시 객체는 @Transactional이 포함된 메서드가 호출될 경우, 트랜잭션을 시작하고 Commit or Rollback을 수행
- CheckedException or 예외가 없을 때는 Commit
- UncheckedException이 발생하면 Rollback
@Transactional(readonly=true)
- read-only 작업에 대해 런타임 최적화를 제공한다.
- transaction subsystem에 대한 hint를 제공한다.
- readOnly 값을 beforeCommit Callback의 인자로 받아오고, 커밋 시점에 변경 감지(change detection)을 억제한다.
- 커밋 전에 ReadOnly 여부를 확인하여 Hibernate 세션의 Flush 모드를 'FlushMode.MANUAL'로 설정하는 데 사용될 수 있다-
- prepareFlushMode()메소드를 보면 readOnly 값이 true일 때 FlushMode를 MANUAL로 설정한다
💡JPA Dirty Checking 개념 JPA의 Dirty Checking(더티 체킹)은 엔티티 객체의 상태 변화를 감지하고, 이를 자동으로 데이터베이스에 반영하는 메커니즘입니다. Dirty Checking은 영속성 컨텍스트(Persistence Context) 내에서 엔티티의 변경 상태를 추적하고, 트랜잭션이 커밋되는 시점에 변경된 내용을 데이터베이스에 자동으로 반영한다.
💡JPA Dirty Checking을 하지 않는다? FlushMode가 MANUAL인 경우, 트랜잭션이 커밋되거나 flush() 메서드가 호출될 때까지는 영속성 컨텍스트의 변경 내용이 데이터베이스에 반영되지 않는다. 따라서 @Transactional(readOnly = true)를 사용하면 트랜잭션이 읽기 전용이 되고, FlushMode가 MANUAL로 설정되어 더티 체킹에 의한 자동적인 데이터베이스 반영이 일어나지 않게 된다
⇒ [결론] 명시적으로 flush()를 호출하지 않는 이상 트랜잭션이 끝날 때까지 데이터베이스에 변경 내용이 반영되지 않는다.
⇒ [이점] 엔티티에 대한 Snapshot을 관리하는 메모리를 절감할 수 있고 DB에 쓰기 작업을 발생시키지 않게 됩니다.
@Transactional(readonly=true)의 주의할점
- 조회 Connection의 Connection Pool 운영 문제
- JpaTransactionManager > doCleanUpAfterCompletion(...)
- 트랜잭션에서 커넥션이 반환되는 시점은 commit() 이후 시점
- @Transactional은 AOP 기반으로 하기 때문에 Target 메서드의 로직이 모두 종료된 후 commit() 로직이 실행된다.
- @Transactional(readOnly = true) 옵션을 사용하면
- 해당 조회 메서드가 모두 종료된 이후 DB Connection이 끊어지게 된다는 뜻이고
- 반대로 해석하면 DB 조회 이후 Connection이 사용되지 않는 상황에서도 자원이 바로 반납되지 않는다는 것
- 낙관적 락에 Transaction(readOnly=true)설정하는 것을 주의를 해야한다?
- 낙관적락이 버전으로 관리할텐데 버전이 붙여진 필드에 붙여진 것에… 수정되지 않아야할 것에 수정이 되는 문제가 생겨서 주의해야한다고 한다
- 읽기 전용 중에 버전이 바뀌면서 생기는 문제라고 한다….
- 뒤에서 서술 더 할 예정..!
오늘의 결론 : 궁금했던 부분
JPA Transaction 이 필요 없는 경우
원자성이 보장되어야할 경우는 여러 데이터에 대한 update가 필요할때 이다.
그래서 아래와 같은 3가지 경우에는 필요 없다.
- 일단 조회만 할 경우, Transactional이 필요하지 않다.
- 이미 영구적인 데이터를 조회만 하고 있기 때문에, Transaction이 보장해 주는 ACID의 기능 중 Durability, 영구 적용성이 필요가 없습니다. ⇒ 트랜잭션 없이도 데이터 정합성이 유지되므로 @Transactional이 필요 없음.
- DB에서 제공해 주는 일관된 읽기를 위한다면 application 입장에서의 단일 스레드 안에서 같은 data 요청을 여러 번 하는 것이 유지보수성이 높아지는지, 성능적 이점이 있는지 고민해 볼 필요가 있습니다.
- 하나의 row만 update 할 경우, Transactional이 필요하지 않습니다.
- 이미 하나의 데이터에 관한 update는 DB에서 원자성을 보장해주기 때문에 Transactional이 필요하지 않습니다.
- 동시성 제어만 필요해서 Transactional을 사용하는 거라면, 다른 방법을 고려할 수 있습니다.
- 단일 select update와 같은 경우에 동시성 제어가 필요한 경우에는 Transactional 만으로는 완벽한 제어가 불가능한 경우가 있습니다. (mysql의 경우 phantom read)
- Transactional isolation을 serializable 모드로 강하게 한다던지 (이 경우 성능에 있을 수 있긴함)
- optimistic lock을 따로 적용을 한다던지 추가적인 작업이 필요합니다.
- 그렇게 까지 진행할 작업이 아니라면 비즈니스-도메인 구조에 대해서 다시 생각해 볼 수 있습니다.
- 단일 key에 대한 update가 동시적으로 들어올 수 있는 비즈니스 구조 자체가 문제가 있는 건 아닌지 생각해 볼 수 있습니다.
- redis 등을 활용한 서비스 api 요청 단위에서의 중복 요청 제어 등을 고려해 볼 수 있습니다.
- 단일 select update와 같은 경우에 동시성 제어가 필요한 경우에는 Transactional 만으로는 완벽한 제어가 불가능한 경우가 있습니다. (mysql의 경우 phantom read)
위 3가지 경우를 제외하고 나면 JPA 입장에서 여러 데이터를 save 하게 되는 경우로 귀결된다.
JPA Transactional에서 주의할점
- 그렇다면 Transactional을 아예 안 쓰려고 해도 JPA 사용 시 기본적인 메서드 사용 시에는 내부 코드에서 Transactional이 사용된다.
- findById, save, delete 등을 구현하고 있는 SimpleJPARepository는 클래스 레벨에서 이미 @Transactional(readOnly=true) 가 선언되어 있다. 그리고 save, delete 등의 update 메서드에는 @Transactional이 붙어 있다.
- 다만 Querydsl, jpql 와 같이 custom 쿼리를 사용할 경우에는 simpleJPARepository 메서드 구현체가 동작하지 않기 때문에 default Transactional에서 자유로워지게 된다.
- ⇒ 따라서 내가 원하는 대로 잘 사용하고 있는지 확인할 필요가 있다.
JPA Transactional(readOnly=true)에서 주의할점
- @Transaction(readOnly=true)는 Dirty checking을 안해서 성능상 이점이 있다?
- propagation이 설정되어 있지 않아 default propagation setting은 REQUIRED로써 상위 메서드의 transaction이 없을 시에는 만들고, 있을 시에는 참여하여 DB로 실제 Transaction 사용을 요청하는 동작을 수행합니다.
그로인해 우리는 select 절만 필요한데 autoCommit setting, commit 요청, set session transaction 세팅 등으로 6개의 쿼리가 더 날아가고 있다.
대부분의 DB입장에서는 Read only 트랜잭션을 쓰게 되면 성능상 약간의 이점이 있거나 또는 없다고 이야기 한다.
이 기능의 주요 목적은 ACID 중 읽기 작업만 수행했을때, Isaolation 제공을 위한 일관된 읽기 수행 제공에 있기 때문이다. 단순히 이러한 이점을 위해서 단순 쿼리로 인해 DB에 6배의 추가 네트워크 요청이 늘어 나는 것은 비효율적이다.
실제로 set_option과 commit이 성능상 영향을 미친다. (조회 쿼리도 아니고 DB cpu나 메모리를 잡아먹는 큰 허들이 될까 싶었지만 select 쿼리가 약 2~3배 정도 증가해 DB 사용 성능이 향상된 것을 알 수 있고, api 테스트 결과 역시 마찬가지로 약 2배가량 좋아진 것을 확인할 수 있다.) ⇒ 이를 통해 set_option, commit 쿼리가 적어도 DB 조회 성능에 유의미 한 영향은 미칠 수 있다
만약 @Transactional(readOnly=true)를 CQRS패턴을 적용해, 읽기 전용 DB와 쓰기전용 DB를 분리했다면 성능상 이점이 있을 수 있겠다만, 그렇지 않은 상황이라면 안 쓰는게 좋다.
그렇다면 set_option, auto commit옵션을 설정하면 되는것 아닌가?
- transactional 사용 중 최초 db config 옵션에 auto commit을 빼면 최소한 트랜잭션 수행 시 set autoCommit=1,0에 대한 요청은 줄일 수 있습니다.
- 하지만 commit, set session 변환에 대한 쿼리는 없어지지 않는다.
⇒ 해당 옵션 변경을 위한 노력 보다는 Transactional 자체를 줄이는 것이 더 낫다.
근데 MYSQL의 경우에는 readOnly=true가 성능상 이점이 있다고 하는데…
MYSQL의 readOnly = true의 이점
1. 트랜잭션 ID(TRX_ID) 생성을 최소화하여 InnoDB 내부 데이터 구조 크기 감소 → 성능 최적화.
2. UNDO 로그(롤백 로그) 생성 방지 → I/O 성능 개선
3. MVCC(멀티버전 동시성 제어)에서 불필요한 복사본 생성 방지
4. InnoDB의 SHOW ENGINE INNODB STATUS에서 READ ONLY 트랜잭션이 제외됨 → 내부 구조 관리 효율 증가
MySQL의 READ ONLY 트랜잭션은 성능을 최적화할 수 있지만, Spring이 이를 활용하는 방식은 네트워크 비용을 증가시킬 수 있기 때문에 반드시 더 나은 선택이 아닐 수 있다.
그렇다면 언제 @Transactional(readOnly = true)를 사용할까?
- 읽기 전용 트랜잭션이 많고, 오래 지속되는 경우
- 긴 시간 동안 동일한 데이터 일관성을 유지하면서 조회할 때 (배치)
- 트랜잭션이 내부적으로 여러 번 DB와 연결되는 경우
- 같은 트랜잭션에서 여러 개의 SELECT 쿼리를 실행해야 하는 경우, READ ONLY 설정이 트랜잭션 컨텍스트 유지에 도움을 줄 수 있음.
- 트랜잭션 내에서 여러 개의 SELECT를 실행하면서도 일관된 데이터 정합성을 유지해야 할 때
- CQRS 패턴을 적용한 경우
- 읽기 전용 DB에서 실행되므로 트랜잭션 부하를 최소화
결론
- 단건 요청(update, insert 모두)에 대해서는 Transaction 제거하기
- 단건 업데이트는 더티체킹으로 실행가능
- 클래서 레벨에서 Transactional 설정 X
- transactional 사용 구간 안에 3rd party api(외부 API)가 끼지 않도록 persistence layer 바깥에서는 transactional을 사용하지 않는다.
@Service
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.process();
// 🚨 3rd-party API 호출이 트랜잭션 내부에 포함됨 (위험)
paymentGateway.charge(order.getTotalPrice());
orderRepository.save(order);
}
- 만약 paymentGateway.charge() 호출이 실패하면, DB 변경 사항까지 롤백됨.
- 하지만 결제 API 실패가 DB 변경사항 롤백의 이유가 되지 않을 수도 있음.(상황상*)
4. @Transactional(readOnly=true)에 propagation 넣기
- CQRS 패턴에서 @Transactional(readOnly=true)와 함께 propagation 설정을 잘 활용하면 readOnly DataSource는 보지만 transaction은 수행되지 않아 트랜잭션 없는 readOnly DB 접근이 가능해진다.
- 이거는 카카오페이의 케이스 이기 때문에 참고로 보시길..
- 제약사항
- 하나의 스레드 내에 DB connection pool 연결은 1개가 좋다.
- DB connection이 하나라면 transaction도 스레드당 1개여야 한다.
- readOnly 단독 사용의 경우 transaction은 없어야 한다.
- 우선 transactional이 발생하지 않는 propagation의 종류는 다음과 같습니다.
- SUPPORTS : default Propagation인 REQUIRED와 비슷한 동작을 취하고 있어 메서드 단독 수행 시에는 트랜잭션이 동작하지 않지만 상위에 트랜잭션이 있는 경우 상위트랜잭션에 포함되어 수행되는 전파방식
- NEVER
- NOT_SUPPORTED
- Transactional의 경우 default REQUIRED
Transactional에 대해 좀 알아보고 정리해봤답니다..!
카카오페이의 아티클 내용을 거의 다 가져온거긴 한데 내 궁금증의 흐름대로 정리해봤습니다
Reference
https://tech.kakaopay.com/post/jpa-transactional-bri/#transactional이란
https://medium.com/gdgsongdo/transactional-바르게-알고-사용하기-7b0105eb5ed6
https://studyandwrite.tistory.com/573
https://lob-dev.tistory.com/58
'CS > CS' 카테고리의 다른 글
로드밸런싱 알고리즘과 SPOF에 의한 DNS 로드밸런싱 (0) | 2025.02.11 |
---|---|
[운영체제] ThreadPool /Blocking Queue의 종류들은 각각 언제 사용할까 (1) | 2025.02.04 |
[DB] 데이터베이스와 SQL (0) | 2022.09.19 |
댓글