▶ LEVEL 2
▷ 7. N+1
● 문제의 요구사항
1. CommentController 클래스의 getComments() API를 호출할 때 N+1문제가 나타나고 있다. 해당 문제가 발생하지 않도록 코드를 수정해라
문제의 요구사항을 보면 현재 N+1문제가 나타나고 있음을 알 수 있다.
문제가 보여준 N+1 문제는

commnets 테이블에서 데이터를 조회한 후, 각 comment마다 개별적으로 users 테이블에서 데이터를 조회하는 문제이다.
우선 문제가 나타나는 코드가 어디인지 살펴보도록 하자
@GetMapping("/todos/{todoId}/comments")
public ResponseEntity<List<CommentResponse>> getComments(@PathVariable long todoId) {
return ResponseEntity.ok(commentService.getComments(todoId));
}
먼저 컨트롤러이다.
컨트롤러에서 getComments() API를 호출할 때 N+1 문제가 나타난다고 한다.
그럼 다음으로 getComments 코드를 살펴보자
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())
);
dtoList.add(dto);
}
return dtoList;
}
getComments() 는 todoId에 해당하는 유저를 찾아서 List형식으로 값을 리턴하는 형식이다. 그렇다면 commentRespository가 원인일 것이다.
@Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
쿼리문을 보면 comments테이블에서 특정 todoId에 해당하는 댓글 목록을 조회하고 각 comment의 userId를 기반으로 개별적으로 user 테이블에서 조회하고 있다.
그럼 N+1문제를 해결하려면 어떻게 코드를 개선해야할까
바로 Fetch Join을 사용하는 것이다.
패치 조인을 사용하면 한 번의 쿼리로 comments와 users 정보를 함께 가져올 수 있게 된다.
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
이렇게 쿼리문을 수정하면 댓글을 조회할 때 User의 정보까지 한 번의 쿼리로 가져오기 때문에 N+1문제를 해결할 수 있다.
▷ 8. QueryDSL
● 문제의 요구사항
1. JPQL로 작성된 findByIdWithUser 를 QueryDSL로 변경한다.
2. N+1문제가 발생하지 않도록 한다.
먼저 QueryDSL에 대하여 알고 가자
● QueryDSL이란?
: 타입 안정성을 제공하는 SQL 및 JPQL 쿼리 빌더 라이브러리로, JPQL 또는 SQL을 자바 코드로 직관적으로 작성할 수 있도록 도와주는 도구이다.
● QueryDSL의 장점
1. 타입 안정성
- 문자열 기반이 아닌 코드 기반으로 쿼리를 작성할 수 있다.(컴파일 시점에 오류를 발견할 수 있다.
2. 동적 쿼리 작성이 쉬움
- 조건문을 동적으로 추가할 수 있다.
3. 코드 가독성과 유지보수성 증가
- 좀 더 직관적이고 가독성이 좋다.
- IDE의 자동 완성 기능을 활용할 수 있다.
4. JPA와 SQL 지원
- QueryDSL뿐만 아니라, Native SQL과 함께 사용할 수 있다.
이제 해당 코드를 살펴보도록 하자
@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
해당 코드는 Todo 엔티티와 User엔티티가 N+1관계를 띄고 있으며, UserId와 TodoId가 일치하는 Todo데이터만 조회한다.
그럼 이를 QueryDSL로 바꿔보도록 하자
먼저 QueryDSL을 적용할 사용자 정의 리포지토리 인터페이스를 만든다.
public interface TodoRepositoryQuery {
Optional<Todo> findByIdWithUser(Long todoId);
}
그 후 사용자 정의 리포지토리를 구현한다.
여기에는 QueryDSL을 이용하여 findByIdWithUser 메서드를 작성한다.
@RequiredArgsConstructor
public class TodoRepositoryImpl implements TodoRepositoryQuery{
private final JPAQueryFactory queryFactory;
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = queryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
}
}
그 후 Todo리포지토리에 있었던 findByIdWithUser()를 지우면 TodoService에 오류가 나타나게 된다.
해당 클래스에서 작성한 findByIdWithUser()를 사용하기 위해서 Todo리포지토리의 상속에 사용자 정의 리포지토리도 상속하면된다.
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryQuery
그러면 오류가 사라지며 서비스에서는 Todo리포지토리를 불러와서 findByIdWithUser() 메서드를 사용할 수 있게 된다.
'TIL' 카테고리의 다른 글
| 플러스 주차 개인 과제 TIL 6. (0) | 2025.03.17 |
|---|---|
| 플러스 주차 개인 과제 TIL 5. (0) | 2025.03.14 |
| 플러스 주차 개인과제 TIL 3. (0) | 2025.03.12 |
| 플러스 주차 개인과제 TIL 2. (0) | 2025.03.11 |
| 플러스 주차 개인과제 TIL 1. (0) | 2025.03.10 |