@Transactional 제대로 사용하기
원래 이전에는 Transactional이 어떻게 동작하는지에 대해서만 알고 있었다.
하지만 이번 프로젝트를 진행하면서 자꾸 DB 커넥션 pool starvation 에러가 발생하고, 이게 OOM 문제로까지 이어질 수도 있겠다고 판단하여 제대로 해결해야 하는 상황이 됐다.
그래서 더 공부를 해보니, 지금까지 @Transactional을 잘못 사용하고 있었다는 걸 알 수 있었다.
솔직히 그냥 아무생각 없이 서비스 클래스 전체에 Transactional을 적용했었는데, 이게 문제를 만들 수 있다는 점을 생각하지 못하고 있었다.
예를 들어, 외부 API를 호출하는 부분에 막 Transactional을 붙여놓고, 해당 요청이 20초 정도 걸리는 요청이라고 가정해보자.
트랜잭션 범위 안에서 DB 접근이 한 번이라도 발생하면, 이후 로직에서 DB를 더 이상 사용하지 않더라도 해당 스레드는 메서드 종료 시점까지 커넥션을 반환하지 못한 채 유지된다.
이 상태가 반복되면 커넥션 풀은 빠르게 고갈되고, 커넥션을 할당받지 못한 요청 스레드들이 대기 큐에 쌓이게 된다.
결국 요청 지연이 누적되고, 대기 중인 스레드와 객체들이 메모리를 계속 점유하면서 OOM으로 이어질 수 있는 구조가 만들어진다.
이 경험을 통해 알게 된 건, @Transactional이 단순히 롤백과 커밋을 보장해주는 기능이 아니라는 점이었다.
트랜잭션의 범위를 어떻게 설정하느냐가, 서비스의 안정성과 직결될 수 있다는 사실을 체감하게 되었다.
그래서 이후에는 트랜잭션을 클래스 전체에 기본으로 적용하는 방식이 아니라, 정말로 DB 정합성이 필요한 구간에만 최소한으로 적용해야 한다는 기준을 세우게 되었다.
정리해보면 다음과 같다.
- 트랜잭션 범위는 최대한 짧게 유지
- 외부 API 호출, 네트워크 통신 로직은 트랜잭션 외부에서 처리
- DB 변경이 필요한 메서드에만 @Transactional 적용
- 서비스 클래스 단위의 무분별한 적용 지양
이번 문제를 겪으면서, 성능 이슈나 장애는 서버 스펙이나 설정 때문만이 아니라, 어노테이션 하나의 사용 방식에서도 충분히 발생할 수 있다는 걸 확실히 느끼게 되었다.
이에 더해, @Transactional이 제공하는 옵션들에 대해 이해할 필요성도 느꼈다.
그 중 readOnly 옵션과 트랜잭션 전파 옵션이 있다. 전에 글로 정리한 적은 있지만 아직 제대로 이해하지 못한 것 같아 다시 정리해보았다.
readOnly 옵션
조회 로직에도 습관처럼 @Transactional을 붙이고 있었는데, 사실 단순 조회의 경우에는 트랜잭션이 필요하지 않은 경우도 많다.
그럼에도 불구하고 트랜잭션을 열어버리면, 불필요한 오버헤드가 발생할 수 있다.
@Transactional(readOnly = true)는 해당 트랜잭션이 읽기 전용임을 명시하는 옵션이다.
이 옵션을 사용하면 JPA 기준으로는 변경 감지(Dirty Checking)가 비활성화되고, 일부 구현체에서는 flush 자체가 생략되기도 한다.
정리하면 다음과 같은 효과가 있다.
- 변경 감지 비활성화
- 불필요한 flush 방지
- 읽기 전용 쿼리에 대한 의도 명확화
- 조회 성능 및 리소스 사용 측면에서 유리
물론 readOnly = true가 커넥션을 아예 사용하지 않는다는 의미는 아니지만, 불필요한 작업을 줄여주는 명확한 힌트가 된다는 점에서 충분히 의미가 있다.
트랜잭션 전파 옵션
다음으로 트랜잭션 전파 옵션이다.
이번 문제를 겪으면서 느낀 건, 트랜잭션은 “있다 / 없다”의 문제가 아니라 어디서 시작되고, 어디까지 이어지느냐의 문제라는 점이었다.
이때 중요한 역할을 하는 것이 바로 트랜잭션 전파 옵션이다.
예를 들어 상위 메서드에 트랜잭션이 걸려 있고, 그 안에서 호출되는 하위 메서드들도 모두 같은 트랜잭션에 묶여 있다면, 의도하지 않게 트랜잭션 범위가 계속 확장되는 구조가 만들어질 수 있다.
특히 외부 API 호출이나 시간이 오래 걸리는 로직이 이런 흐름 안에 포함되어 있다면, 앞서 겪었던 커넥션 점유 문제가 그대로 반복될 수 있다.
이런 상황에서는 REQUIRES_NEW처럼 기존 트랜잭션과 분리된 트랜잭션을 명시적으로 사용하는 방식이나, 아예 트랜잭션 외부로 로직을 분리하는 설계가 필요하다.
결국 전파 옵션은 단순한 문법 요소가 아니라,
트랜잭션 범위를 의도대로 제어하기 위한 중요한 설계 도구라는 걸 다시 한 번 느끼게 되었다.