Spring에서는 서비스 계층이나 레포지토리 계층에서 트랜잭션을 제어하기 위해 @Transactional 애노테이션을 자주 사용한다. 이 중에서도 @Transactional(readOnly = true)는 트랜잭션이 조회 전용임을 명시하는 설정으로, 내부적으로 다양한 성능 최적화를 가능하게 한다. 하지만 이 설정이 되어 있는 트랜잭션 안에서 직접 SQL을 사용하여 데이터를 수정하거나 삽입하는 경우에는 여러 가지 심각한 문제가 발생할 수 있다. 이 글에서는 그 이유와 발생할 수 있는 단점들을 상세히 정리하고자 한다.

readOnly = true는 단순히 성능 향상을 위한 옵션이 아니라, 해당 트랜잭션 내에서 쓰기 작업은 절대로 발생하지 않을 것이라는 개발자의 약속에 가깝다. Hibernate(JPA 구현체)는 이 설정을 통해 변경 감지(Dirty Checking)를 비활성화하고, 트랜잭션 커밋 시 flush() 호출도 생략한다. 또한 일부 데이터베이스는 이를 감지해 락을 줄이거나 읽기 전용 트랜잭션에 최적화된 실행 계획을 적용하기도 한다. 따라서 이 설정은 조회 전용 트랜잭션에서는 확실한 성능 이점을 제공한다.

문제는 이러한 readOnly 트랜잭션 내에서 JPA를 우회하여 직접 SQL을 실행하는 경우이다. 예를 들어 JdbcTemplate.update(...)나 EntityManager.createQuery("UPDATE ...") 같은 방식으로 데이터베이스에 직접 쓰기를 수행할 수는 있다. 하지만 이는 기술적으로 가능할 뿐, 여러 면에서 위험한 방식이라 할 수 있다.

가장 큰 문제는 영속성 컨텍스트(Persistence Context)와 실제 데이터베이스 상태 사이의 불일치이다. JPA는 트랜잭션 범위 내에서 조회한 엔티티들을 1차 캐시에 저장하고, 이를 기준으로 데이터를 관리한다. 그런데 직접 SQL을 통해 DB를 수정하면 JPA는 그 사실을 알지 못하기 때문에, 동일한 트랜잭션 안에서 해당 엔티티를 다시 조회하거나 사용할 때 수정 전 상태의 값을 계속 유지하게 된다. 이로 인해 트랜잭션 내에서 데이터 일관성이 무너지는 결과를 초래할 수 있다.

또한 직접 SQL을 사용하면 JPA가 제공하는 다양한 부가 기능이 모두 무력화된다. 예를 들어 엔티티의 변경 시 자동으로 수행되는 1차/2차 캐시 무효화, 엔티티 생명주기 이벤트(@PreUpdate, @PostUpdate), 감사 로깅(@LastModifiedDate), 버전 관리(@Version) 등의 기능이 전혀 작동하지 않는다. 이로 인해 캐시 불일치, 로깅 누락, 병행 수정 충돌 등의 문제가 발생할 수 있다.

더 나아가, 직접 SQL을 사용하는 방식은 코드의 객체지향성을 저해한다. JPA는 도메인 중심 설계를 바탕으로 쿼리 추상화와 타입 안정성을 제공하는데, 직접 SQL을 사용하면 이러한 이점을 모두 포기하게 된다. SQL과 비즈니스 로직이 섞이면서 응집도가 떨어지고, 유지보수 시 쿼리와 객체 모델의 변경을 동시에 고려해야 하므로 개발 효율성도 크게 저하된다. 특히 대규모 프로젝트나 협업 환경에서는 이러한 방식이 기술 부채로 이어지기 쉽다.

물론, 직접 SQL이 필요할 수밖에 없는 상황도 존재한다. 예를 들어 대용량 배치 처리나 통계성 집계 쿼리처럼 객체 단위가 아닌 행 단위로 일괄 수정이 필요한 경우에는 SQL이 더 적합할 수 있다. 이럴 때는 readOnly = true 트랜잭션이 아닌 별도의 쓰기 트랜잭션을 분리하여 처리하는 것이 바람직하다. 또는 해당 작업을 별도의 서비스로 분리하거나, flush() 호출 이후 강제로 영속성 컨텍스트를 갱신하는 등의 방식으로 일관성을 유지하는 방법도 고려할 수 있다.

결론적으로 @Transactional(readOnly = true)는 단순한 설정이 아니라, 개발자가 “이 트랜잭션은 오직 읽기만 수행한다”고 명확히 선언하는 행위이다. 이 약속을 어기고 직접 SQL을 통해 쓰기를 시도하면, 내부적으로는 아무런 예외 없이 동작할 수도 있지만, 그 뒤에 따라오는 여러 가지 부작용은 프로젝트의 안정성과 일관성을 해치는 주범이 될 수 있다. 따라서 readOnly 트랜잭션 내에서는 정말로 읽기만 수행하는 것이 바람직하며, 불가피한 쓰기 작업이 필요한 경우에는 반드시 트랜잭션을 분리하거나 적절한 설정 변경을 통해 우회 전략을 세우는 것이 좋다.

'SPRING (SPRINGBOOT)' 카테고리의 다른 글

JPA는 왜 트랜잭션을 강제할까?  (0) 2025.04.19