OSIV
OSIV는 Open Session in View의 줄임말로 영속성 컨텍스트 생존 범위를 결정하는 중요한 설정입니다.
영속성 컨텍스트를 Service와 Repository 영역 이상으로 Controller와 View 영역까지 살려둬서 영속성 컨텍스트의 지연 로딩과 같은 기능을 Controller와 View 영역에서 사용할 수 있도록 만듭니다.
OSIV는 정확히는 하이버네이트에서 사용되는 용어이고 JPA에서는 OEIV(Opern EntityManager in View)라고 합니다.
하지만 관례상 OSIV라고 부릅니다.
OSIV는 설정이기 때문에 On/Off 할 수 있습니다. 지금부터 OSIV를 On 했을 때와 Off 했을 때 영속성 컨텍스트의 생존 범위를 확인하고 장단점을 살펴봅시다.
OSIV ON
spring.jpa.open-in-view를 true 또는 false로 설정하여 OSIV를 ON/OFF 할 수 있습니다.
따로 OSIV를 false로 설정하지 않으면 OSIV는 ON으로 설정됩니다.
JPA가 기본적으로 언제 DB 커넥션을 가져오고 반환할까요?
OSIV 설정으로 DB 커넥션을 반환하는 타이밍을 조절할 수 있기 때문에 위와 같은 궁금증으로 시작하겠습니다.
영속성 컨텍스트를 사용하려면 DB 커넥션을 사용해야 하기 때문에 DB 커넥션과 영속성 컨텍스트는 밀접한 연관이 있습니다. 다시 말해 JPA의 영속성 컨텍스트는 DB 커넥션을 1:1로 물고 동작합니다.
그렇다면 DB 커넥션은 JPA가 어떻게 얻을까요? JPA는 DB 트랜잭션(@Transactional)이 시작될 때 DB 커넥션을 가져옵니다. 그리고 트랜잭션이 끝나면 DB 커넥션을 반환하고 영속성 컨텍스트도 끝나는 것입니다.
아래 코드와 같이 Service 계층에서 @Transactional 애노테이션을 사용해 DB 트랜잭션을 시작하고 종료합니다.
@Transactional
public Long addRoom(Room room) {
return roomRepository.save(room);
}
위의 addRoom 함수가 호출되면 DB 트랜잭션이 시작하고 DB 커넥션을 가져와 영속성 컨텍스트가 동작합니다. 그 이후에 addRoom 함수가 return 문을 만나 종료되면 DB 트랜잭션이 종료되며 DB 커넥션도 반환되고 영속성 컨텍스트도 종료됩니다.
OSIV 설정이 개입하지 않는다면 이와 같은 흐름으로 동작하게 됩니다. 하지만 OSIV 설정을 On 하면 DB 커넥션의 반환 시점이 달라집니다. 물론 OSIV를 On 했다고 해서 DB 트랜잭션의 생명주기가 변하진 않습니다.
하지만 아래 그림과 같이 트랜잭션이 끝나도 DB 커넥션을 반환하지 않습니다. DB 커넥션을 트랜잭션 범위 밖에서도 유지하며 프록시 객체 초기화 등과 같은 영속성 컨텍스트의 이점을 View 또는 Controller와 같은 DB 트랜잭션 범위 밖에서 활용하도록 만듭니다.

OSIN ON은 트랜잭션이 끝나도 DB 커넥션을 반환하지 않으며 영속성 컨텍스트를 끝까지 살립니다. API의 경우는 API가 클라이언트에게 데이터를 반환할 때까지 DB 커넥션을 반환하지 않으며 영속성 컨텍스트를 유지하고 View의 경우는 ViewTemplate으로 뷰가 랜더링 될 때까지 유지합니다. 이후에 클라이언트 요청에 따른 응답이 나가면 그때 DB 커넥션을 반환하고 영속성 컨텍스트도 종료합니다.
OSIV 전략은 트랜잭션 시작처럼 DB 커넥션 시작 시점부터 API 응답이 끝날 때까지 영속성 컨텍스트와 DB 커넥션을 유지합니다. 그래서 View Template이나 API Controller 계층에서 프록시 객체에 접근하여 실제 객체를 얻는 지연 로딩이 가능해던 것입니다. 이것 자체가 OSIV 전략의 큰 장점입니다.
만약 Controller 계층에서 영속성 컨텍스트가 살아있지 않을 때 프록시 객체에 접근하면 실제 객체가 아닌 프록시 객체 자체를 반환될 것입니다. 지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 DB 커넥션을 유지합니다.
하지만 기본값으로 설정된 OSIV ON을 그대로 두면, 애플리케이션 시작 시점에 아래와 같은 warn 로그를 남깁니다.
2021-01-18 21:54:44.750 WARN 36808 --- [ restartedMain] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
너무 오랜 시간 동안 DB 커넥션 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있습니다. 이것이 결국 장애로 이어질 수 있기 때문에 warn 로그를 남기는 것입니다.
예를 들어서 컨트롤러가 외부 API를 호출하면 외부 API 대기 시간만큼 DB 커넥션 리소스를 반환하지 못하고, 유지해야 합니다. 또한 외부 API가 블로킹이라도 걸게 되면 스레드 풀이 다 찰 때까지 DB 커넥션을 반환받지 못합니다.
OSIV OFF
OSIV를 OFF는 spring.jpa.open-in-view: false로 설정할 수 있습니다.
OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고, DB 커넥션을 반환합니다. 따라서 DB 커넥션이 부족한 상황을 피할 수 있습니다. OSIV 설정을 OFF 하면 아래 그림과 같이 영속성 컨텍스트의 생존 범위가 바뀝니다.

하지만 OSIV를 끄면 트랜잭션 범위가 곧 영속성 컨텍스트 생존 범위가 되기 때문에 모든 지연 로딩을 트랜잭션 안에서 처리해야 합니다. View나 Controller에서는 더 이상 프록시 객체에 접근해 실제 객체를 얻는 지연 로딩이 불가능합니다.
따라서 트랜잭션이 끝나기 전에 페치 조인을 사용해 지연 로딩을 강제로 호출해서 프록시 객체가 아닌 실제 객체를 얻어 둬야 합니다.
Command(명령)와 Query 분리
실무에서는 OSIV를 끈 상태로 복잡성을 관리하는 게 좋은 방법이라고 합니다. Command와 Query를 분리하여 이를 실천할 수 있습니다. Command와 Query를 분리하는 개념을 CQS 디자인 패턴이라고 부릅니다. CQS 디자인 패턴은 직접 프로젝트에 적용해보고 공부한 이후에 글로 정리해보겠습니다.
보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제 되지 않습니다. 하지만 복잡한 화면이나 API 스펙을 위한 쿼리는 성능을 최적화하는 것이 중요합니다. 하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아닙니다. 그래서 비즈니스 로직과 조회용 쿼리의 관심사를 분리하면 유지보수 관점에서 큰 의미가 있습니다.
예를 들어 OrderService가 있다면 OrderService에는 핵심 비즈니스 로직을 두고 OrderQueryService 클래스를 만들어 화면이나 API에 맞춘 서비스 코드(주로 @Transactional(readOnly = true) 사용)를 둔다면 관심사가 분리된 설계라고 볼 수 있습니다.
고객 서비스의 실시간 API는 OSIV를 끄고, ADMIN처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 키는 방향으로 설계합시다.
'Spring > API 성능 최적화' 카테고리의 다른 글
컬렉션 조회 API 성능 최적화 (V5. JPA에서 DTO 직접 조회) (0) | 2024.10.13 |
---|---|
컬렉션 조회 API 성능 최적화 (V4. hibernate.default_batch_fetch_size) (0) | 2024.10.05 |
컬렉션 조회 API 성능 최적화 (V1 ~ V3. 컬렉션 페치 조인과 데이터 뻥튀기) (1) | 2024.10.01 |
xToOne 관계 조회 API 성능 최적화 (1) | 2024.09.29 |