User entity와 ProfilImg entity가 다음과 같이 ManyToOne 관계를 가지고 있다.
User
public class User extends Timestamped {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
...
@ManyToOne(fetch = FetchType.LAZY)
private ProfileImg profileImg;
ProfileImg
public class ProfileImg {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
...
코드를 보면 User 객체에서 profileImg에 대한 패치방식을 즉시로딩에서 지연로딩으로 설정을 해주었다.
엔티티 매니저를 통해 엔티티를 조회하면 연관관계에 매핑되어 있는 엔티티를 실제 사용할 때 조회
그렇게 때문에 위와 같은 단방향 ManyToOne 관계일 경우, User엔티티를 조회하게되면 ProfileImg엔티티는 DB에서 조회하지 않는다. 그렇다고 인스턴스가 null로 되어 있지는 않고, proxy 객체로 생성되어 있는데, proxy 객체는 실제 객체에 대한 참조값을 보관해 놓는다. 그리고 로직상에서 getter에 의해 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 되고 이때 추가적으로 쿼리문이 발생하게 된다.
즉시 로딩을 하게되면 user객체가 불릴때 언제나 profileImg는 같이 불리기 때문에 쿼리가 무조건 두번씩 발생한다. user 객체가 불릴때 언제나 profileImg가 필요한것이 아니고 쿼리가 항상 두번씩 발생한다는점은 트래픽이 증가할수록 성능저하가 심해질것이라고 판단하여 지연로딩으로 설정하였다.
즉시로딩에서 지연로딩으로 패치전략만 바꾸었을뿐인데 여러 군데에서 LazyInitializationException이 발생하였다.
발생 위치
마이페이지 수정 기능
댓글 작성 기능
myPageService 에러 발생 위치
/* 내가 작성한 생드백 리스트 */
public MyPageResponseDto getMyPostList(int pageNo, int sizeNo,
UserDetailsImpl userDetails) {
User user = userDetails.getUser();
...
/* response dto 만들기 */
MyPageResponseDto responseDto = MyPageResponseDto.builder()
.userId(user.getId())
.nickname(user.getNickname())
.profileImgUrl(user.getProfileImg().getProfileImgUrl()) <-- 여기
.level(user.getLevel())
.mbti(user.getMbti())
.myPostList(postDtoList)
.build();
return responseDto;
}
postComment 에러 발생 위치
@Transactional
public PostCommentDto postComment(long postId, String content,
UserDetailsImpl userDetails) {
...
User user = userDetails.getUser();
Comment comment = Comment.builder()
.comment(content)
.likedByWriter(false)
.user(user)
.post(post)
.build();
comment = commentRepository.save(comment);
...
String profileImgUrl = user.getProfileImg().getProfileImgUrl(); <-- 여기
return new PostCommentDto(
userDetails.getUser().getId(),
userDetails.getUser().getNickname(),
comment.getId(),
content,
TimeConversion.timeConversion(comment.getCreatedAt()),
userDetails.getUser().getTotalCount(),
0L,
userDetails.getUser().getMbti(),
false,
profileImgUrl,
userDetails.getUser().getLevel()
);
}
LazytializationException의 발생 원인은 다음과 같다.
JPA에서 관리하는 세션이 종료 된 후(정확하게는 persistence context가 종료 된 후) 관계가 설정된 엔티티를 참조하려고 할 때 발생
솔직히 저 말은 JPA에 대한 공부가 부족하여 정확히 이해가 가진 않는다. 디버깅을 위해 여러 자료들을 읽은 후 내가 이해한 바는 다음과 같다.
위 에러는 영속성 컨텍스트에 들어있지 않은 준영속 상태의 엔티티에 지연로딩을 시도했기 때문에 발생
그렇다면 현재 user가 준영속 상태가 되었다는 얘기인데... 처음에는 도저히 이해가 가지 않았다.
https://cantcoding.tistory.com/78
위 블로그에서 힌트를 얻고 정말 오랜 삽질 끝에 이유를 알아냈다.
User user = userDetails.getUser();
위 코드가 실행될때, userDetails 인터페이스에서 오버라이드 되는 메소드인 getUser() 메소드가 실행된다.
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("유저가 없습니다"));
return new UserDetailsImpl(user);
}
}
getUser() 메소드는 위 UserDetailsServiceImpl에서 조회된 유저 정보가 넘어오는것인데, 서비스 로직에서 유저를 조회 한것이 아니라 이미 UserDetailsServiceImple에서 트랜잭션이 끝나버렸기 때문에 영속성 컨텍스트의 생명주기가 끝나버렸기 때문에 lazy loading방식의 추가적인 쿼리 요청으로 proxyt 객체를 채우지 못하는것으로 파악 되었다.
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "profileImg")
@Query(value = "select u from User u where u.username = :username")
Optional<User> findByUsername(@Param("username") String username);
UserDetailsServiceImpl에서 유저 정보를 조회할때, @EntityGraph를 활용한 페치 조인을 걸어주어서 profileImg를 proxy가 아닌 실제 객체로 가져오게 설정해 주어서 해결이 되었다.
JPA 패치전략에 대한 이해도 향상
영속성 컨텍스트와 프록시에 대한 이해도 향상
에러 해결을 위한 Spirng Data JPA에 대한 전반적인 개념 정리
JPA에 대한 공부가 더 필요할 것 같다. 현재 문제를 해결하면서 이해한 과정중에 애매모호 했던 부분이 있어서 잘못 알고 있는 부분이 있을것 같다. 특히, 트랜잭션과 영속성 컨텍스트에 대해 더 학습 할 예정이다.