Search

LazyInitializationException 해결하기

태그
JPA
분류
Spring Boot

LazyInitializationException 의 발생

소셜로그인 코드를 작성하고 실행했더니 아래와 같은 오류가 나타났습니다. 다들 한번쯤은 겪는다는 LazyInitalizationException 이였는데요, 왜 이런 오류가 발생했던 걸까요?
2024-06-24T13:57:32.112+09:00 DEBUG 15460 --- [nio-8080-exec-3] o.s.web.client.RestTemplate : Reading to [java.util.Map<java.lang.String, java.lang.Object>] 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.s.CustomOAuth2UserService : ============================================================================ 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.s.CustomOAuth2UserService : getAttributes: {sub=107260565118160509484, name=김시은, given_name=시은, family_name=김, picture=https://lh3.googleusercontent.com/a/ACg8ocJS5We9Pzn4BvwlL4vnypALLCHvg3VrHNLkOFRL0SZ8XwX5gA=s96-c, email=wldsmtldsm65@gmail.com, email_verified=true} 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.s.CustomOAuth2UserService : ============================================================================ 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.e.OAuth2UserInfoFactory : ============================================================= 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.e.OAuth2UserInfoFactory : Google login Request sent 2024-06-24T13:57:32.113+09:00 INFO 15460 --- [nio-8080-exec-3] c.p.t.g.a.o.e.OAuth2UserInfoFactory : ============================================================= Hibernate: select u1_0.id,u1_0.birthday,u1_0.email,u1_0.is_withdrawal,u1_0.nickname,u1_0.password,u1_0.profile_id,u1_0.provider,u1_0.role,u1_0.sex,u1_0.signup_date,u1_0.user_tag from user u1_0 where u1_0.email=? and u1_0.provider=? 2024-06-24T13:57:32.135+09:00 DEBUG 15460 --- [nio-8080-exec-3] .s.ChangeSessionIdAuthenticationStrategy : Changed session id from 9390C8B22997E5E31616A5D9A48C061A 2024-06-24T13:57:32.135+09:00 DEBUG 15460 --- [nio-8080-exec-3] .s.o.c.w.OAuth2LoginAuthenticationFilter : Set SecurityContextHolder to OAuth2AuthenticationToken [Principal=com.planner.travel.global.auth.oauth.entity.CustomOAuth2User@2459db7b, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9390C8B22997E5E31616A5D9A48C061A], Granted Authorities=[ROLE_USER]] 2024-06-24T13:57:32.136+09:00 INFO 15460 --- [nio-8080-exec-3] a.o.h.OAuth2AuthenticationSuccessHandler : ============================================================================ 2024-06-24T13:57:32.136+09:00 INFO 15460 --- [nio-8080-exec-3] a.o.h.OAuth2AuthenticationSuccessHandler : Social login successful. Social type is google 2024-06-24T13:57:32.136+09:00 INFO 15460 --- [nio-8080-exec-3] a.o.h.OAuth2AuthenticationSuccessHandler : ============================================================================ 2024-06-24T13:57:32.139+09:00 ERROR 15460 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception org.hibernate.LazyInitializationException: could not initialize proxy [com.planner.travel.domain.profile.entity.Profile#2] - no Session at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102) ~[hibernate-core-6.4.4.Final.jar:6.4.4.Final] at com.planner.travel.domain.profile.entity.Profile$HibernateProxy$MzhvZwtd.getProfileImageUrl(Unknown Source) ~[main/:na] at com.planner.travel.global.auth.oauth.handler.OAuth2AuthenticationSuccessHandler.setRedirectUrl(OAuth2AuthenticationSuccessHandler.java:69) ~[main/:na] at com.planner.travel.global.auth.oauth.handler.OAuth2AuthenticationSuccessHandler.onAuthenticationSuccess(OAuth2AuthenticationSuccessHandler.java:58) ~[main/:na]
Plain Text
복사

profileImageUrl 호출 시 Exception 발생!

CustomOAuth2User 에서 User 엔티티는 정상적으로 불러 온다는 것을 확인했습니다. 하지만 User 와 관련된 Profile 의 필드 값에 접근할 때 LazyInitalizationException 이 발생하였습니다.
@Slf4j @Component @RequiredArgsConstructor public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private String PRE_FRONT_REDIRECT_URL = "http://localhost:5173"; private final ObjectMapper objectMapper; private final UserRepository userRepository; private final TokenGenerator tokenGenerator; private final CookieUtil cookieUtil; private final RedisUtil redisUtil; @Override @Transactional public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); String provider = customOAuth2User.getUser().getProvider(); // String email = getEmailByProvider(provider, customOAuth2User); if (response.isCommitted()) { log.info("============================================================================"); log.info("Social login response has been successfully sent."); log.info("============================================================================"); } log.info("============================================================================"); log.info("Social login successful. Social type is {}", provider); log.info("============================================================================"); String accessToken = tokenGenerator.generateToken(TokenType.ACCESS, String.valueOf(customOAuth2User.getUser().getId())); String refreshToken = tokenGenerator.generateToken(TokenType.REFRESH, String.valueOf(customOAuth2User.getUser().getId())); response.setHeader("Authorization", "Bearer " + accessToken); cookieUtil.setCookie("refreshToken", refreshToken, response); redisUtil.setData(String.valueOf(customOAuth2User.getUser().getId()), refreshToken); String frontendRedirectUrl = setRedirectUrl(customOAuth2User, accessToken); response.sendRedirect(frontendRedirectUrl); } private String setRedirectUrl (CustomOAuth2User customOAuth2User, String accessToken) { String encodedUserId = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getId()), StandardCharsets.UTF_8); String encodedNickname = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getNickname()), StandardCharsets.UTF_8); String encodedUserTag = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getUserTag()), StandardCharsets.UTF_8); String encodedBirthday = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getBirthday()), StandardCharsets.UTF_8); String encodedEmail = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getEmail()), StandardCharsets.UTF_8); String encodedProfileImgUrl = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getProfile().getProfileImageUrl()), StandardCharsets.UTF_8); String encodedIsBirthDay = URLEncoder.encode(String.valueOf(isBirthdayToday(customOAuth2User.getUser().getBirthday()))); String encodedSex = URLEncoder.encode(String.valueOf(customOAuth2User.getUser().getSex())); String frontendRedirectUrl = String.format( "%s/oauth/callback?token=%s&userId=%s&nickname=%s&userTag=%s&birthday=%s&email=%s&profileImgUrl=%s&isBirthday=%s&sex=%s", PRE_FRONT_REDIRECT_URL, "Bearer " + accessToken, encodedUserId, encodedNickname, encodedUserTag, encodedBirthday, encodedEmail, encodedProfileImgUrl, encodedIsBirthDay, encodedSex ); return frontendRedirectUrl; } private boolean isBirthdayToday(LocalDate birthday) { return birthday != null && birthday.getMonth() == LocalDate.now().getMonth() && birthday.getDayOfMonth() == LocalDate.now().getDayOfMonth(); } }
Java
복사

LazyInitalizationException 이 뭔데?

LazyInitializationException 은 Hibernate 에서 지연로딩된 엔티티를 세션이 종료된 후 접근하려고 할 때 발생하는 예외입니다. 성능 최적화를 위해 연관된 데이터를 실제로 필요할 때 까지 로딩하지 않는 방식이 지연 로딩인데, 이 때 Hibernate 는 연관 엔티티에 대한 참조만을 proxy 객체로 반환합니다. 이후 해당 프록시 객체의 필드에 접근하려고 할 때 데이터베이스에서 실제 데이터를 가져옵니다.하지만 이 때 세션이 이미 종료되었다면 Hibernate 는 프록시 객체를 초기화할 수 없기 때문에 예외가 발생하게 됩니다. 데이터베이스와의 연결이 끊겼기 때문이죠.

그래서 어떻게 해결 해야할까?

방법은 세가지가 있습니다. 첫번째는 User 와 Profile 의 관계에서 Fetch Type 을 Lazy 대신 EAGER 를 사용하는 것입니다. 하지만 이전에 갓영한님의 강의를 들었을 때 쓰지말라고 했으므로, 그냥 쓰지 않기로 했습니다.
다음으로는 Hibernate.initialize() 를 사용하는 것입니다. 이를 사용하면 트랜잭션이 열려 있는 동안 해당 프록시를 초기화할 수 있습니다. 하지만 이 방법은 사용해 본적이 없으므로 패스.
결론적으로 저는 마지막 방식을 사용했습니다. 이미 세션이 종료된 후라 Profile 을 초기화할 수 없는것이 문제이기 때문에 아래와 같이 User 엔티티를 다시 로드해주었습니다.
User user = userRepository.findById(customOAuth2User.getUser().getId()) .orElseThrow(EntityNotFoundException::new);
Java
복사
이렇게 다시 User 엔티티를 명시적으로 로딩 했습니다. 이렇게 하면 연관된 엔티티들도 함께 로드됩니다. 결론적으로 트랜잭션 범위 내에서 이 작업이 수행되기 때문에 문제였던 예외가 발생하지 않습니다. 이는 DB 를 한번 더 타긴 하지만 성능에 큰 영향을 주지 않기 때문에 이와 같이 해결했습니다. 하지만 더 나은 방법이 있을 수도 있기에 proxy, transaction, session 에 대해 더 공부해야겠습니다.

최종코드

@Slf4j @Component @RequiredArgsConstructor public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private String PRE_FRONT_REDIRECT_URL = "http://localhost:5173"; private final ObjectMapper objectMapper; private final UserRepository userRepository; private final TokenGenerator tokenGenerator; private final CookieUtil cookieUtil; private final RedisUtil redisUtil; @Override @Transactional public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal(); String provider = customOAuth2User.getUser().getProvider(); // String email = getEmailByProvider(provider, customOAuth2User); User user = userRepository.findById(customOAuth2User.getUser().getId()) .orElseThrow(EntityNotFoundException::new); if (response.isCommitted()) { log.info("============================================================================"); log.info("Social login response has been successfully sent."); log.info("============================================================================"); } log.info("============================================================================"); log.info("Social login successful. Social type is {}", provider); log.info("============================================================================"); String accessToken = tokenGenerator.generateToken(TokenType.ACCESS, String.valueOf(user.getId())); String refreshToken = tokenGenerator.generateToken(TokenType.REFRESH, String.valueOf(user.getId())); response.setHeader("Authorization", "Bearer " + accessToken); cookieUtil.setCookie("refreshToken", refreshToken, response); redisUtil.setData(String.valueOf(user.getId()), refreshToken); UserInfoResponse userInfoResponse = new UserInfoResponse( user.getId(), user.getNickname(), user.getUserTag(), user.getBirthday(), user.getEmail(), user.getProfile().getProfileImageUrl(), isBirthdayToday(user.getBirthday()), user.getSex() ); String frontendRedirectUrl = setRedirectUrl(userInfoResponse, accessToken); response.sendRedirect(frontendRedirectUrl); } public String setRedirectUrl (UserInfoResponse userInfoResponse, String accessToken) { String encodedUserId = URLEncoder.encode(String.valueOf(userInfoResponse.userId()), StandardCharsets.UTF_8); String encodedNickname = URLEncoder.encode(String.valueOf(userInfoResponse.nickname()), StandardCharsets.UTF_8); String encodedUserTag = URLEncoder.encode(String.valueOf(userInfoResponse.userTag()), StandardCharsets.UTF_8); String encodedBirthday = URLEncoder.encode(String.valueOf(userInfoResponse.birthday()), StandardCharsets.UTF_8); String encodedEmail = URLEncoder.encode(String.valueOf(userInfoResponse.email()), StandardCharsets.UTF_8); String encodedProfileImgUrl = URLEncoder.encode(String.valueOf(userInfoResponse.profileImgUrl()), StandardCharsets.UTF_8); String encodedIsBirthDay = URLEncoder.encode(String.valueOf(userInfoResponse.isBirthday())); String encodedSex = URLEncoder.encode(String.valueOf(userInfoResponse.sex())); String frontendRedirectUrl = String.format( "%s/oauth/callback?token=%s&userId=%s&nickname=%s&userTag=%s&birthday=%s&email=%s&profileImgUrl=%s&isBirthday=%s&sex=%s", PRE_FRONT_REDIRECT_URL, "Bearer " + accessToken, encodedUserId, encodedNickname, encodedUserTag, encodedBirthday, encodedEmail, encodedProfileImgUrl, encodedIsBirthDay, encodedSex ); return frontendRedirectUrl; } private boolean isBirthdayToday(LocalDate birthday) { return birthday != null && birthday.getMonth() == LocalDate.now().getMonth() && birthday.getDayOfMonth() == LocalDate.now().getDayOfMonth(); } }
Java
복사