OSIV - Spring Data JPA 를 시작하기 전에 꼭 알았어야 할 옵션

IAMCODER·2021년 9월 15일
0
post-custom-banner

신규 프로젝트에 JPA 를 도입하기 위해 팀원들과 열심히 스터디 하고 프로젝트를 진행했다.
서로 힘을 합쳐 프로젝트를 거의 마무리 했을 무렵 청천벽력 같은 말을 들었다.

"조회가 너무 느려요!"

개발 할 떄는 문제가 발생하지 않았지만 QA지 조회 속도가 느리다는 점이 드러나게 되었다.
문제가 되는 조회 기능을 확인해보니 N+1 쿼리 문제처럼 보였다. 한 번 조회시 관계를 가지는 엔티티들을 전부 가져오고 있었다.
fetch type 을 살펴봤지만 설정된 값은 Lazy 였다. Entity graph 설정도 문제가 안되었다. 조회 기능이었기 때문에 transaction 이 걸려 있지도 않았다.

대체 무엇이 fetch type 설정도 무시하면서 불러오는걸까?

OSIV - Open Session in View

흔히 OSIV 로 알려진 영속성 컨텍스트 관리 방법은 Open Session in View 라는 단어 처럼 요청을 받는 순간부터 응답을 하는 순간 까지 DB와의 세션을 유지시킨다는 전략이다.
굳이 이런 옵션이 있는 이유는 무엇일까?
JPA 를 사용하면서 지연된 읽기로 LazyInitializationException을 본 경험은 한번씩 있을 것이다. 이 문제를 회피하는 가장 빠른 방법은 비즈니스 로직을 트랜젝션으로 묶는 것이다(물론 개인적으로 추천하지는 않음). 트랜젝션 내에서는 아직 DB와의 세션이 유지되기 때문에 fetch 타입을 lazy 로 설정해두어도 관계된 엔티티 값을 조회 할 때 DB에서 바로 찾아서 전달 할 수 있다.

OSIV 도 이와 유사한 방식을 가능하게 하는 옵션이다. 차이점이 있다면 트랜젝션을 서버 응답 내용을 작성할 때 까지 유지시킨다는 점이다. OSIV는 개발 편의성을 위한 것으로 이것을 끈채로 Thymeleaf로 랜더링을 하거나 json 으로 전송할 때 엔티티에 연관관계로 정의된 맴버변수를 get 만 해도 LazyInitializationException이 발생한다.

이처럼 설정하면 여러모로 편리한 기능이지만 한가지 문제가 있다.

N + 1 쿼리 문제

N + 1 쿼리 문제는 lazy loading 으로 설정한 하위 엔티티를 처음 쿼리시 불러오지 않고 필요한 시점에 따로 조회하여 성능을 떨어뜨리는 것을 말한다. OSIV 설정이 비활성화된 경우이거나 서비스 레이어 테스트 코드와 같이 servlet 을 타지 않고 동작할 떄에는 N + 1 문제는 발생할 수 없다. lazy loading 으로 설정된 하위 엔티티를 get 하는 순간 LazyInitializationException 이 발생하기 때문이다. 그렇지만 OSIV 가 활성화 된 상태에서 servlet 으로 들어온 요청을 처리 할 때에는 트랜젝션이 계속 유지 되기 때문에 하위 엔티티를 조회하는 족족 쿼리가 실행되며 단순히 Null 확인을 위한 get 에서도 쿼리가 실행된다. 물론 비즈니스 로직 상에서는 케이스에 따라 거의 차이가 없을 수 있다.

하지만 엔티티를 DTO 로 변환시킬 때 문제가 발생한다. 보통 이런 작업은 Map struct 나 Model mapper 같은 라이브러리를 사용한다. DTO 에 하위 엔티티의 값들도 함께 저장된다면 별도로 설정하지 않는 이상 DTO 로 매핑할 떄 마다 하위 엔티티를 조회하게 된다.

만약 연관 관계가 복잡한 엔티티를 DTO 로 변환하게 될 경우 설계에 따라 단순한 조회에도 엄청난 수의 N + 1 쿼리가 실행되기 때문에 조회 속도가 엄청나게 느려 질 수 있다. 설상가상으로 퍼포먼스를 위해 OSIV 를 비활성화 한 후 변환을 하게 되면 LazyInitializationException 이 발생한다.

OSIV 를 끈 상태에서 하위 엔티티까지 함께 변환하려면 Hibernate.isInitialized 메소드로 영속성 컨텍스트에 불러와졌는지 확인해야 한다. 연관 관계를 가지는 하위 엔티티는 하이버네이트에서 관리하므로 null이 아니기 때문에 null 여부로 초기화 되었는지 확인할 수 없다. 그래서 Map struct 나 Model mapper 를 사용해 변환하는 경우 직접 변환 과정을 구현해야 한다.

그래서 OSIV 를 사용해야 하나?

개인적으로는 비활성화 하는 것을 추천한다. 토이 프로젝트 정도는 편의상 활성화 시켜서 할 수 있으나 실제 운영될 서비스는 비활성화 한 상태에서 진행하는 것이 안전한 것으로 생각된다.

혹은 OSIV를 활성화 한 상태에서 서비스 계층이나 DAO 계층에 대한 테스트 코드를 작성하는 방법도 있다. OSIV 는 servlet 을 통한 요청을 처리할 때에만 적용되므로 서비스와 DAO 계층에 대해 테스트 코드에서는 별도의 처리가 없다면 LazyInitializationException가 발생해 테스트 코드를 통과하기 위해서는 OSIV 를 비활성화 한 경우와 동일하게 작성해야 하고 OSIV 의 편리함은 취하면서 N + 1 쿼리 문제가 발생하는 것을 막을 수 있다.

post-custom-banner

0개의 댓글