10. creww 쿼리개선(1)
creww project 쿼리개선
시작
맨 처음 코드를 작성하면서 N+1 문제들을 생각 안하고 코드를 작성했다.
그래서 도메인 순서대로 쿼리를 개선해야할지..어찌하지?
생각하다가 일단 Postman을 실행시킨 뒤에 id가 1인 보드에 전체 게시글 요청을 보냈다.
getPosts 라는 서비스 메서드를 먼저 해결하기로..
문제점
쿼리 개선 전 서비스 로직
public Page<PostResponse> getPosts(Long boardId, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Post> posts = postRepository.findByBoardId(boardId, pageable);
return posts.map(post -> {
String username = userRepository.findById(post.getUserId())
.map(User::getUsername)
.orElse("유저 없음");
return new PostResponse(post.getId(), post.getTitle(), post.getContent(), post.getUserId(), username, post.getCreatedAt(),post.getViews());
});
}
쿼리문
Hibernate:
/* select
generatedAlias0
from
Post as generatedAlias0
where
generatedAlias0.boardId=:param0 */ select
post0_.id as id1_3_,
post0_.created_at as created_2_3_,
post0_.updated_at as updated_3_3_,
post0_.board_id as board_id4_3_,
post0_.content as content5_3_,
post0_.title as title6_3_,
post0_.user_id as user_id7_3_,
post0_.views as views8_3_
from
post post0_
where
post0_.board_id=? limit ?
Hibernate:
select
user0_.id as id1_4_0_,
user0_.email as email2_4_0_,
user0_.password as password3_4_0_,
user0_.username as username4_4_0_
from
user user0_
where
user0_.id=?
Hibernate:
select
user0_.id as id1_4_0_,
user0_.email as email2_4_0_,
user0_.password as password3_4_0_,
user0_.username as username4_4_0_
from
user user0_
where
user0_.id=?
Hibernate:
/* select
generatedAlias0
from
Post as generatedAlias0
where
generatedAlias0.boardId=:param0 */ select
post0_.id as id1_3_,
post0_.created_at as created_2_3_,
post0_.updated_at as updated_3_3_,
post0_.board_id as board_id4_3_,
post0_.content as content5_3_,
post0_.title as title6_3_,
post0_.user_id as user_id7_3_,
post0_.views as views8_3_
from
post post0_
where
post0_.board_id=? limit ?
Hibernate:
select
user0_.id as id1_4_0_,
user0_.email as email2_4_0_,
user0_.password as password3_4_0_,
user0_.username as username4_4_0_
from
user user0_
where
user0_.id=?
Hibernate:
select
user0_.id as id1_4_0_,
user0_.email as email2_4_0_,
user0_.password as password3_4_0_,
user0_.username as username4_4_0_
from
user user0_
where
user0_.id=?
Postman으로 요청시 결과
{
"content": [
{
"id": 1,
"title": "test3",
"content": "content3",
"userId": 1,
"username": "teste",
"createdAt": "2024-05-22T14:32:31.989315",
"views": 82
},
{
"id": 2,
"title": "test1",
"content": "content1",
"userId": 1,
"username": "teste",
"createdAt": "2024-05-22T14:32:38.370372",
"views": 8
},
{
"id": 3,
"title": "test",
"content": "content",
"userId": 1,
"username": "teste",
"createdAt": "2024-05-22T14:32:46.729826",
"views": 2
},
{
"id": 4,
"title": "aa",
"content": "aaa",
"userId": 1,
"username": "teste",
"createdAt": "2024-05-26T13:58:37.68011",
"views": 1
},
{
"id": 7,
"title": "hi(update)",
"content": "test comment update",
"userId": 1,
"username": "teste",
"createdAt": "2024-06-05T15:18:53.632",
"views": 0
},
{
"id": 8,
"title": "알림",
"content": "알림",
"userId": 1,
"username": "teste",
"createdAt": "2024-06-06T17:48:55.107",
"views": 0
},
{
"id": 9,
"title": "알림",
"content": "알림",
"userId": 2,
"username": "teste2",
"createdAt": "2024-06-28T18:19:30.56",
"views": 0
}
]
}
요청 했을때 나온 결과에서 게시글은 여러개인데 작성자 id를 보면 {userId:1,userId:2} 이렇게 두명의 작성자만 있다는 것을 주목해야 한다. (요청시 page 정보도 나오는데 그건 뺐음)
게시글을 전체 조회할때 총 7개의 게시글을 가져온다 (페이징으로 한 페이지 당 10개 씩의 게시글만 가져오게 설정)
유저 정보를 n번 갖고온다.
1페이지를 가져올때 10명의 각자 다른 유저가 쓴 글이면 쿼리는 11번 실행된다.
해결법
일단 나의 코드는 엔티티간의 의존도를 낮추려고 작성해서 연관관계가 맵핑되어있지가 않다.
그래서 fetch join은 사용불가
팀 프로젝트때 사용했던 Batch기법이 생각났다.
적용해보자
public Page<PostResponse> getPosts(Long boardId, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Post> posts = postRepository.findByBoardId(boardId, pageable);
//모든 포스트의 userId 가져오기
Set<Long> userIds = new HashSet<>();
for(Post post : posts.getContent()){
userIds.add(post.getUserId());
}
//한번에 모두 조회
List<User> users = userRepository.findAllById(userIds);
Map<Long, String> userMap = new HashMap<>();
for (User user : users) {
userMap.put(user.getId(), user.getUsername());
}
//posts에서 각 post의 userId를 사용하여 해당 사용자의 이름을 찾음
//post의 데이터와 찾은 사용자 이름을 사용하여 새로운 Page<PostRespons>를 생성
return posts.map(post -> {
String username = userMap.getOrDefault(post.getUserId(), "유저 없음");
return new PostResponse(post.getId(), post.getTitle(), post.getContent(),
post.getUserId(), username, post.getCreatedAt(), post.getViews());
});
}
개선 전과 후 차이점
데이터 조회 방식:
개선 전: 각 게시글마다 개별적으로 사용자 정보를 조회 (N+1 문제 발생)
개선 후: 모든 관련 사용자의 ID를 모아 한 번에 사용자 정보를 조회
쿼리 실행 횟수:
개선 전: 게시글 조회 1번 + 사용자 정보 조회 N번 (N은 게시글 수)
개선 후: 게시글 조회 1번 + 사용자 정보 일괄 조회 1번
성능 영향:
게시글 수가 증가해도 항상 2번의 쿼리만 실행되어 성능이 일정하게 유지됨
특히 대량의 데이터를 처리할 때 성능 향상이 두드러짐
확장성:
- 사용자 수가 증가해도 추가적인 쿼리 실행 없이 효율적으로 처리 가능
개선된 쿼리문
Hibernate:
/* select
generatedAlias0
from
Post as generatedAlias0
where
generatedAlias0.boardId=:param0 */ select
post0_.id as id1_3_,
post0_.created_at as created_2_3_,
post0_.updated_at as updated_3_3_,
post0_.board_id as board_id4_3_,
post0_.content as content5_3_,
post0_.title as title6_3_,
post0_.user_id as user_id7_3_,
post0_.views as views8_3_
from
post post0_
where
post0_.board_id=? limit ?
Hibernate:
/* select
generatedAlias0
from
User as generatedAlias0
where
generatedAlias0.id in (
:param0
) */ select
user0_.id as id1_4_,
user0_.email as email2_4_,
user0_.password as password3_4_,
user0_.username as username4_4_
from
user user0_
where
user0_.id in (
? , ?
)
good~
Subscribe to my newsletter
Read articles from Thunder directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Thunder
Thunder
안녕하세요! Web개발을 공부하고 있는 윤종일 입니다. 현재는 Java 백엔드 개발을 깊게 파고들고 있어요! 제 궁극적인 목표는 풀스택 개발자가 되는 것입니다. 프론트엔드와 백엔드 모두를 자유롭게 넘나들며, 사용자에게 가치를 전달할 수 있는 완성도 높은 애플리케이션을 만드는 것이 목표입니다.