주뇽's 저장소

[Troubleshooting] Spring Boot와 JPA에서 유저의 총 좋아요 개수를 조회할 때 발생한 LazyInitializationException 해결 본문

웹개발/SpringBoot

[Troubleshooting] Spring Boot와 JPA에서 유저의 총 좋아요 개수를 조회할 때 발생한 LazyInitializationException 해결

뎁쭌 2024. 5. 3. 21:00
728x90
반응형

 최근에 Spring Boot와 JPA를 사용하여 게시글 프로젝트를 진행하던 중, 유저가 받은 총 좋아요 개수를 조회하는 과정에서 LazyInitializationException이 발생하는 문제를 겪었다. 이 글에서는 해당 문제의 원인과 해결 방법에 대해 자세히 설명한다.

문제 상황:

  • UserAccount 엔티티와 Post 엔티티가 일대다 관계로 매핑되어 있다
  • UserAccount 엔티티에서 posts 필드가 지연 로딩(Lazy Loading)으로 설정되어 있다
  • 유저가 받은 총 좋아요 개수를 조회하기 위해 UserAccount의 getTotalLikeCount() 메서드에서 posts 컬렉션에 접근하려고 한다
  • getTotalLikeCount() 메서드 내부에서 LazyInitializationException 발생한다

문제 원인:

LazyInitializationException은 지연 로딩으로 설정된 연관 관계를 불러오려고 할 때 세션이 이미 종료된 경우 발생한다. getTotalLikeCount() 메서드에서 posts 컬렉션에 접근하려고 할 때, 세션이 종료되어 있어 지연 로딩이 불가능하기 때문에 예외가 발생한 것이다.

해결 방법 1: @Transactional 어노테이션 사용한다

처음에는 @Transactional 어노테이션을 사용하여 문제를 해결하려고 했다. @Transactional 어노테이션을 getTotalLikeCount() 메서드가 호출되는 서비스 메서드에 추가하면 해당 메서드가 트랜잭션 내에서 실행되므로 지연 로딩이 가능해진다.

@Service
public class MyUserService {
    @Transactional
    public int getLike(Long userId) {
        UserAccount userAccount = userRepository.findById(userId)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));

        return userAccount.getTotalLikeCount();
    }
}


그러나 이 방법으로 문제가 완전히 해결되지 않았고, 여전히 LazyInitializationException이 발생했다.

 

해결 방법 2: @EntityGraph 어노테이션 사용한다

다음으로 시도한 방법은 @EntityGraph 어노테이션을 사용하는 것이었다. @EntityGraph 어노테이션을 사용하면 지연 로딩으로 설정된 연관 관계를 즉시 로딩(Eager Loading)으로 변경할 수 있다.

1. UserRepository에 @EntityGraph 어노테이션을 사용하여 posts 연관 관계를 즉시 로딩으로 변경한다

@Repository
public interface UserRepository extends JpaRepository<UserAccount, Long> {
    @EntityGraph(attributePaths = {"posts"})
    Optional<UserAccount> findWithPostsById(Long id);
}


2. 서비스 메서드에서 findWithPostsById 메서드를 사용하여 UserAccount를 조회한다

@Service
public class MyUserService {
    public int getLike(Long userId) {
        UserAccount userAccount = userRepository.findWithPostsById(userId)
                .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));

        return userAccount.getTotalLikeCount();
    }
}


이 방법을 사용하니 LazyInitializationException이 발생하지 않고 정상적으로 총 좋아요 개수를 조회할 수 있었다. @EntityGraph 어노테이션을 사용하여 posts 연관 관계를 즉시 로딩으로 변경하고, findWithPostsById 메서드를 통해 UserAccount를 조회했기 때문에 세션이 종료되어도 문제없이 posts 컬렉션에 접근할 수 있게 되었다.

 

해결 방법 3: DTO를 사용하여 필요한 데이터만 조회한다

마지막으로 고려한 방법은 DTO(Data Transfer Object)를 사용하여 필요한 데이터만 조회하는 것이다. 도메인 엔티티 대신 DTO를 사용하면 필요한 데이터만 선택적으로 가져올 수 있어 성능 최적화에 도움이 된다.

1. UserAccountDTO 클래스 생성한다

public class UserAccountDTO {
    private Long id;
    private String nickname;
    private int totalLikeCount;

    // Constructor, Getters, Setters
}


2. UserRepository에 JPQL 쿼리를 사용하여 필요한 데이터만 조회하고 UserAccountDTO로 매핑한다

@Repository
public interface UserRepository extends JpaRepository<UserAccount, Long> {
    @Query("SELECT new cohttp://m.example.dto.UserAccountDTO(u.id, u.nickname, SUM(p.likeCount)) " +
           "FROM UserAccount u LEFT JOIN u.posts p " +
           "WHERE u.id = :userId " +
           "GROUP BY u.id, u.nickname")
    UserAccountDTO findUserAccountDTOById(@Param("userId") Long userId);
}


3. 서비스 메서드에서 UserAccountDTO를 사용하여 필요한 데이터 조회한다

@Service
public class MyUserService {
    public UserAccountDTO getLike(Long userId) {
        return userRepository.findUserAccountDTOById(userId);
    }
}



DTO를 사용하는 방법은 지연 로딩 문제를 근본적으로 해결하진 않지만, 필요한 데이터만 선택적으로 조회할 수 있어 성능 최적화에 도움이 된다. 또한 도메인 엔티티와 프레젠테이션 계층을 분리하는 장점도 있다.

마무리:
Spring Boot와 JPA에서 유저의 총 좋아요 개수를 조회할 때 발생한 LazyInitializationException을 해결하는 방법에 대해 알아보았다. @Transactional 어노테이션을 사용하는 것만으로는 문제를 완전히 해결하기 어려울 수 있으며, @EntityGraph 어노테이션을 사용하거나 DTO를 활용하는 것이 효과적인 해결책이 될 수 있다.