T_era

쿼리를 직접적으로 작성하는 건 아쉬워서 DSL을 사용해봤다 근데 왜 업데이트가 반영이 안되지? 본문

Programing/Spring

쿼리를 직접적으로 작성하는 건 아쉬워서 DSL을 사용해봤다 근데 왜 업데이트가 반영이 안되지?

블스뜸 2025. 5. 23. 17:23

JPA를 사용하며 JpaRepository.save() 메서드가 특정 필드만 변경되었음에도 불구하고 UPDATE 쿼리가 전체 필드를 갱신하는 것처럼 보일 수 있다는 의문을 가지게 되었다.
(정확히는 JPA의 변경 감지(Dirty Checking)는 기본적으로 변경된 필드만 갱신하지만, 특정 상황이나 구현체에 따라 전체 필드를 갱신하는 쿼리가 발생할 수도 있다.)
현재 프로젝트에서는 업데이트 요청이 적어 큰 문제가 없으나, 향후 성능 튜닝을 위해 QueryDSL 도입을 고려하게 되었다. 그런데 QueryDSL 적용 시 영속성 컨텍스트와 관련된 새로운 문제에 직면하게 되었다.

오늘은 QueryDSL이 무엇인지 정리하고, 영속성 컨텍스트와 어떤 문제가 발생할 수 있는지 함께 살펴보겠다.

QueryDSL이란?

QueryDSL은 데이터베이스 접근 시 메서드 체인 형태의 쿼리 문법을 사용하여 SQL(또는 JPQL)을 타입 세이프하게 작성할 수 있도록 돕는 **외부 라이브러리(DSL)**이다.

QueryDSL을 사용하는 이유:

  1. 타이핑 이슈 해결 (타입 안정성):
    • 직접 SQL 쿼리문을 문자열 형태로 작성할 경우, 오타나 문법 오류가 발생해도 컴파일 시점에는 이를 감지하지 못하여 런타임 오류로 이어진다. 이는 문제 추적 및 해결을 어렵게 만든다.
    • QueryDSL은 Q-Entity라는 메타모델 클래스를 사용하여 엔티티 필드를 기반으로 쿼리를 작성하므로, 컴파일 시점에 오류를 발견할 수 있어 타입 안정성을 보장한다.
  2. 쿼리의 복잡성 해소 (직관성):
    • 복잡한 SQL 쿼리는 문자열 형태로 작성될 때 가독성이 떨어지고 해석하기 어렵다. 이는 유지보수 비용을 증가시킨다.
    • QueryDSL은 SQL과 유사하면서도 객체지향적인 문법을 제공하여, 복잡한 쿼리도 직관적으로 구성하고 이해할 수 있도록 돕는다.
  3. 쿼리의 유연성 확보 (동적 쿼리 용이):
    • 조건에 따라 쿼리 구성 요소가 동적으로 추가되거나 제외되어야 하는 상황에서 문자열 쿼리는 관리하기 매우 어렵다.
    • QueryDSL은 BooleanBuilder 또는 Predicate와 같은 기능을 활용하여 조건에 따라 쿼리를 유연하게 조합할 수 있는 동적 쿼리 작성을 매우 용이하게 한다. 이는 검색 조건이 많거나 복잡하게 조합되는 경우에 특히 유용하다.
  4. 다양한 환경 지원:
    • QueryDSL은 JPA/Hibernate뿐만 아니라 JDBC, SQL, MongoDB, Lucene 등 다양한 데이터 저장 기술에 대한 쿼리 작성을 지원한다. 이를 통해 여러 기술 스택을 사용하는 프로젝트에서도 일관된 쿼리 작성 경험을 제공한다.

QueryDSL 사용 시 영속성 컨텍스트 문제

QueryDSL은 위와 같은 장점들을 가지고 있지만, JPA의 영속성 컨텍스트와 함께 사용할 때 주의해야 할 부분이 있다. 이는 QueryDSL 자체의 문제가 아니라, JPQL(Java Persistence Query Language)의 벌크 연산이나 Native SQL 쿼리가 영속성 컨텍스트에 있는 엔티티의 상태를 직접 업데이트하지 않기 때문에 발생하는 문제이다.

문제 발생 시나리오:

  • 개발자가 QueryDSL을 사용하여 JPQL UPDATE 또는 DELETE 쿼리(벌크 연산)를 실행하여 데이터베이스의 데이터를 직접 변경한다.
  • 그러나 영속성 컨텍스트에는 여전히 변경 전 상태의 엔티티 객체가 캐싱되어 있을 수 있다.
  • 이후 해당 엔티티를 영속성 컨텍스트에서 다시 조회하거나 사용하려 하면, 실제 데이터베이스의 변경된 내용이 아닌, 영속성 컨텍스트에 캐싱된 오래된(Stale) 데이터를 참조하게 되어 데이터 불일치가 발생할 수 있다.

해결 방안:

이러한 데이터 불일치 문제를 해결하기 위해서는 몇 가지 방법이 있다.

  1. 영속성 컨텍스트 초기화 (Clear): 벌크 연산 후 EntityManager.clear()를 호출하여 영속성 컨텍스트를 비워버린다. 이렇게 하면 다음에 엔티티를 조회할 때 데이터베이스에서 최신 데이터를 다시 불러온다.
  2. 영속성 컨텍스트 동기화 (Refresh): 변경된 엔티티를 EntityManager.refresh()를 통해 데이터베이스의 최신 상태로 강제 동기화할 수 있다. (단, 변경된 엔티티가 명확할 때 사용)
  3. 트랜잭션 분리: 벌크 연산을 별도의 트랜잭션으로 분리하고, 해당 트랜잭션이 종료된 후 새로운 트랜잭션에서 데이터를 조회하면 영속성 컨텍스트가 초기화되어 최신 데이터를 가져올 수 있다.

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까지 트랜잭션 범위를 확장 시키는 것이라 볼 수 있다