문제 상황
@GetMapping("/test")
fun getTest(
@AuthenticationPrincipal user: User?
): StudentInfoResponse {
return (user as Student).toStudentInfoResponse()
}
@AuthenticationPrincipal 어노테이션을 이용하여, 로그인시 userDetailsService에서 가져온 userDetails 정보를 getMyInfo 함수에서 사용하고자 했습니다.
@Where(clause = "user_is_delete = false")
@SQLDelete(sql = "UPDATE `user` SET user_is_delete = true where id = ?")
@Table(name = "user")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "user_type")
@Entity
abstract class User(
name: String,
email: String,
password: String,
role: Role
): BaseTimeEntity(), UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
var name: String = name
protected set
var email: String = email
protected set
private var password: String = password
override fun getPassword(): String {
return this.password
}
@ElementCollection
var roleList: MutableList<Role> = ArrayList()
protected set
@Column(name = "user_is_delete")
var isDeleted: Boolean = false
protected set
init {
this.roleList.add(role)
}
//나머지 부분 생략
}
failed to lazily initialize a collection of role: {~}.roleList, could not initialize proxy - no Session
위와 같이 user 프록시에서 @ElementCollection으로 지정된 roleList를 Lazy로딩으로 참조하지 못하여 발생
문제해결 1
각종 페이지들을 참조한 결과, 해당 에러가 UserDetailsService가 작동하는 SecurityFilter의 Transaction이 전이되지 않았기에 @ElementColllection 로 선언된 roleList의 정보를 참조하지 못하였기에 발생한 문제라고 생각되어 user Entity를 고쳤습니다.
@ElementCollection
var roleList: MutableList<Role> = ArrayList()
protected set
@ElementCollection(fetch = FetchType.EAGER)
var roleList: MutableList<Role> = ArrayList()
protected set
그러나 이번에는 student를 toStudentInfoResponse를 통해 dto로 바꿔주던 과정에서 fielidTrainingList를 지연로딩하지 못하여 또 다시 에러가 발생.
이대로 모든 list를 eager로딩할 순 없었기에 다른 해결 방법을 찾아보았습니다.
문제해결 2
근본적으로 해당 문제를 해결할 수 있는 방법은 UserDetailsService를 통해 받아오는 userDetails를 영속성 context에 넣어주어 해결하는 것
1. Open Session In View
open Session In View란 이름에서 알 수 있듯이 hibernate session을 view에서도 open한다는걸 의미합니다. 하지만 해당 값의 default는 true이기에 이미 controller에서 OSIV가 작동하고 있음을 알 수 있습니다.
→ 영속성 컨텍스트는 Controller에서도 작동하고 있었음.
2. SecurityFilter에서 주입받는 UserDetails는 OSIV 밖에서 작동
OSIV의 기본값을 통해 알 수 있듯이, SecurityFilter는 OSIV 밖에서 작동하고 있음을 알 수 있음.
내부를 뜯어보면,
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
...
@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;
}
@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);
}
};
}
}
}
public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {
...
@Override
public void preHandle(WebRequest request) throws DataAccessException {
...
}
@Override
public void postHandle(WebRequest request, @Nullable ModelMap model) {
}
@Override
public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
...
}
...
}
위와 같이 interceptor로 구현되고 있음을 확인할 수 있습니다.(Open Session In View는 hibernate 용어로, jpa에선 OpenEntityManagerInView로 부름)
위 그림들을 통해 SecurityFilter가 작동하는 부분은 Interceptor 이전임을 확인할 수 있으며, 또한 filter를 제외한 대부분의 영역에서 EntityManager를 이용할 수 있음을 알 수 있습니다.
UserDetailsService에서 유저 정보를 불러오는 방법
UserDetailsService는 기본적으로 SecurityFilter 내부에서 작동하며, SecurityFilter는 다른 여러 Filter와 filterchain을 이루어 동작하기에 왜 UserDetailsService를 통해 불러 온 UserDetails의 영속성이 전이되지 않는지 알 수 있습니다.
해결 방법
OSIV가 Interceptor로 작동하고 있었기에 영속성이 전이되지 않았기에, Interceptor를 Filter로 교체하여 filterChain 최우선으로 두면 됩니다.
다행히도, OpenEntityManagerIn를 Filter로 동작시킬 수 있게끔 구현한 OpenEntityManagerInView 클래스가 존재하며, 이를 Bean으로 등록시키면 해결됩니다.
@Configuration
class OpenEntityManagerConfiguration {
@Bean
fun openEntityManagerInViewFilter(): FilterRegistrationBean<OpenEntityManagerInViewFilter> {
val filterFilterRegistrationBean = FilterRegistrationBean<OpenEntityManagerInViewFilter>()
filterFilterRegistrationBean.setFilter(OpenEntityManagerInViewFilter())
filterFilterRegistrationBean.order = Int.MIN_VALUE
return filterFilterRegistrationBean
}
}
'SpringBoot' 카테고리의 다른 글
OneToMany 매핑 시 mappedBy 옵션 사용하여야하는 이유 (0) | 2023.06.10 |
---|---|
Spring Author Auditing 오류 (0) | 2023.06.10 |
Spring Async 사용 시 Security Context 전파 오류 (0) | 2023.06.10 |
SpringBoot WAS 기본 이해 (0) | 2023.06.09 |
Spring Async 사용시 SecurityContext Thread 전파 오류 (0) | 2023.06.09 |