UserDetailsService를 통해 받아온 UserDetails를 활용한 LazyLoading 구현 (OpenEntityManagerInView)

xlwdn·2022년 10월 20일
0
post-thumbnail
post-custom-banner

문제 상황


@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로 부름)

Untitled

Untitled

위 그림들을 통해 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
    }
}
post-custom-banner

0개의 댓글