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());
}
...
}
'내일배움캠프 > Spring Plus' 카테고리의 다른 글
[Spring Plus] Level 2-3 요구사항 반영 (0) | 2024.11.15 |
---|---|
[Spring Plus] Level 2-1 요구사항 반영 (0) | 2024.11.14 |
[Spring Plus] Level 1-5 요구사항 반영 (0) | 2024.11.13 |
[Spring Plus] Level 1-4 요구사항 반영 (0) | 2024.11.13 |
[Spring Plus] Level 1-3 요구사항 반영 (0) | 2024.11.12 |