OSIV와 성능 최적화

작성일

OSIV(Open Session In View)

OSIV란 영속성 컨텍스트를 뷰까지 열어두는 기능이다. 영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다. 뷰까지 영속성 컨텍스트가 살아있다면 뷰에서도 지연 로딩을 사용할 수가 있다.

하이버네이트에서는 OSIV(Open Session In View), JPA에서는 Open EntityManager In View라 한다. 하지만, 관례상 OSIV라 한다. OSIV를 알아야한다. 이것을 모르면 장애로 이어질 수 있다.

OSIV 동작 원리

스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV다. 영속성 컨텍스트는 사용자의 요청 시점에서 생성이 되지만, 데이터를 쓰거나 수정할 수 있는 트랜잭션은 비즈니스 계층에서만 사용할 수 있도록 트랜잭션이 일어난다.

참고 JPA가 데이터베이스 커넥션을 가져오고 반환하는 시점?
JPA의 영속성 컨텍스트는 데이터베이스 커넥션과 밀접한 연관이 있다. 영속성 컨텍스트는 데이터베이스 커넥션을 1:1로 쓰면서 동작한다. 그렇다면 영속성 컨텍스트는 언제 데이터베이스 커넥션을 가져올까? 데이터베이스 트랜잭션을 시작할 때 JPA의 영속성 컨텍스트가 데이터베이스 커넥션을 가져온다. 그리고 언제 커넥션을 반환할까? OSIV가 ON일 때는 모든 응답이 끝나고나면 반환되고 OSIV가 OFF일 때는 트랜잭션이 종료되는 시점에 같이 종료된다.

OSIV ON

1

  • spring.jpa.open-in-view : true, 기본값

spring.jpa.open-in-view의 값을 기본값(true)으로 애플리케이션을 구동하면, 애플리케이션 시작 시점에 warn 로그가 남는다. 프록시를 초기화하는 작업을 Service 계층에서 끝내지 않고도 렌더링 시 자동으로 해결하게 해주는 장점이 있는 OSIV 전략에 왜 경고를 줄까?

2022-08-16 13:41:55.714  WARN 38189 --- [  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

OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지한다. 그래서 지금까지 View Template이나 API 컨트롤러에서 지연 로딩이 가능했던 것이다.

지연 로딩은 영속성 컨텍스트가 살아있어야 가능하고, 영속성 컨텍스트는 기본적으로 데이터베이스 커넥션을 유지한다. 이것 자체가 큰 장점이다.

그런데 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다.(커넥션이 마른다고 표현.) 이것은 결국 장애로 이어진다.

예를 들어서 컨트롤러에서 외부 API를 호출하면 외부 API 대기 시간 만큼 커넥션 리소스를 반환하지 못하고, 유지해야 한다. OSIV의 치명적인 단점이다.

OSIV OFF

2

  • spring.jpa.open-in-view : false, OSIV 종료

OSIV를 끄면 트랜잭션을 종료할 때 영속성 컨텍스트를 닫고(em.flush(), em.commit()), 데이터베이스 커넥션도 반환한다. 따라서, 커넥션 리소스를 낭비하지 않는다.

OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다. 결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.

OSIV를 OFF 시킨 상태에서 Controller에서 지연 로딩을 할 경우 에러 로그

org.hibernate.LazyInitializationException: could not initialize proxy [jpabook.jpashop.domain.Member#1] - no Session
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]
	at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]
	at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]
	at jpabook.jpashop.domain.Member$HibernateProxy$fTKfJLZN.getName(Unknown Source) ~[main/:na]
	at jpabook.jpashop.api.OrderApiController.ordersV1(OrderApiController.java:33) ~[main/:na]
...

해결 방안은 OSIV를 ON 하거나, 트랜잭션 안에서 지연로딩을 하거나, 페치 조인을 쓰는 방법이 있다.

커멘드와 쿼리 분리

실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command와 Query를 분리하는 것이다.

보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다. 하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다. 그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미 있다.

단순하게 설명해서 다음처럼 분리하는 것이다.

  • OrderService
    • OrderService: 핵심 비즈니스 로직
    • OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)

보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수 있다.

참고
고객 서비스의 실시간 API(트래픽이 많은 경우)는 OSIV를 끄고, ADMIN 처럼 커넥션을 많이 사용하지 않는 곳에서는 OSIV를 켜는 방식으로 개발을 진행하는 것이 좋다.

참고 커맨드와 쿼리의 분리
Command Query Separation(CQS)를 의미하며 DB의 데이터를 업데이트하는 명령과 조회하는 쿼리를 분리하는 패턴이다. 이 메서드를 호출 했을 때, 내부에서 변경(사이드 이펙트)가 일어나는 메서드인지, 아니면 내부에서 변경이 전혀 일어나지 않는 메서드인지 명확히 분리하는 것. 권장하는 방법은 insert는 id만 반환하고(아무것도 없으면 조회가 안되니), update는 아무것도 반환하지 않고, 조회는 내부의 변경이 없는 메서드로 설계하면 좋다. 이 개념은 개발 전반에 기본개념으로 깔고 가는 것이 좋다.

참고 CQRS 패턴
CQRS 패턴이란, 우리가 보통 이야기하는 CRUD(Create, Read, Update, Delete)에서 CUD(Command)와 R(Query)을 구분하자는 이야기다. 구분하는 이유는, 우리가 Database로부터 데이터를 읽어오고 처리를 하게 되면 이미 그 사이에 데이터가 변경이 되었을 가능성이 높다. CQRS는 이런 변경 가능성을 인정하고 어차피 Read와 CUD 사이에는 delay가 존재할 수 있음을 인정하는 것이다. 이를 통해서 R과 CUD를 구분함으로써 얻는 이점을 설명하는 것이 CQRS패턴이다.

카테고리: