TIL

플러스 주차 개인과제 TIL 4.

phonebee 2025. 3. 13. 19:11

▶ LEVEL 2

▷ 7. N+1

● 문제의 요구사항

1. CommentController 클래스의 getComments() API를 호출할 때 N+1문제가 나타나고 있다. 해당 문제가 발생하지 않도록 코드를 수정해라

 

문제의 요구사항을 보면 현재 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