1. OSIV(Open Session In View) 란??

OSIV(Open Session In View)은 영속성 컨텍스트 관리 영역을 뷰(View)까지 확장하는 기능이다. Open Session In View라 불리는 이유는 JPA의 구현체인 Hibernate 에서 EntityManager의 구현체로 Session 을 사용한다. JPA에서는 OEIV(Open-EntityManager-In-View) 로 부르지만 통상적으로 Open-Session-In-View 로 부른다.
2. Spring-Boot 의 프로퍼티 기반 OSIV 설정 과정
Spring-Boot 는 프로퍼티를 기반으로 한 자동 설정을 지원한다. JpaBaseconfiguration 추상 클래스의 nested static class 인 JpaWebConfiguration 에에 OSIV 를 담당하는 인터셉터인 OpenEntityanagerInViewInterceptor 를 활성 여부를 관리한다. spring.jpa.open-in-view 프로퍼티를 통해 관리되며 별도의 설정이 없을 경우에는 OpenEntityanagerInViewInterceptor 는 빈으로 등록되고 인터셉터로 관리된다.
spring.jpa.open-in-view: false # default value = true
package org.springframework.boot.autoconfigure.orm.jpa;
// imports ...
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
private final DataSource dataSource;
// 여기에 JpaProperties
private final JpaProperties properties;
private final JtaTransactionManager jtaTransactionManager;
private ConfigurableListableBeanFactory beanFactory;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
@ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true)
protected static class JpaWebConfiguration {
private static final Log logger = LogFactory.getLog(JpaWebConfiguration.class);
private final JpaProperties jpaProperties;
protected JpaWebConfiguration(JpaProperties jpaProperties) {
this.jpaProperties = jpaProperties;
}
// 실제 OSIV 를 담당하는 OpenEntityManagerInViewInterceptor 가 빈으로 선언되는 부분
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
if (this.jpaProperties.getOpenInView() == null) {
logger.warn("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");
}
return new OpenEntityManagerInViewInterceptor();
}
@Bean
public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
OpenEntityManagerInViewInterceptor interceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(interceptor);
}
};
}
}
}
3. OpenEntityManagerInViewInterceptor 의 기본 동작을 확인해보자.
실제 코드를 확인해보면 요청이 들어올 경우에는 엔티티 매니저를 생성하고, 요청이 완료되는 경우에는 엔티티 매니저를 종료하는 것을 확인할 수 있다. 이외에도 엔티티 매니저가 관리되기 위한 TransactionSynchronizationManager, AsyncRequestInterceptor 와 같이 트랜잭션 동기화, 비동기를 위한 작업을 위한 설정이 있는 것을 확인할 수 있다.
package org.springframework.orm.jpa.support;
// imports ...
public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {
public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE";
@Override
public void preHandle(WebRequest request) throws DataAccessException {
String key = getParticipateAttributeName();
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) {
return;
}
EntityManagerFactory emf = obtainEntityManagerFactory();
if (TransactionSynchronizationManager.hasResource(emf)) {
// 트랜잭션 동기화와 관련된 로직
} else {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
// EntityManager 가 생성된다.
try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);
AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
asyncManager.registerCallableInterceptor(key, interceptor);
asyncManager.registerDeferredResultInterceptor(key, interceptor);
} catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
}
}
}
@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
if (!decrementParticipateCount(request)) {
EntityManagerHolder emHolder = (EntityManagerHolder)
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); // 2. 엔티티 매니저를 닫는다.
}
}
}
4. 만약 Filter 에서 엔티티를 관리되어야 한다면?
Spring Security 와 같이 FilterChain 기반에서 엔티티를 초기화해야 하는 경우가 있다. 일반적으로 Open-Session-In-View 는 안티 패턴이라 말한다. 요청이 시작, 종료되는 시점까지 DB Connection 을 잡고 있어 실시간 서비스와 같이 트래픽에 민감한 서비스의 경우에 성능 저하 요소가 될 수 있기 때문이다. 하지만 사내의 시스템이 레거시 코드로 인해 필터 쪽에서 엔티티를 초기화해야 되서 불가피하게 사용해야 한다면 OpenEntityManagerInViewFilter 을 선언해서 사용해야 한다. OpenEntityManagerInViewFilter 는 스프링에서 지원해주기 때문에 해당 빈을 등록해서 사용하도록 하자.
package org.springframework.orm.jpa.support;
// 여러가지 import
public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
// 비즈니스 로직
}
// source : https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/
@Component
@Configuration
public class OpenEntityManagerConfig {
@Bean
public FilterRegistrationBean<OpenEntityManagerInViewFilter> openEntityManagerInViewFilter() {
FilterRegistrationBean<OpenEntityManagerInViewFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
filterFilterRegistrationBean.setOrder(Integer.MIN_VALUE); // 최우선 순위로 Filter 등록
return filterFilterRegistrationBean;
}
}
5. 내가 생각하는 open-session-in-view
흔히 open-session-in-view 를 안티 패턴이라 부른다. 이전에 언급했듯이 실시간 트래픽이 중요한 서비스의 경우, 요청 시점에 EntityManager 가 생성되면서 DB Connection Pool 의 Connection 을 붙잡고 있어 Connection Pool 이 고갈될 수 있는 문제가 발생할 수 있다.
open-session-in-view 를 활성화 할 경우, DB 작업이 모두 완료되고 다른 외부 API 요청을 대기하는 시간까지 Connection 을 반환하지 않는다. 이와 같은 병목지점이 생기고 해당 로직이 많은 사용자가 사용한다면 ConnectionPool 의 Connection 을 모두 잡는다면 Connection 을 반환하기를 기다리는 문제가 발생할 수 있다. 이는 사용자에게 나쁜 사용 경험을 제공해주기 때문에 특별한 이유가 없다면 사용하지 않는 것이 적합한 것 같다.
hikariCP 에서 제공하는 프로퍼티들의 기본 값 정도는 참고로 알고 있자.
# 주의 : DB 단위 - s, Spring Boot 단위 - ms
# Spring Boot 설정을 Database 설정보다 적게 설정하는 것을 권장
spring.datasource.hikari.minimumIdle=30000 #30초
spring.datasource.hikari.connectionTimeout=30000 #30초
spring.datasource.hikari.maximumPoolSize=10
spring.datasource.hikari.idleTimeout=600000 # 10분
spring.datasource.hikari.maxLifeTime=1800000 # 30분
spring.datasource.hikari.autoCommit=true
비록 thymeleaf, jsp 와 같은 템플릿 엔진을 어드민 페이지로 사용하는 경우에 편의성을 가져다w주는 장점이 있지만 편리함으로 인해 템플릿 엔진에 의존적인 코드가 발생하고 vue.js 나 react 로 전환할 때의 공수가 늘어날 수 있다. 만약 기존 thymeleaf, jsp 와 같은 템플릿 엔진과 open-session-in-view 활성화해서 개발하고 있다면 최대한 엔티티 초기화를 템플릿 엔진에서 사용하지 않도록 하고, 엔티티 대신에 DTO 를 활용하는 방향으로 코드를 작성하는 방식으로 코드 작업을 하는 것이 낮은 의존성을 기반해서 유지보수하기 용이하지 않을까 싶다.
- 신규 프로젝트에서 사용한다면 open-session-in-view 비활성화 하기
- 기존 프로젝트에서 사용한다면 open-session-in-view 를 비활성화하는 방향으로 작업하기 (e.g. DTO)
6. summary
- OSIV 는 영속성 컨텍스트의 라이프 사이클을 요청, 응답이 끝날 때까지 활성화하는 전략을 말한다.
- OSIV 는 OpenEntityManagerInViewInterceptor, OpenEntityManagerInViewFilter 를 통해 영속화 범위가 확장된다.
- OSIV 는 다른 로직의 처리가 길어지는만큼 Connection 을 붙잡고 있어 커넥션 고갈이 될 수 있는 문제로 인해 안티 패턴이다. 특별한 이유가 없다면 비활성화해서 사용하자. (비활성화 할경우, 영속성 컨텍스트는 @Transactional 내부에서만 관리된다.)
Reference
'spring > summary' 카테고리의 다른 글
[Spring Batch] Job 실행 프로퍼티, JobParameter, Scope (0) | 2024.08.26 |
---|---|
Spring Batch Architecture (0) | 2024.05.10 |
hibernate.query.in_clause_parameter_padding = true ? (1) | 2024.03.06 |
JPA 에 UUID version 7 적용하기 (0) | 2024.03.04 |
[Spring] Spring AOP 동작 알아보기 (0) | 2024.03.04 |
1. OSIV(Open Session In View) 란??

OSIV(Open Session In View)은 영속성 컨텍스트 관리 영역을 뷰(View)까지 확장하는 기능이다. Open Session In View라 불리는 이유는 JPA의 구현체인 Hibernate 에서 EntityManager의 구현체로 Session 을 사용한다. JPA에서는 OEIV(Open-EntityManager-In-View) 로 부르지만 통상적으로 Open-Session-In-View 로 부른다.
2. Spring-Boot 의 프로퍼티 기반 OSIV 설정 과정
Spring-Boot 는 프로퍼티를 기반으로 한 자동 설정을 지원한다. JpaBaseconfiguration 추상 클래스의 nested static class 인 JpaWebConfiguration 에에 OSIV 를 담당하는 인터셉터인 OpenEntityanagerInViewInterceptor 를 활성 여부를 관리한다. spring.jpa.open-in-view 프로퍼티를 통해 관리되며 별도의 설정이 없을 경우에는 OpenEntityanagerInViewInterceptor 는 빈으로 등록되고 인터셉터로 관리된다.
spring.jpa.open-in-view: false # default value = true
package org.springframework.boot.autoconfigure.orm.jpa;
// imports ...
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
private final DataSource dataSource;
// 여기에 JpaProperties
private final JpaProperties properties;
private final JtaTransactionManager jtaTransactionManager;
private ConfigurableListableBeanFactory beanFactory;
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
@ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
@ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true)
protected static class JpaWebConfiguration {
private static final Log logger = LogFactory.getLog(JpaWebConfiguration.class);
private final JpaProperties jpaProperties;
protected JpaWebConfiguration(JpaProperties jpaProperties) {
this.jpaProperties = jpaProperties;
}
// 실제 OSIV 를 담당하는 OpenEntityManagerInViewInterceptor 가 빈으로 선언되는 부분
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
if (this.jpaProperties.getOpenInView() == null) {
logger.warn("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");
}
return new OpenEntityManagerInViewInterceptor();
}
@Bean
public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
OpenEntityManagerInViewInterceptor interceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(interceptor);
}
};
}
}
}
3. OpenEntityManagerInViewInterceptor 의 기본 동작을 확인해보자.
실제 코드를 확인해보면 요청이 들어올 경우에는 엔티티 매니저를 생성하고, 요청이 완료되는 경우에는 엔티티 매니저를 종료하는 것을 확인할 수 있다. 이외에도 엔티티 매니저가 관리되기 위한 TransactionSynchronizationManager, AsyncRequestInterceptor 와 같이 트랜잭션 동기화, 비동기를 위한 작업을 위한 설정이 있는 것을 확인할 수 있다.
package org.springframework.orm.jpa.support;
// imports ...
public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {
public static final String PARTICIPATE_SUFFIX = ".PARTICIPATE";
@Override
public void preHandle(WebRequest request) throws DataAccessException {
String key = getParticipateAttributeName();
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
if (asyncManager.hasConcurrentResult() && applyEntityManagerBindingInterceptor(asyncManager, key)) {
return;
}
EntityManagerFactory emf = obtainEntityManagerFactory();
if (TransactionSynchronizationManager.hasResource(emf)) {
// 트랜잭션 동기화와 관련된 로직
} else {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
// EntityManager 가 생성된다.
try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);
AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
asyncManager.registerCallableInterceptor(key, interceptor);
asyncManager.registerDeferredResultInterceptor(key, interceptor);
} catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
}
}
}
@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
if (!decrementParticipateCount(request)) {
EntityManagerHolder emHolder = (EntityManagerHolder)
TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
logger.debug("Closing JPA EntityManager in OpenEntityManagerInViewInterceptor");
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); // 2. 엔티티 매니저를 닫는다.
}
}
}
4. 만약 Filter 에서 엔티티를 관리되어야 한다면?
Spring Security 와 같이 FilterChain 기반에서 엔티티를 초기화해야 하는 경우가 있다. 일반적으로 Open-Session-In-View 는 안티 패턴이라 말한다. 요청이 시작, 종료되는 시점까지 DB Connection 을 잡고 있어 실시간 서비스와 같이 트래픽에 민감한 서비스의 경우에 성능 저하 요소가 될 수 있기 때문이다. 하지만 사내의 시스템이 레거시 코드로 인해 필터 쪽에서 엔티티를 초기화해야 되서 불가피하게 사용해야 한다면 OpenEntityManagerInViewFilter 을 선언해서 사용해야 한다. OpenEntityManagerInViewFilter 는 스프링에서 지원해주기 때문에 해당 빈을 등록해서 사용하도록 하자.
package org.springframework.orm.jpa.support;
// 여러가지 import
public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
// 비즈니스 로직
}
// source : https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/
@Component
@Configuration
public class OpenEntityManagerConfig {
@Bean
public FilterRegistrationBean<OpenEntityManagerInViewFilter> openEntityManagerInViewFilter() {
FilterRegistrationBean<OpenEntityManagerInViewFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
filterFilterRegistrationBean.setOrder(Integer.MIN_VALUE); // 최우선 순위로 Filter 등록
return filterFilterRegistrationBean;
}
}
5. 내가 생각하는 open-session-in-view
흔히 open-session-in-view 를 안티 패턴이라 부른다. 이전에 언급했듯이 실시간 트래픽이 중요한 서비스의 경우, 요청 시점에 EntityManager 가 생성되면서 DB Connection Pool 의 Connection 을 붙잡고 있어 Connection Pool 이 고갈될 수 있는 문제가 발생할 수 있다.
open-session-in-view 를 활성화 할 경우, DB 작업이 모두 완료되고 다른 외부 API 요청을 대기하는 시간까지 Connection 을 반환하지 않는다. 이와 같은 병목지점이 생기고 해당 로직이 많은 사용자가 사용한다면 ConnectionPool 의 Connection 을 모두 잡는다면 Connection 을 반환하기를 기다리는 문제가 발생할 수 있다. 이는 사용자에게 나쁜 사용 경험을 제공해주기 때문에 특별한 이유가 없다면 사용하지 않는 것이 적합한 것 같다.
hikariCP 에서 제공하는 프로퍼티들의 기본 값 정도는 참고로 알고 있자.
# 주의 : DB 단위 - s, Spring Boot 단위 - ms
# Spring Boot 설정을 Database 설정보다 적게 설정하는 것을 권장
spring.datasource.hikari.minimumIdle=30000 #30초
spring.datasource.hikari.connectionTimeout=30000 #30초
spring.datasource.hikari.maximumPoolSize=10
spring.datasource.hikari.idleTimeout=600000 # 10분
spring.datasource.hikari.maxLifeTime=1800000 # 30분
spring.datasource.hikari.autoCommit=true
비록 thymeleaf, jsp 와 같은 템플릿 엔진을 어드민 페이지로 사용하는 경우에 편의성을 가져다w주는 장점이 있지만 편리함으로 인해 템플릿 엔진에 의존적인 코드가 발생하고 vue.js 나 react 로 전환할 때의 공수가 늘어날 수 있다. 만약 기존 thymeleaf, jsp 와 같은 템플릿 엔진과 open-session-in-view 활성화해서 개발하고 있다면 최대한 엔티티 초기화를 템플릿 엔진에서 사용하지 않도록 하고, 엔티티 대신에 DTO 를 활용하는 방향으로 코드를 작성하는 방식으로 코드 작업을 하는 것이 낮은 의존성을 기반해서 유지보수하기 용이하지 않을까 싶다.
- 신규 프로젝트에서 사용한다면 open-session-in-view 비활성화 하기
- 기존 프로젝트에서 사용한다면 open-session-in-view 를 비활성화하는 방향으로 작업하기 (e.g. DTO)
6. summary
- OSIV 는 영속성 컨텍스트의 라이프 사이클을 요청, 응답이 끝날 때까지 활성화하는 전략을 말한다.
- OSIV 는 OpenEntityManagerInViewInterceptor, OpenEntityManagerInViewFilter 를 통해 영속화 범위가 확장된다.
- OSIV 는 다른 로직의 처리가 길어지는만큼 Connection 을 붙잡고 있어 커넥션 고갈이 될 수 있는 문제로 인해 안티 패턴이다. 특별한 이유가 없다면 비활성화해서 사용하자. (비활성화 할경우, 영속성 컨텍스트는 @Transactional 내부에서만 관리된다.)
Reference
'spring > summary' 카테고리의 다른 글
[Spring Batch] Job 실행 프로퍼티, JobParameter, Scope (0) | 2024.08.26 |
---|---|
Spring Batch Architecture (0) | 2024.05.10 |
hibernate.query.in_clause_parameter_padding = true ? (1) | 2024.03.06 |
JPA 에 UUID version 7 적용하기 (0) | 2024.03.04 |
[Spring] Spring AOP 동작 알아보기 (0) | 2024.03.04 |