Spring

[SpringBoot] OpenSessionInView

꾸준함. 2022. 3. 29. 11:30

개요

JPA를 사용하는 서비스를 개발하는 도중 Controller에서 데이터를 변경했는데도 불구하고 DB에 변경이 되지 않는 문제가 발생했습니다.

검색해보니 이는 Transactional 범위 밖에서 데이터를 변경했기 때문에 발생했던 문제이고 자세한 내용을 알기 위해서는  OpenSessionInView(OSIV) 동작 원리를 알아야 한다고 해서 간단히 정리해보겠습니다. (OSIV는 hibernate에서 사용하는 용어이고 사실 JPA에서는 OpenEntityManagerInView(OEIV)라고 지칭합니다.)

내용을 잘 정리해주신 Shi._.D TIL님께 감사드립니다.

 

* 해당 글은 JPA 영속성 컨텍스트에 대해 안다는 전제 하에 작성했습니다.

https://jaimemin.tistory.com/1898

 

[JPA] PersistenceContext 간단 정리

개요 이번 게시글에서는 JPA 내부 구조 및 동작 방식을 파악하는데 중요한 개념인 PersistenceContext(영속성 컨텍스트)에 대해 알아보겠습니다. 1. EntityManagerFactory & EntityManager JPA 내부 구조를 살펴보..

jaimemin.tistory.com

 

1. OSIV 요약

OSIV는 JPA PersistenceContext를 뷰 렌더링이 끝날 때까지 유지해주는 역할을 수행합니다.

OSIV는 뷰를 랜더링 할때까지 영속성 컨텍스트를 유지하기 때문에 Lazy Loading을 지원하며 엔티티 객체 변경은 반드시 트랜잭션 안에서 수행해야합니다.

트랜잭션 안에서 데이터 변경을 수행해야 하는 이유는 영속 상태인 객체들은 트랜잭션 내에서 객체 상태의 변경이 감지되었다가 트랜잭션이 종료될 때 PersistenceContext에 의해 DB에 반영되기 때문입니다.

앞서 개요에 설명했다시피 컨트롤러에서 객체를 변경했을 때 DB에 반영되지 않았던 이유는 Controller가 트랜잭션 범위 밖에 위치하고 있기 때문입니다.

따라서, 위 문제를 해결하기 위해서는 데이터 변경하는 기능을 Service로 위임 후 트랜잭션 내에서 처리하거나 Controller에서 Repository를 직접 사용하면 됩니다. (Repository에는 기본적으로 @Transactional 어노테이션이 붙어 있습니다.)

 

2. 과거의 OSIV 동작 원리 (Transaction per Request)

OSIV의 가장 간단한 구현 방법은 클라이언트 요청이 들어오자마자 인터셉터나 필터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이며 이를 트랜잭션 방식의 OSIV라고 말합니다.하지만 해당 방법에는 Controller나 View 같은 프레젠테이션 계층이 엔티티를 변경할 수 있다는 문제점이 있습니다.OSIV가 활성화되어 있을 경우 엔티티가 항상 영속 상태이기 때문에 변경 감지 기능이 동작하여 @Transactional로 감싸 져있지 않은 프레젠테이션 계층에서도 객체 변경이 즉각 DB에 반영되기 때문에 해당 문제가 발생합니다.

 

위 문제를 방지하는 방법들은 아래와 같습니다.

  • 엔티티를 read-only 인터페이스로 제공
  • 엔티티를 ready-only 메서드만 가진 Wrapper 클래스로 매핑
  • DTO만 반환

 

하지만 위에서 언급한 해결책들은 모두 추가적인 코드를 작성해야 한다는 점과 관리 포인트가 늘어난다는 단점이 존재합니다.

따라서, 요즘에는 Transaction per Request 방식의 OSIV를 적용하지 않고 비즈니스 계층에서 Transaction을 유지하는 장식의 OSIV를 사용하며 이 것이 Spring 프레임워크가 제공하는 OSIV입니다.

 

3. Spring 프레임워크에서 제공하는 OSIV (비즈니스 계층 Transaction)

스프링 프레임워크에서 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 적용하는 OSIV이며 2번에서 소개한 Transaction per Request OSIV의 문제점을 보완해주는 방식입니다.

 

스프링은 아래와 같이 다양한 OSIV를 제공합니다.

  • org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor
  • org.springframework.orm.hibernate5.support.OpenSessionInViewFilter
  • org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor
  • org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter

 

Spring 프레임워크에서 제공하는 OSIV의 동작 원리는 아래와 같습니다.

  • 클라이언트 요청이 들어오면 스프링 인터셉터 혹은 서블릿 필터에서 PersistenceContet를 생성 (트랜잭션이 시작하는 것은 아님)
  • Repository 혹은 Service 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 PersistenceContext를 찾아와서 트랜잭션을 시작
  • Repository 혹은 Service 계층이 종료되면 트랜잭션을 커밋하고 PersistenceContext를 flush (트랜잭션은 끝, PersistenceContext는 아직 존재)
  • Controller와 View까지 PersistenceContext가 유지되므로 조회한 엔티티는 영속 상태를 유지 (따라서, lazy loading 가능)
  • 스프링 인터셉터나 서블릿 필터로 요청이 들어오면 flush 없이 PersistenceContext 종료

 

위 동작 원리를 보면 PersistenceContext가 프레젠테이션 계층까지 살려두면서 변경을 불가능하게 만든 것을 확인할 수 있습니다. (Transaction per Request OSIV의 단점 보완)

 

위 내용을 보면 왜 제가 컨트롤러에서 변경한 객체 데이터가 DB에 반영이 되지 않은 것을 확인할 수 있습니다.

하지만, 스프링에서 제공하는 OSIV도 프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출할 경우 문제가 발생하며 아래 코드를 통해 문제점을 파악할 수 있습니다.

 

 

위 코드는 아래와 같이 동작합니다.

  • 서비스의 getAccount를 통해 불러온 Account를 PersistenceContext에 저장 (트랜잭은 시작하지 않은 상태)
  • 불러온 Account 객체의 nickname을 변경
  • 트랜잭션이 존재하는 비즈니스 로직 실행
  • 트랜잭션 AOP가 동작하면서 PersistenceContext의 트랜잭션이 시작되며 randomLogicWithTransaction() 메서드 수행
  • randomLogicWithTransaction() 메서드가 종료되면 트랜잭션 AOP는 트랜잭션을 커밋하고 PersistenceContext를 flush -> nickname이 변경된 상태로 DB에 반영

 

위 문제는 하나의 PersistenceContext를 여러 트랜잭션이 공유함에 따라 발생한 문제입니다.

위 문제를 해결하기 위해서는 Service 계층에서 트랜잭션이 있는 비즈니스 로직을 모두 처리한 후 프레젠테이션 계층에서 엔티티를 변경해야 합니다.

 

* 트랜잭션이 같을 경우 항상 같은 PersistenceContext를 사용하고, PersistenceContext와 트랜잭션의 생명주기는 동일

* OSIV를 사용하지 않을 경우 트랜잭션의 생명주기와 PersistenceContext의 생명주기가 같으므로 위 문제들이 발생하지 않음

* 스프링에서 제공하는 OSIV는 같은 PersistenceContext를 여러 트랜잭션이 공유할 수 있으므로 문제 발생 가능

 

4. 정리

스프링에서 제공하는 OSIV의 장점

  • 엔티티 수정이 트랜잭션이 있는 계층에서만 동작
  • 트랜잭션이 없는 프레젠테이션 계층에서는 Lazy Loading을 포함한 조회만 가능

스프링에서 제공하는 OSIV의 단점

  • PersistenceContext를 여러 트랜잭션이 공유할 수 있어 문제가 발생 가능(특히 Rollback 시 골치 아픔)
  • 프레젠테이션 계층에서 Lazy Loading에 의해 SQL이 실행되기 때문에 쿼리 튜닝 시에 관리 포인트가 넓어짐
  • OSIV는 엔티티를 영속 상태로 유지하기 때문에 DB 커넥션 리소스를 길게 사용해 실시간 트래픽이 중요한 서비스일 경우 커넥션이 모자라 장애 발생 가능

 

정리하자면 OSIV는 Spring 프레임워크에서 디폴트로 활성화되어 있고 여러모로 편리하지만 만능은 아닙니다.

예를 들자면, 복잡한 화면의 경우 엔티티로 조회하기보다 DTO로 조회하는 것이 성능상 유리합니다.

또한, OSIV는 같은 JVM을 벗어난 원격 상황(원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것이 불가능)에서는 클라이언트에서 필요한 데이터를 모두 JSON을 생성해서 반환해야 한다는 제약사항이 존재합니다.

 

따라서 권장하는 방법은 아래와 같습니다.

  • 실시간 트래픽이 중요한 경우 OSIV 사용하지 말고, DTO로 직접 조회
  • Admin 페이지 같이 실시간 트래픽이 중요하지 않을 경우 OSIV 사용

 

OSIV를 수동으로 비활성화하는 방법은 아래와 같습니다.

application.properties에 아래 줄 추가

spring.jpa.open-in-view=false

 

참고

https://ttl-blog.tistory.com/183

 

[JPA] OSIV란? (feat. 스프링 JPA의 작동원리, 퍼사드 패턴 등)

OSIV에 다가가기까지 조금 서론이 길다. (JPA의 작동원리, FACADE 계층 등) OSIV에 대해서만 궁금하다면 그쪽 부분만 찾아서 보길 바란다. 스프링에서 JPA를 사용하게 되면, 스프링 컨테이너가 트랜잭

ttl-blog.tistory.com

 

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발 (백기선 강사님)

반응형