오늘 프로젝트를 하다가 LazyInitializationException 예외를 맞닥뜨렸다. 전부터 겪었던 문제였는데 당시에는 JPA를 잘 몰랐기 때문에 그냥 FetchType.EAGER를 모두 적용해서 무식하게 해결하긴 했는데 이제는 어느정도 배웠으니 해당 문제를 해결해 보자고 생각했다.
문제의 원인
@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {
private final OAuth2Response oAuth2Response;
private final Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Set.of(new SimpleGrantedAuthority(role.getName().name())); // 여기서 문제
}
// 나머지 코드 생략
}
ㄴCustomOAuth2User
@Transactional
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response response = null;
// 코드 생략
Optional<User> foundUser = userRepository.findByProviderId(response.getProviderId());
User user;
// 코드 생략
return new CustomOAuth2User(response, user.getUserRole().getRole()); // 여기서 문제
}
ㄴ CustomOAuth2UserService.loadUser()
문제는 어디서 발생했을까... 발생한 로그를 따라가보니 return문의 Role을 넘겨준 부분이 문제였다.
CustomOAuth2User에서는 Role을 받아서 이를 권한 설정에 사용하는데 여기서 사용할 때 LazyInitializationException이 발생한 것이다.
이제 원인이 되는 곳을 알았으니 예외가 발생한 이유를 알아야 한다. 이유는 테이블 구조를 봐야 알기가 쉽다.

일단 FetchType을 모두 다시 LAZY로 바꿨기 때문에 이제 모든 엔티티는 사용하는 시점에 불러온다.
나는 지금 users, user_roles, roles를 모두 사용할 것이기 때문에 모든 엔티티가 사용된다. 그렇기 때문에 EAGER로 설정하거나 JPQL 조인을 두 번 사용하면 해결될거 같았다. 원인이 영속성 컨텍스트의 관리가 끝난 이후에 roles에 접근하려고 했기 때문이라고 생각했기 때문이다. 왜냐하면 CustomOAuth2User를 생성할 때 getRole()로 roles의 엔티티를 가져온다고 생각했는데 user_roles 테이블은 외래키의 주인인 테이블로 users와 roles의 식별자 정보를 모두 가지고 있다. 그렇기 때문에 직접적으로 UserRole 엔티티에서 Role 엔티티의 내용을 바로 사용하지 않는 이상은 해당 엔티티를 가져오지 않은 상태이다. 이 상태에서 영속성 컨텍스트의 관리가 종료되었기 때문에 이후에 Role 엔티티의 내용에 접근하려고 하지만 해당 엔티티의 내용을 가져오지 않은 상태여서 LazyInitializationException 예외가 발생한 것이다.
앞서 언급했던 EAGER는 전역 설정이기 때문에 항상 모든 엔티티를 가져오게 되어서 비효율적이고,
JPQL로 조인을 두 번 사용하는 것은 다른 곳에서 해당 메서드를 사용할 때 불필요하게 다른 엔티티까지 한번에 불러오니 조금 사용하기가 꺼려졌다.
그래서 다른 방법을 찾아보기로 했다.
문제 해결
일단 기본적으로 모든 엔티티의 n대1에 해당하는 부분들은 기본값이 FetchType.EAGER이기 때문에 전부 LAZY로 설정해놓았다. 필요한 부분만 최적화를 위해 JPQL로 조인해서 한꺼번에 가져오기로 하고 해결한 방법은 다음과 같다.
RoleEnum role = user.getUserRole().getRole().getName();
return new CustomOAuth2User(response, role);
CustomOAuth2UserService.loadUser()에서 문제가 되는 부분이 Role 엔티티 자체를 사용하지 않고 넘겼기 때문이므로 단순히 Role의 name을 미리 가져와서 해당 값을 넘기도록 변경했다. 물론 JPQL로 페치 조인을 두 번 하면 코드를 변경하지 않아도 잘 작동하지만 매번 필요할 때마다 조인을 해주어야 하기 때문에 번거로워서 간단한 방법을 택했다.
이렇게 해서 이제 문제가 해결되었다!
'공부 > JPA' 카테고리의 다른 글
| [JPA] 엔티티 그래프(EntityGraph) 사용하기 (0) | 2024.03.11 |
|---|---|
| [JPA] 스프링 OSIV (Open Session In View) 사용하기 (0) | 2024.03.02 |
| [JPA] OneToOne에서의 N + 1 문제 (0) | 2024.02.27 |
| [JPA] 프록시 (Proxy) (0) | 2024.02.24 |
| [JPA] 다대다 연관 관계에서 복합키 사용하기 (0) | 2024.02.21 |