- [트러블 슈팅] 회원 상세 조회 문제 발생(2025.01.06)2025년 01월 07일 17시 17분 04초에 업로드 된 글입니다.작성자: do_hyuk
회원 상세 조회 api 호출 시 다음과 같은 에러 발생
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.st.eighteen_be.user.domain.UserInfo.userQuestions: could not initialize proxy - no Session
에러 내용을 살펴보면 org.hibernate.LazyInitializationException은 Hibernate에서 발생하는 이벤트이다.
이 오류는 영속성 컨텍스트가 종료되어 버려서, 지연 로딩을 할 수 없어서 발생하는 오류이다.
JPA에서 지연로딩을 하려면 항상 영속성 컨텍스트가 있어야 한다.
보통 트랜잭션 밖에서 조회하면 이런 문제가 발생한다.
디버깅에서 확인된 지연 초기화 에러 현재 로직 흐름을 간단히 표현하자면 다음과 같다.
UserController -> UserDtoService -> UserService -> UserRepository
UserDtoService에서 User 객체를 UserService를 통해 불러온다.//UserDtoService public UserDetailsResponseDto findByUniqueId(String uniqueId) { UserInfo userInfo = userService.findByUniqueId(uniqueId); int likeCount = likeService.countLikes(userInfo.getId()); return getUserDetailsResponseDto(userInfo, likeCount); } private UserDetailsResponseDto getUserDetailsResponseDto(UserInfo userInfo, int likeCount) { List<String> images = getImages(userInfo); List<UserQuestion> questions = userInfo.getUserQuestions(); List<UserQuestionResponseDto> responseDtoList = questions.stream() .map(UserQuestionResponseDto::new) .toList(); return new UserDetailsResponseDto(userInfo, likeCount, images, responseDtoList); }
이때 UserService에만 트랜잭션을 걸어놨기 때문에 UserService.findByUniqueId() 호출이 끝나고 Session이 종료되어버린 것이다.
그렇기 때문에 getUserDetailsResponseDto() 메서드에서 사용할 userInfo 객체에는 userQuestions()가 지연 로딩되지 않아 해당 문제가 발생한 것이다.
1. LazyInitializationException 해결하기 (Anti-Pattern)
LazyInitializationException 예외 해결을 위한 여러가지 방법이 있지만 다음의 방법들은 권장되지 않는 해결 방법이다.
1.1 OSIV(Open Session In View) 활성화
Spring의 open-in-view 프로퍼티를 활성화 해준다.
기본값은 true 이며 활성화 되었을 땐 응답이 완료되거나 뷰가 렌더링 될 때까지 영속성 컨텍스트를 유지한다.
@Transactional 어노테이션이 사용된 메서드가 종료되어도 컨트롤러 리턴 시점까지 세션을 유지해주지만 이 방법은 데이터베이스 커넥션을 계속 이어서 사용하고 세션의 수명이 길어짐에 따라 성능과 확장성 측면에서 좋지 않기 때문에 권장되지 않는다.
spring.jpa.open-in-view: true
1.2 hibernate.enable_lazy_load_no_trans 활성화
Spring의 hibernate.enable_lazy_load_no_trans 프로퍼티를 활성화 해준다.
이 방법은 세션이 종료된 이후에도 다른 세션을 사용하여 데이터를 조회한다. 여러 개의 지연 로딩이 있는 경우에 대해 각각 새로운 데이터베이스 커넥션을 획득하여 조회하기 때문에 성능 측면에서 좋지 않고 커넥션 풀을 고갈시키는 장애를 유발할 수 있기 때문에 권장되지 않는다.
spring.jpa.properties.hibernate.enable_lazy_load_no_trans: true
1.3 지연 로딩을 즉시 로딩으로 바꾸기
연관관계가 설정된 엔티티에 FetchType.EAGER를 사용하여 즉시 로딩으로 엔티티를 조회하면 연관된 객체가 모두 영속성 컨텍스트에 생성되어 해결이 가능하다. 하지만 연관된 엔티티가 컬렉션과 같은 경우엔 성능에 문제가 발생할 수 있기 때문에 권장되지 않는다.
2. LazyInitializationException 해결하기 (권장 방법)
다음의 방법들을 사용하여 LazyInitializationException 예외를 해결하는 것이 권장된다.
2.1 @Transactional 사용하기
서비스 계층에서 @Transactional 어노테이션을 읽기 전용(readOnly = true)으로 사용하여 해결하는 방법이다.
트랜잭션을 읽기 전용으로 사용하면 Dirty Checking을 하지 않아 성능을 향상시키고 데이터의 의도하지 않은 변경을 방지해준다. LazyInitializationException 예외는 JPA의 영속성 컨텍스트가 종료된 후에 연관관계가 설정된 엔티티를 조회하려고 할 때 발생하기 때문에 세션이 유지되도록 트랜잭션을 설정해준다. 서비스 계층에서 트랜잭션을 시작하면 Repository까지 해당 트랜잭션이 전파되어 사용된다. 따라서 지연 로딩 시점까지 세션을 유지하여 사용할 수 있다.
@Transactional(readOnly = true) public UserDetailsResponseDto findByUniqueId(String uniqueId) { UserInfo userInfo = userService.findByUniqueId(uniqueId); int likeCount = likeService.countLikes(userInfo.getId()); return getUserDetailsResponseDto(userInfo, likeCount); } @Transactional(readOnly = true) protected UserDetailsResponseDto getUserDetailsResponseDto(UserInfo userInfo, int likeCount) { List<String> images = getImages(userInfo); List<UserQuestion> questions = userInfo.getUserQuestions(); List<UserQuestionResponseDto> responseDtoList = questions.stream() .map(UserQuestionResponseDto::new) .toList(); return new UserDetailsResponseDto(userInfo, likeCount, images, responseDtoList); }
'포트폴리오 > Eighteen' 카테고리의 다른 글
뱃지 시스템 적용 (0) 2025.01.15 [트러블슈팅] 랜덤 Pagination 너무 쉽게 봤다. (0) 2025.01.09 [트러블 슈팅] redis 관련 문제 발생(2025.01.03) (0) 2025.01.03 [트러블 슈팅] 요청 값 Dto에 어떻게 매핑되는가 (0) 2024.11.15 [트러블 슈팅] JwtFilter와 Security Config의 동작 순서 (0) 2024.10.16 댓글