현재 `SpringBoot`를 사용하시는 대부분의 개발자라면, 대부분 `Spring Data JPA`를 사용할 것이다.
또한, `Spring`에서 제공해주는 `@Transactional`이라는 어노테이션을 많이 활용할 것이다.
그런데, 이것저것 공부를 하며 내가 몰랐던 `JPA Transactional`에 관한 많은 지식을 카카오페이의 테크 블로그를 통해 알게 되었다.
따라서, 이번 포스팅 내용은 해당 블로그에서 얻은 지식을 정리하는 것이다.
추후, 진행중인 `StudyWithMe`에도 적용해 볼 예정이다.
그럼 시작해보자.
@Transactional이란?
`Spring Transactional annotation`, 이하 `@Transactional`은 `Spring`에서 메서드의 원자성을
보장하기 위해 정의된 `annotation interface`이다.
`Spring`으로 원자성을 보장하기 위해서는 `persistence layer`를 구성해 수행하는데, 이는 보통 DB 연결로 수행하기 때문에 구현체로 DB 관련 `TransactionaManager`를 많이 사용하게 된다.
그렇다면 우리는 원자성 보장을 위해 `@Transactional`을 꼭 써야 할까?
그렇지 않다고 한다. `@Transactional`을 쓸 상황을 최대한 줄여야 한다고 한다.
@Transactional 기능의 역할과 책임의 제한
위에서 말한 `@Transactional` 스펙은 DB 한정이 아닌 기능 동작에 관한 원자성을 보장하는 interface이다.
하지만, `Spring`이 데이터 저장소는 아니기 때문에 DB 등 다른 솔루션 없이 원자성을 보장하는 것에 한계가 있다.
그러다보니 자연스럽게 DB 조회 및 업데이트 관련 원자성 보장에 많이 활용되는 편이다.
특히, `Spring`에서는 DB 접근을 위해 JPA를 활용하는 경우가 많기에 구현체로 `JPA Transactional`을 많이 사용한다.
데이터 접근에 대한 원자성 보장을 위한 JPA Transaction 역할
그렇다면 언제 이 원자성이 보장되어야 할까?
바로 여러 데이터에 관한 `update`가 필요할 때이다.
이 외의 경우에는 필요가 없다.
다음 3가지가 필요 없는 경우에 해당한다.
- 조회만 필요한 경우, Transactional 필요하지 않다.
- 이미 영구적인 데이터를 조회만 하고 있기 때문에, `Transaction`이 보장해주는 ACID의 기능 중 Durability, 영구 적용성이 필요가 없다.
- DB에서 제공해주는 일관된 읽기를 위한다면 application 입장에서의 단일 스레드 안에서 같은 data 요청을 여러 번 하는 것이 유지보수성이 높아지는지, 성능적 이점이 있는지 고민해 볼 필요가 있다.
- 하나의 row만 update 할 경우, Transactional이 필요하지 않다.
- 이미 하나의 데이터에 관한 update는 DB에서 원자성을 보장해주기 때문에 Transactional 필요하지 않다.
- 동시성 제어만 필요한 경우, 다른 방법을 고려할 수 있음
- 단일 select update와 같은 경우에 동시성 제어가 필요하다면 Transactional 만으로는 완벽한 제어가
불가능한 경우가 있다.(mysql의 경우 `phantom read`)- Transactional isolation을 serializable 모드로 강하게 한다던지(격리성은 높지만, 동시성이 낮음)
- `optimistic lock`을 따로 적용한다던지(추가적인 작업 필요)
- 그렇게까지 진행할 작업이 아니라면 비즈니스-도메인 구조에 대해 다시 생각
- 단일 key에 관한 update가 동시적으로 들어올 수 있는 비즈니스 구조 자체가 문제가 있는 건 아닌지 생각
- redis 등을 활용한 서비스 api 요청 단위에서의 중복 요청 제어 등을 고려
- 단일 select update와 같은 경우에 동시성 제어가 필요하다면 Transactional 만으로는 완벽한 제어가
위 3가지 경우를 제외하고 나면
"JPA에서 `@Transactional`이 필요한 경우는 결국 여러 데이터를 `save`해야 하는 경우로 귀결된다."
정리
- 단일 `update` -> 원자성(DB 자체 제공) -> `@Transactional` 불필요
- 조회만 수행 -> `@Transactional` 필요 없음
- 단일 `SELECT 후 UPDATE` -> 동시성 문제가 없다면 `@Transactional` 불필요, 하지만, 동시성 고려 필요
- 여러 데이터 `save()` -> `@Transactional` 필요(일관성을 보장해야 하므로)
JPA Transactional 함정
의도하지 않은 Transactional 동작
위와 같이 Transactional 사용을 최대한 절제하고 쓰지 않는다고 해도 JPA 사용 시 기본적인 메서드(Query Method) 사용 시에는 내부 코드에서 Transactional이 사용된다.
가장 대표적인 게 `findbyId`, `save`, `delete` 등을 구현하고 있는 `SimpleJpaRepository` 메서드들이다.
해당 클래스는 클래스 레벨에서 이미 `@Transactional(readOnly=true)`가 선언되어 있으며 save, delete 등의 update 메서드에는 `@Transactional`이 붙어있다.
예시
JPA는 어떤 datasource든 동일한 접근 방식을 제공하려는 프레임워크이고, 기본적으로 commit을 통한 최종 데이터 update를 수행하기 위해서는 `@Transactional`을 활용하게 되어있기 때문이다.
다만, QueryDSL이나 JPQL 사용 등의 custom 쿼리 동작 시에는 당연하게도 SimpleJpaRepository 메서드 구현체가 동작하지 않기 때문에 default Transactional에서 자유로워지게 된다.
`@Transactional(readOnly=true)` 동작에서 DB로의 쿼리 전파
`@Transactional(readOnly=true)` 사용 시 spring 진영에서는
애플리케이션 입장에서 `dirtyChecking`을 진행하지 않기 때문에 성능 개선의 측면이 있다고 이야기한다.
또한, 몇몇 개발자들의 입장에서는 코드를 명시적으로 작성하기에 명시적인 효과도 있다고 이야기한다.
다만, 여기서 의도치 않은 동작이 추가로 진행되기도 한다.(아래 상황)
`@Transaction(readOnly = true)` 사용 시 실제 DB 트랜잭션 모드를 readOnly로 설정해 DB 트랜잭션을 사용하게끔 동작한다.
이를 위해 `autoCommit setting`, `commit 요청`, `set session transaction` 세팅 등으로 6개의 쿼리가 더 나가게 된다.
즉, `dirtyChecking` 모드가 아닌 `Manual`모드로 바꿔주기에 성능 개선에 도움이 될 수 있긴 하지만,
앞선 6개의 쿼리처럼 단순한 추가 코드 작성으로 인해 DB까지의 네트워크 요청 건수 또한 최대 6배까지 늘어날 수 있다.
또한, 대부분의 DB 입장에서는 `read-only` 세팅으로 트랜잭션을 열게 되면 성능적 이점이 아주 약간 있거나 없을 것으로 설명한다고 한다.
따라서, Transactional 자체를 줄이는 게 좀 더 성능적으로 의미가 있다.
마무리하며
이번 포스팅을 작성하며 JPA Transactional에 대해 깊이 그리고 더 정확히 알게 되고 다시 한번 생각해 보는 계기가 되었다.
기회가 된다면 꼭 카카오페이의 블로그 포스팅을 살펴보는 것을 추천한다.
'Spring > JPA' 카테고리의 다른 글
[JPA] getReferenceById() 리마인드 (0) | 2024.12.23 |
---|---|
[QueryDSL] QueryDSL이란? (0) | 2024.03.30 |
Spring Pagination 연습 (0) | 2024.03.08 |
[SpringBoot] OSIV와 성능 최적화 (0) | 2024.02.01 |
[SpringBoot] JPA Collection 페이징 처리 (0) | 2024.01.29 |