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~