Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

JH 개발 블로그

@AuthenticationPrincipal 사용시 LazyInitializationException 본문

JAVA

@AuthenticationPrincipal 사용시 LazyInitializationException

쿠우우훈 2022. 11. 19. 06:07

프로젝트를 진행하면서 @AuthenticationPrincipal로 받은 userPrincipal에서 getMember()를 통해 현재 로그인된 사용자 정보를 가져온다.

 

문제 발생은 userPrincipal에서 가져온 Member에서 연관관계에 있는 객체 조회( 예를 들어 게시글 )를 하기 위해 lazy 로딩을 하려고 할 때 발생했다. proxy ~ 'no Session'  LazyInitializationException을 내뱉으며 조회가 안됐다.

 

그 이유를 찾아보니 아래와 같았다.

 

FetchType이 Lazy일땐 먼저 프록시(빈 객체) 객체를 불러온다.

필요할 때 이 객체를 조회하면 DB에서 객체를 조회하는 쿼리가 발생하면서, 프록시 객체가 DB에 저장된 객체로 변한다.

이러한 lazy 로딩은 영속화 되어 있는 상태에서만 가능하다. 컨트롤러, 서비스, 레포지토리단까지 OSIV가 기본으로 실행되고 있으므로 OSIV가 적용되는 범위 안에서는 Lazy 로딩이 오류가 나지 않는다. 하지만 filter는 OSIV 범위 밖이기 때문에 영속화가 되지 않은 객체를 가져온다.

 

기본적으로 Spring Security에서 인증을 하기 위해서는 먼저 필터를 거치는데, 이는 컨트롤러보다 앞단에 존재하고, OSIV의 적용범위가 아니다. 따라서 컨트롤러에서 @AuthenticationPrincipal 등을 이용해서 가져오는 사용자 정보는 필터단에서 가져오는 영속화 되지 않은 상태이므로, lazy로 설정되어 있는 연관객체를 조회하면 LazyInitializationException이 발생하는 것이다. 

 

 

 

문제 해결 방법을 하나씩 살펴보자.


1. 불러올 객체의 패치타입을 FetchType.EAGER로 변경


이 방법은 매우 간단하지만, 하지만 실무에선 N+1 문제로, 대부분 LAZY를 사용한다고 한다. 실무에선 복잡한 쿼리가 많아서 직접 쿼리를 짜는 경우가 많은데 JPQL 로 모든 MEMBER를 불러올때 TEAM이 EAGER로 되어있으면 멤버 한명을 불러올때마다 팀을 조회하는 쿼리가 나간다. 따라서 MEMBER가 N명이면, 쿼리는 1개 날렸는데 결과적으로 쿼리가 N개가 더 추가돼서 총 N+1 나가는게 N+1문제다. 이렇듯 즉시로딩은 불필요한 쿼리가 발생할 수 있기 때문에 사용을 지양한다.N+1 문제는 JPQL fetch join이나, 엔티티 그래프 기능으로 해결할 수 있다고 한다.
나중에 추가하겠다.

@OneToMany: LAZY
@ManyToOne: EAGER
@ManyToMany: LAZY
@OneToOne: EAGER

엔티티를 조회시 엔티티 조회 쿼리 + 연관관계가 EAGER로 설정된 각각의 엔티티들을 조회하는 쿼리까지 나간다. 연관관계가 LAZY로 설정된 엔티티들은 조회 X.


2. @AuthenticationPrincipal로 불러온 Member 객체에서 id를 get 해서 find 메소드로 repository에서 다시 member를 찾는다(영속성 컨텍스트 활성화) 그 후 getposts() 한다 => 비효율적으로 느껴진다. 멤버에서 다시 멤버를 찾는다..?

3. 영속성 컨텍스트가 존재할때 미리 필요한 entity(post)를 활성화 시킨다.
이방법은 위에서 설명한 eager랑 비슷한 결인데, 스프링 시큐리티에서 적용하는 방법도 있다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException("Can not find email.");
        }
        user.getPosts().size(); // 여기서 미리 활성화 시켜줌
        return new UserPrincipal(user);
    }
}


이 방법은 항상 멤버가 조회될때 같이 post가 조회된다는 단점이 있다. 따라서 탈락이라고 생각한다. 그리고 필터단은 OSIV 범위가 아니기 때문에 findByEmail 후 끝나기버리기 때문에 @Transactional 어노테이션을 같이 붙여야 한다.


4. OSIV를 필터단까지 적용시킨다.
OSIV default값은 true다. 따라서 컨트롤러까지 영속성 컨텍스트가 적용이 된다. 하지만 
@AuthenticationPrincipal에서 얻어지는 UserDetails는 Spring Security filter단에서 적용된다. 필터는 컨트롤러보다 더 앞단에 있으므로 OSIV가 필터에도 적용되도록 변경하는 것이다. 이러면 @AuthenticationPrincipal를 사용하였을 때 UserDetails도 영속된 상태로 불러진다.

@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.  jpa repository에서 

List<Post> findByMember(Member member);
List<Post> findByMember_MemberId(Long memberId);


로 찾는 방법이다. 
findBy(외래키객체) 형식 또는
findBy(외래키객체)_(외래키객체)(외래키필드) 형식으로 찾을 수 있다.

 

 

1번 탈락
2번은 멤버를 조회하는 쿼리 + 포스트 조회 쿼리 총 두개가 발생한다.

3번 탈락
4번의 단점은 잘 모르겠는데 필터단까지 영속성 컨텍스트가 적용되므로 어떠한 낭비가 생기지 않을까?
5번은 포스트 조회 쿼리만 발생한다.

 

일단 나는 4번으로 선택하였다.

 

 

 

 

 

 


주제를 벗어나서, @ManyToOne에서 지연로딩의 쿼리는 어떻게 되는걸까?
예를들어 Post 클래스에서 Member는 N:1의 관계다. 그럼 Default Fetch Type은 EAGER다.
따라서 Post를 조회할 시 Post 테이블에 Member가 left outer join 되면서 Member까지 조회한다. 만약 LAZY로 바꾼다면 단순히 Post만 조회된다. 따라서 Member까지 조회할 필요가 없을 시 Fetch Type을 LAZY로 바꾸는게 좋다. (웬만하면 다 LAZY로 하자)

public class Post {

	...
    
    ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "memberId")
    private Member member;
    }



참고 사이트
http://jaynewho.com/post/39
https://zzang9ha.tistory.com/347
https://tecoble.techcourse.co.kr/post/2020-08-31-entity-lifecycle-1/
https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/

Comments