📌 오류 발생 원인
Member member = memberRepository.findById(Long.valueOf(memberId))
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 계정입니다."));
Authentication authentication = jwtProvider.getAuthentication(member);
먼저, memberRepository에서 findById를 통해 Member(회원) 엔티티를 조회해온다.
public Authentication getAuthentication(Member member) {
MemberContext memberContext = new MemberContext(member);
return new UsernamePasswordAuthenticationToken(memberContext, null,
memberContext.getAuthorities());
}
jwtProvider.getAuthentication에서는 인증된 회원 정보를 관리하고자 MemberContext를 생성하고, 권한을 함께담아 토큰을 생성한다.
public MemberContext(Member member) {
super(member.getUsername(), member.getPassword(), getAuthorities(member.getRoleSet()));
id = member.getId();
username = member.getUsername();
email = member.getEmail();
createdDate = member.getCreatedDate();
updatedDate = member.getUpdatedDate();
roleSet = member.getRoleSet();
}
MemberContext의 생성자를 보면, member.getRoleSet( )에서 문제가 발생하게 된다.
public class Member extends BaseEntity {
...
/*
@ElementCollection : 컬렉션의 각 요소를 저장할 수 있다. 부모 Entity와 독립적으로 사용 X
@CollectionTable : @ElementCollection과 함께 사용될 때, 생성될 테이블의 이름 지정
*/
@ElementCollection(targetClass = MemberRole.class)
@CollectionTable(name = "member_roles",
joinColumns = @JoinColumn(name = "member_id"))
@Enumerated(EnumType.STRING)
private Set<MemberRole> roleSet;
}
Member에서의 roleSet은, @ElementCollection으로 설정되어 있으며, 기본적으로 Lazy loading이 default 값이다.
따라서, member.getRoleSet( )을 하게 되면, lazy loading이 동작하게 되면서, 조회 sql이 발생하게 된다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.jwt_restapi.member.entity.Member.roleSet: could not initialize proxy - no Session
그런데, 예상했던 대로 lazy loading이 발생하지 않고, 왜 이러한 오류가 발생하게 되는 것일까?
그 이유를 알기 위해서 JPA 영속성 컨텍스트에 대해 알아야 한다.
영속성 컨텍스트는 엔티티를 보관해두었다가 member.getRoleSet( )과 같이 필요에 의해 lazy loading이 이루어지게 한다.
보통은 Repository에서 member를 조회해오고, 추후에 member.getRoleSet( )을 해도, 영속성 컨텍스트에 엔티티가 저장되어 있기 때문에, Lazy Loading이 문제 없이 일어난다.
그러나, 나의 경우에는 Filter에서 동작하길 바라고 있었고 엔티티의 상태를 보면, 영속성 컨텍스트에 저장되었다가 분리된 상태 즉, 준영속 상태로 지연로딩을 사용하지 못한다.
따라서, lazyinitializationexception 가 발생하게 된 것이다.
📌 오류 해결 과정
lazyinitializationexception를 해결하는 방법에는 여러 가지가 있다.
그 중 나는 두 가지의 방법만 적용해볼 수 있었는데, 하나는 EAGER 전략이고, 또 하나는 fetch join 방식이다.
1. Lazy에서 Eager로
@ElementCollection(targetClass = MemberRole.class, fetch = FetchType.EAGER)
@CollectionTable(name = "member_roles",
joinColumns = @JoinColumn(name = "member_id"))
@Enumerated(EnumType.STRING)
private Set<MemberRole> roleSet;
단순하게, default 값으로 LAZY였던 것을 EAGER 방식으로 적용해주면 된다.
이렇게 적용해주면, left join을 통해 조회해오는 것을 확인해볼 수 있다.
Fetch 전략을 바꿔주는 것만으로도 바로 오류가 해결되지만, 생각해야 하는 부분이 있다.
Member를 조회해올 때마다, 항시 roleSet 조회가 같이 이루어진다는 것이다.
따라서, 불필요한 정보를 항상 조회해오게 되는 것은 아닌지 충분히 고려하고 적용해주어야 한다.
2. fetch Join
불필요한 조회는 줄이고 싶어서 필요할 때만 join해오는 것을 선택하게 되었다.
@Query("select m from Member m join fetch m.roleSet where m.id = :memberId")
Optional<Member> findMemberById(@Param("memberId") Long id);
실행해보면 하나의 조회문으로 roleSet까지 조회해오는 것을 확인해볼 수 있다.
3. @EntityGraph
@EntityGraph(attributePaths = {"roleSet"})
@Query("select m from Member m where m.id = :memberId")
Optional<Member> findById(@Param("memberId") Long id);
해당 어노테이션을 사용하면, 원하는 속성을 조인해온다.
EAGER과 마찬가지로 left join이 이루어진다.
left join은 inner join에 비해 성능 문제가 발생할 수 있으므로, 무분별한 left join은 성능을 좋지 못하게 한다.
카테시안 곱이 발생하므로 distinct 키워드를 사용하여 중복을 없애주는 것이 좋다. 또는 List 타입이 아닌 Set 타입을 사용하여 중복을 제거하는 것을 권장한다.
fetch join과 @EntityGraph는 JPA N + 1 문제의 해결 방식이기도 하며, 아래에 정리되어 있다.
그 외에도 필터를 빈으로 등록, @Transactional 사용, AOP로 트랜잭션 관리 등 여러 방법이 있겠지만, 적용해보지는 못했다.