T_era
쿼리를 직접적으로 작성하는 건 아쉬워서 DSL을 사용해봤다 근데 왜 업데이트가 반영이 안되지? 본문
JPA를 사용하며 JpaRepository.save() 메서드가 특정 필드만 변경되었음에도 불구하고 UPDATE 쿼리가 전체 필드를 갱신하는 것처럼 보일 수 있다는 의문을 가지게 되었다.
(정확히는 JPA의 변경 감지(Dirty Checking)는 기본적으로 변경된 필드만 갱신하지만, 특정 상황이나 구현체에 따라 전체 필드를 갱신하는 쿼리가 발생할 수도 있다.)
현재 프로젝트에서는 업데이트 요청이 적어 큰 문제가 없으나, 향후 성능 튜닝을 위해 QueryDSL 도입을 고려하게 되었다. 그런데 QueryDSL 적용 시 영속성 컨텍스트와 관련된 새로운 문제에 직면하게 되었다.
오늘은 QueryDSL이 무엇인지 정리하고, 영속성 컨텍스트와 어떤 문제가 발생할 수 있는지 함께 살펴보겠다.
QueryDSL이란?
QueryDSL은 데이터베이스 접근 시 메서드 체인 형태의 쿼리 문법을 사용하여 SQL(또는 JPQL)을 타입 세이프하게 작성할 수 있도록 돕는 **외부 라이브러리(DSL)**이다.
QueryDSL을 사용하는 이유:
- 타이핑 이슈 해결 (타입 안정성):
- 직접 SQL 쿼리문을 문자열 형태로 작성할 경우, 오타나 문법 오류가 발생해도 컴파일 시점에는 이를 감지하지 못하여 런타임 오류로 이어진다. 이는 문제 추적 및 해결을 어렵게 만든다.
- QueryDSL은 Q-Entity라는 메타모델 클래스를 사용하여 엔티티 필드를 기반으로 쿼리를 작성하므로, 컴파일 시점에 오류를 발견할 수 있어 타입 안정성을 보장한다.
- 쿼리의 복잡성 해소 (직관성):
- 복잡한 SQL 쿼리는 문자열 형태로 작성될 때 가독성이 떨어지고 해석하기 어렵다. 이는 유지보수 비용을 증가시킨다.
- QueryDSL은 SQL과 유사하면서도 객체지향적인 문법을 제공하여, 복잡한 쿼리도 직관적으로 구성하고 이해할 수 있도록 돕는다.
- 쿼리의 유연성 확보 (동적 쿼리 용이):
- 조건에 따라 쿼리 구성 요소가 동적으로 추가되거나 제외되어야 하는 상황에서 문자열 쿼리는 관리하기 매우 어렵다.
- QueryDSL은 BooleanBuilder 또는 Predicate와 같은 기능을 활용하여 조건에 따라 쿼리를 유연하게 조합할 수 있는 동적 쿼리 작성을 매우 용이하게 한다. 이는 검색 조건이 많거나 복잡하게 조합되는 경우에 특히 유용하다.
- 다양한 환경 지원:
- QueryDSL은 JPA/Hibernate뿐만 아니라 JDBC, SQL, MongoDB, Lucene 등 다양한 데이터 저장 기술에 대한 쿼리 작성을 지원한다. 이를 통해 여러 기술 스택을 사용하는 프로젝트에서도 일관된 쿼리 작성 경험을 제공한다.
QueryDSL 사용 시 영속성 컨텍스트 문제
QueryDSL은 위와 같은 장점들을 가지고 있지만, JPA의 영속성 컨텍스트와 함께 사용할 때 주의해야 할 부분이 있다. 이는 QueryDSL 자체의 문제가 아니라, JPQL(Java Persistence Query Language)의 벌크 연산이나 Native SQL 쿼리가 영속성 컨텍스트에 있는 엔티티의 상태를 직접 업데이트하지 않기 때문에 발생하는 문제이다.
문제 발생 시나리오:
- 개발자가 QueryDSL을 사용하여 JPQL UPDATE 또는 DELETE 쿼리(벌크 연산)를 실행하여 데이터베이스의 데이터를 직접 변경한다.
- 그러나 영속성 컨텍스트에는 여전히 변경 전 상태의 엔티티 객체가 캐싱되어 있을 수 있다.
- 이후 해당 엔티티를 영속성 컨텍스트에서 다시 조회하거나 사용하려 하면, 실제 데이터베이스의 변경된 내용이 아닌, 영속성 컨텍스트에 캐싱된 오래된(Stale) 데이터를 참조하게 되어 데이터 불일치가 발생할 수 있다.
해결 방안:
이러한 데이터 불일치 문제를 해결하기 위해서는 몇 가지 방법이 있다.
- 영속성 컨텍스트 초기화 (Clear): 벌크 연산 후 EntityManager.clear()를 호출하여 영속성 컨텍스트를 비워버린다. 이렇게 하면 다음에 엔티티를 조회할 때 데이터베이스에서 최신 데이터를 다시 불러온다.
- 영속성 컨텍스트 동기화 (Refresh): 변경된 엔티티를 EntityManager.refresh()를 통해 데이터베이스의 최신 상태로 강제 동기화할 수 있다. (단, 변경된 엔티티가 명확할 때 사용)
- 트랜잭션 분리: 벌크 연산을 별도의 트랜잭션으로 분리하고, 해당 트랜잭션이 종료된 후 새로운 트랜잭션에서 데이터를 조회하면 영속성 컨텍스트가 초기화되어 최신 데이터를 가져올 수 있다.
OSIV (Open Session In View)와 영속성 컨텍스트
이러한 영속성 컨텍스트와 관련된 문제는 Spring의 OSIV(Open Session In View) 기능과 결합될 때 더욱 복잡해질 수 있다.
- OSIV의 역할: Spring의 OSIV는 웹 요청의 시작부터 응답이 끝날 때까지 영속성 컨텍스트를 유지하는 역할을 한다. 이는 주로 트랜잭션이 종료된 서비스 계층 이후, View 계층에서 지연 로딩(Lazy Loading)이 필요한 경우 유용하게 사용된다.
- 문제 발생: 하지만 위에서 언급한 QueryDSL을 통한 벌크 연산 등으로 데이터베이스가 직접 변경되었으나 영속성 컨텍스트는 업데이트되지 않은 상황에서 OSIV가 활성화되어 있다면, View 계층에서도 오래된 데이터를 참조하게 될 수 있다. 즉, 서비스 계층에서 트랜잭션이 끝나 데이터베이스에는 변경이 반영되었지만, OSIV로 인해 View 계층에 전달된 영속성 컨텍스트에는 변경 전 상태의 데이터가 남아있어 사용자에게 혼동을 줄 수 있다.
- OSIV 비활성화: 이 문제를 해결하고 개발자가 트랜잭션과 영속성 컨텍스트의 생명주기를 보다 명확하게 제어하고자 할 때 application.properties에 spring.jpa.open-in-view: false 속성을 추가하여 OSIV 기능을 비활성화할 수 있다. OSIV를 비활성화하면 영속성 컨텍스트는 트랜잭션 범위 내에서만 유지되므로, 트랜잭션 종료 후에는 최신 데이터를 다시 조회해야 한다.

OSIV는 해당 이미지에서 VIEW까지 트랜잭션 범위를 확장 시키는 것이라 볼 수 있다
'Programing > Spring' 카테고리의 다른 글
| 좋아요 기능과 동시성 문제? (0) | 2025.06.02 |
|---|---|
| 스프링 시큐리티 인증 및 인가 흐름 (0) | 2025.05.26 |
| 매번 인증 코드를 넣어야하는데 이를 하나로 해결해주는 ArgumentResolver를 사용해보자 (0) | 2025.05.22 |
| @Entity 엔티티에서 사용하는 어노테이션과 이유 (0) | 2025.05.20 |
| Spring Data JPA 인터페이스 메서드 자동 실행 원리 (0) | 2025.05.16 |