[Spring Plus] Level 2-2 요구사항 반영

2024. 11. 14. 21:20내일배움캠프/Spring Plus

 'Level 2-2' 의 요구사항을 반영한 내용을 기록한 포스팅이다. 어떠한 생각과 과정을 통해 요구사항을 반영했는지 알 수 있도록 작성해 보았다.

 

0. 요구사항

 현재 API(GET /todos/{todoId}/comments) 요청시 발생하는 'N+1' 문제를 해결해야 한다.

 

 

1. 문제 원인 파악

 우선 CommentService 클래스를 확인해보니 댓글 목록 조회에 사용되는 'getComments()' 를 확인하였다. 해당 메서드는 패스 파라미터로 전달받은 일정 ID 로 CommentRepository 인터페이스의 'findByTodoWithUser()' 를 호출해 반환받은 'List<Comment>' 로 'List<CommentResponse>' 를 생성해 반환하고 있었다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {
    ...

    public List<CommentResponse> getComments(long todoId) {
        List<Comment> commentList = commentRepository.findByTodoIdWithUser(todoId);

        List<CommentResponse> dtoList = new ArrayList<>();
        for (Comment comment : commentList) {
            User user = comment.getUser();
            CommentResponse dto = new CommentResponse(
                    comment.getId(),
                    comment.getContents(),
                    new UserResponse(user.getId(), user.getEmail(), user.getNickname())
            );
            dtoList.add(dto);
        }
        return dtoList;
    }
}

 

CommentRepository 인터페이스의 'findByTodoWithUser()' 경우 JPQL 을 사용해 쿼리가 아래와 같이 지정되어 있었다.

public interface CommentRepository extends JpaRepository<Comment, Long> {

    @Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
    List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}

 

여기까지 확인하니 어느정도 문제의 원인이 보였다. 현재 Comment 클래스의 'user' 필드에는 'FetchType.LAZY(지연로딩)' 이 적용되어 있어 조회시 반환되는 Comment 객체는 실제 User 객체가 아닌 프록시 객체를 가지게 된다.

 

이런 상태에서 CommentResponse 를 생성을 위해 Comment 객체의 'user' 필드에 접근하니 User 객체를 조회하는 쿼리가 추가적으로 날아가며 'N+1' 문제가 발생한 것이라 파악했다. 일정에 서로 다른 수 많은 사용자가 댓글을 작성할 수록 'N' 의 값은 커질 것이다.

 

 

2. 문제 해결

 N+1 문제를 해결하는 방법에는 여러 방법이 있겠지만 나의 경우 DB 에서 댓글 목록을 조회할 때 한 번에 사용자(댓글 작성자) 정보도 함께 조회하는 방식(= fetch join)을 택했다. 그래서 아래와 같이 'JOIN' 이 아닌 'JOIN FETCH' 를 쿼리에 적용하였다.

public interface CommentRepository extends JpaRepository<Comment, Long> {

    @Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId")
    List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}

 

물론 이 방법이 BEST 라고는 생각되지 않는다. 같은 사용자가 여러 댓글을 남길 경우 같은 사용자의 정보를 중복해서 가져오기 때문이다. 이러한 부분까지 반영한다면 차라리 일정 ID 에 작성된 댓글 목록을 조회하고 조회한 댓글들의 작성자들의 ID 를 모아 추가적으로 해당 ID 를 가진 사용자 정보를 조회하는, 2개의 쿼리를 날리는 방식을 사용할 것 같다. 하지만 요구사항에서는 일단 N+1 문제 해결을 최우선으로 이야기 했기에 'fetch join' 을 사용하는 방법을 택하게 되었다.

 

이후 Postman 을 통해 API 테스트를 아래와 같이 진행하였다.

댓글 목록 조회 요청
댓글 목록 조회시 사용되는 쿼리

 

테스트 결과는 이전과 같고, 중요한 것은 해당 요청시 사용되는 쿼리의 개수인데 위 이미지와 같이 해당 요청에는 하나의 쿼리만이 사용된 것을 확인할 수 있었다.

 

 

3. 그 외

 해당 요청 응답에 사용자 닉네임이 'null' 로 반환되는 것을 확인User 클래스의 'fromAuthUser()' 를 (연관된 private 생성자 또한)수정하였다.

@Getter
@Entity
@NoArgsConstructor
@Table(name = "users")
public class User extends Timestamped {
    ...

    private User(Long id, String email, UserRole userRole, String nickname) {
        this.id = id;
        this.email = email;
        this.userRole = userRole;
        this.nickname = nickname;
    }

    public static User fromAuthUser(AuthUser authUser) {
        return new User(authUser.getId(), authUser.getEmail(), authUser.getUserRole(), authUser.getNickname());
    }

    ...
}