컨트롤러에서 로그인된 유저에 대해 영속화된 엔티티를 가져오려고 하였다.
검증처리도 해야할 뿐더러, 이를 비스니스 레이어로 넘겨주려고 하였다.
UserDetails 에서 User 도메인 엔티티를 넘겨주려고 하는데, 영속화된 User를 가져올 수 없었다.
왤까? 어떻게 해야 영속화된 User를 가져올 수 있을까??
Entity Lifecycle을 고려해 코드를 작성하자 2편
영속화를 담당하는 Open Session In View는 Interceptor를 통해 적용되기 때문이다.
Spring Security에서 Filter를 통해 User 정보를 가지고 오는데, Filter는 Interceptor보다 먼저 실행된다.
이로 인해, 영속성 컨텍스트가 종료된 상황에서의 User를 가져온다.
즉, Controller에서 @CurrnetUser를 통해 가지고 온 User객체는 준영속화된 상태이다.
해결방법은 간단한다.
아래 코드를 “복붙” 하면 된다.
@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;
}
}
여기까지가 간단한 설명이고
그렇다면 왜 그런지를 알아보자.
이를 알기 위해서는 Spring Security 동작과정을 간단하게 뜯어보아야 한다.
Entity Lifecycle을 고려해 코드를 작성하자 2편
Architecture :: Spring Security
위 그림과 같이, Spring 에서는 Container를 통해 요청을 전달하는 Servlet 이전에 Filter로서 Security 검증을 처리한다.
DelegatingFilterProxy에서 사용자의 요청을 가로채 Spring Security의 기능들이 수행되며 모든 요청에 대해 보안이 적용되게끔 한다.
우리가 기존에 구현한 CustomUserDetailsService도 Spring Security 인증과정에 필요한 UserDetailsService를 구현한 것이다.
이 Service를 통해 DB에 존재하는 User 정보와 사용자가 입력한 로그인 정보를 대조해 인증/인가를 진행한다.
그렇다면
이 과정이 과연 영속화 과정에 있을까?
정답은 아니다.
영속화는 OpenEntityManagerInViewInterceptor 를 통해 구현된다.
@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 {
...
@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
...
return new OpenEntityManagerInViewInterceptor();
}
@Bean
public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(OpenEntityManagerInViewInterceptor interceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addWebRequestInterceptor(interceptor);
}
};
}
}
하지만 Spring Security가 실행되는 단계인 Filter 는 Interceptor의 이전단계이다.
따라서 FilterChainProxy에서 CustomUserDetailsService는 Service 내에서 @Transactional이 적용되는 부분에서만 영속성 컨텍스트가 유지된다는 뜻이다.
즉 영속성 컨텍스트가 종료된 상황에서 Controller에서 UserDetails 를 통해 가지고 온 User객체는 준영속화된 상태이다.
따라서 다시 영속성 컨텍스트가 OSIV를 통해 Controller에 주입된다 하더라도 가져온 User는 이미 준영속화 된 User이다.
뭐, 틀린 말은 아니다.
그렇다고 맞는 말도 아니다.
여기서 OSIVFilter
라고 칭하는 OpenEntityManagerInViewFilter
는
OpenEntityManagerInViewInterceptor
를 Filter로 순서를 바꿔주기 위한
Spring Boot에서 지원해주는 클래스이다.
그러나 스프링부트의 기본설정은 OpenEntityManagerInViewFilter
가 아니다 ㅎㅎ
코드로 보는 OSIV (Open Session In View)
Spring-Boot 는 프로퍼티를 기반으로 한 자동 설정을 지원한다.
JpaBaseconfiguration 추상 클래스의 nested static class 인 JpaWebConfiguration
에
OSIV 를 담당하는 인터셉터인 OpenEntityanagerInViewInterceptor
를 활성 여부를 관리한다.
spring.jpa.open-in-view
프로퍼티를 통해 관리되며 별도의 설정이 없는 경우에는
OpenEntityanagerInViewInterceptor
는 빈으로 등록되고 인터셉터로 관리된다.
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);
}
};
}
}
spring.jpa.open-in-view: false # default value = true
OSIVFilter를 사용하게 되면 아래와 같이 동작된다.
추가로 OSIV 자체가 View 와 DB 시점에서 악영향을 끼칠 수 있는 가능성을 띄는지에 대해서 알아보면 좋을 것 같다.
대충 읽었는데 솔직히 View 에 대해서는 신경쓰지 않아서 넘어갔다.
Why is Hibernate Open Session in View considered a bad practice?