카테고리 없음

반정규화를 통한 성능 향상

olsohee 2024. 5. 21. 16:00

이 글은 프로젝트에서 게시글 페이징 조회시 성능 향상 과정을 기록하는 글이다. 성능 향상을 위해 적용한 것들을 순서대로 설명하겠다.

1. 배치 사이즈 설정

문제

게시글 페이징 조회시 게시글의 좋아요 수, 댓글 수, 북마크 수를 함께 반환하기 위해 Post 엔티티를 Like, Comment, Bookmark 엔티티와 조인해야 한다. 그리고 post.getLikes().size()와 같이 Post 엔티티와 연관된 Like 엔티티의 개수가 몇개인지 판단하여 좋아요 수를 알아내는 방식을 사용했다. 그런데 이 방법은 N+1 문제가 발생한다. 

해결 방법 1. 페치 조인

따라서 해결 방법으로 페치 조인을 떠올렸다. 그런데 페치 조인은 ToMany 관계에 대해서 페치 조인을 하나만 적용할 수 있다. 나는 Like, Comment, Bookmark에 대해 모두 페치 조인을 하려고 했기 때문에 MultipleBagFetchException 예외가 발생하며, 이 방법을 적용할 수 없다.

해결 방법 2. 배치 사이즈

따라서 배치 사이즈를 설정해줬다. 다음과 같이 배치 사이즈를 설정하면, Like 엔티티를 조회해올 때 like 테이블에서 설정한 배치 사이즈만큼 한 번에 조회해온다.

결과

100개의 게시글을 리스트로 조회한다고 가정할 때

배치 사이즈 적용 전에는 다음과 같이 N+1 문제가 발생했다.

  • post 테이블 조회 sql 1번
  • likes 테이블 조회 sql 100번
  • bookmark 테이블 조회 sql 100번
  • comment 테이블 조회 sql 100번

그러나 배치 사이즈 적용 후 다음과 같이 쿼리 수가 훨씬 감소했다.

  • post 테이블 조회 sql 1번
  • likes 테이블 조회 sql 1번
  • bookmark 테이블 조회 sql 1번
  • comment 테이블 조회 sql 1번

또한 API 응답 시간을 비교했을 때도 251ms ➡️ 118ms약 53% 향상되었다.

2. 반정규화

문제

게시글 리스트를 조회할 때 좋아요 수는 배치 사이즈로 설정한 값만큼 한 번에 like 테이블에서 데이터를 조회해온 후, 그 개수를 통해 좋아요 수를 알 수 있었다. 

 

그런데 여기에는 2가지 문제가 있다. 첫 번째로는 단지 좋아요 수만 필요할 뿐 Like 엔티티의 세부적인 필드 값은 알 필요가 없다. 그런데 조인을 통해 Post와 연관된 모든 Like 엔티티를 조회해오는 것은 매우 비효율적이다. 

 

두 번째로는 좋아요 수를 기준으로 정렬해서 게시글 리스트를 조회할 때이다. 좋아요 수가 높은 순으로 게시글을 조회하기 위해서 다음과 같이 쿼리를 작성했다.

위 쿼리는 post와 like 테이블을 조인하는 과정에서 post 테이블의 모든 레코드를 대상으로 like 테이블과 매칭 작업이 수행된다. 즉 post 테이블을 풀스캔한다. 따라서 게시글이 100만개이면 100만개의 레코드에 접근하여 조인하기 때문에 매우 비효율적이다.

 

정말로 풀스캔하는지를 알아보기 위해 테스트로 100개의 게시글을 넣고 다음 명령어를 실행해봤다.

explain SELECT p.*
FROM post p
LEFT JOIN likes l ON l.post_id = p.post_id
GROUP BY p.post_id
ORDER BY COUNT(l.like_id) DESC
LIMIT 100 OFFSET 0;

 

결과는 다음 사진과 같다. PK와 FK 인덱스를 사용한다. 그러나 PK 인덱스를 사용할 때 rows의 값이 100이다. 즉, PK 인덱스를 통해 접근하는 레코드 수가 100개 즉, 모든 레코드에 접근한다는 의미이다.

해결: 반정규화

  • 좋아요 수만 필요한데 Like 엔티티 자체를 조회하는 문제
  • 조인을 위해 post 테이블을 풀스캔 하는 문제

위 두 문제를 해결하기 위해 Post 엔티티에 likeCount 필드를 추가하여 반정규화를 적용했다. 따라서 게시글 리스트를 조회할 때 Post 엔티티의 likeCount 필드 값을 통해 좋아요 수를 알 수 있으며, like 테이블과 조인할 필요가 없다.

결과

100만 개의 데이터를 넣고 랜덤한 게시글에 총 1000개의 좋아요를 추가해줬다. 그리고 좋아요 수가 높은 순으로 100개의 게시글 리스트를 조회했다.

 

결과적으로 5.06s ➡️ 2.18s약 56.92% 향상되었다.

 

또한 좋아요 수를 기준으로 게시글 리스트를 조회할 때 복잡한 쿼리를 작성해주지 않아도 되며, 다음과 같이 스프링에서 제공해주는  @PageableDefault를 사용하면 된다. 따라서 다음과 같이 쿼리 파라미터를 유동적으로 설정하여 요청을 보내면, 다음과 같이 리포지토리의 하나의 메소드로 커버할 수 있다.

 

http://localhost:8080/community/posts/like?sort=likeCount,desc&size=100