hanghae99/프로젝트

[부하테스트/JPA] Artillery로 성능 향상을 위한 N+1 문제 해결 과정

욘아리 2024. 3. 5. 01:58

마지막 요구사항인 부하테스트를 artillery를 이용해 진행해 보니 게시물 조회 API에 대한 성능 문제를 발견할 수 있었다. 쿼리문을 확인해 보니 여러 개의 select 문이 찍혀있는 것을 확인할 수 있었고, 찾아보니 이 문제가 흔히 말하는 N+1 문제였다. 게시물 조회 시 게시물이 작성자(사용자), 좋아요 수와 연관 관계가 설정되어 있어 조회된 데이터 개수만큼 연관 관계 조회 쿼리가 추가되고 있었다. 따라서 불필요한 조회로 성능 저하 문제를 발생시키고 있었다. N+1 문제를 개선하고 부하테스트를 진행해 그 전과 비교해 보았다.

 

❓문제 상황

Hibernate: // 게시물 조회
    /* <criteria> */ select
        p1_0.id,
        p1_0.content,
        p1_0.created_at,
        p1_0.modified_at,
        p1_0.title,
        p1_0.user_id 
    from
        post p1_0 
    order by
        p1_0.created_at desc 
    limit
        ?, ?
// ...
Hibernate: // 작성자 정보 조회 * 5
    select
        u1_0.id,
        u1_0.active,
        u1_0.created_at,
        u1_0.description,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.profile_image,
        u1_0.role 
    from
        user u1_0 
    where
        u1_0.id=?
// ...
Hibernate: 좋아요 누른 작성자 조회 * 10
    select
        pl1_0.post_id,
        pl1_0.id,
        u1_0.id,
        u1_0.active,
        u1_0.created_at,
        u1_0.description,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.profile_image,
        u1_0.role 
    from
        post_like pl1_0 
    left join
        user u1_0 
            on u1_0.id=pl1_0.user_id 
    where
        pl1_0.post_id=?

 

DB에 post 10개, 작성자 5명(총 사용자는 6명), 좋아요 누른 정보 10개가 들어있었다. 게시물 조회 시 메인 게시물 정보 조회 쿼리 (1번)작성자 정보 조회 쿼리 (5번), 좋아요 정보 조회 쿼리 (10번)16번의 쿼리가 실행되었다. 모두 게시물 목록을 완전히 구성하기 위해 필요한 데이터를 가져오는 데 사용된 쿼리다. 

 

📍해결 과정 - Artilley 설치 및 실행

우선 부하테스트를 진행하기 위해 가장 많이 사용하는 artillery를 사용해서 진행했다. 처음 테스트해 보기 때문에 제일 많이 사용하는 툴인 아틸러리를 선택했다.

  1. 설치 npm install -g artillery@latest
  2. 버전 확인 artillery --version
    설치 완료
  3. test-config.yaml 작성
  4. 실행 artillery run --output report.json test-config.yaml
  5. 결과 파일 html 파일로 만들어 주고 확인 artillery report ./report.json

 

💡해결 방법

테스트를 위해 사용자 1000명, 게시물 1000개의 더미데이터를 넣고 테스트를 진행함

 

✔️ 1:N의 관계인 좋아요 테이블에 @BatchSize 사용

연관된 좋아요 데이터를 일정량만큼 묶어서 한 번에 가져올 수 있도록 수정

PostLike entity
Post entity

 

✔️ N:1의 관계인 user는 fetch join 사용

게시물을 조회하는 쿼리와 함께 관련된 사용자 정보를 한 번의 쿼리로 불러올 수 있도록 수정

PostRepository

 

📉 테스트 결과

Hibernate: 
    /* SELECT p FROM Post p JOIN FETCH p.user 
    order by p.createdAt desc */ select
            p1_0.id,
            p1_0.content,
            p1_0.created_at,
            p1_0.modified_at,
            p1_0.title,
            u1_0.id,
            u1_0.active,
            u1_0.created_at,
            u1_0.description,
            u1_0.email,
            u1_0.modified_at,
            u1_0.nickname,
            u1_0.password,
            u1_0.profile_image,
            u1_0.role 
        from
            post p1_0 
        join
            user u1_0 
                on u1_0.id=p1_0.user_id 
        order by
            p1_0.created_at desc 
        limit
            ?, ?
Hibernate: 
    /* SELECT count(p) FROM Post p JOIN p.user */ select
            count(p1_0.id) 
        from
            post p1_0 
        join
            user u1_0 
                on u1_0.id=p1_0.user_id
Hibernate: 
    select
        pl1_0.post_id,
        pl1_0.id,
        u1_0.id,
        u1_0.active,
        u1_0.created_at,
        u1_0.description,
        u1_0.email,
        u1_0.modified_at,
        u1_0.nickname,
        u1_0.password,
        u1_0.profile_image,
        u1_0.role 
    from
        post_like pl1_0 
    left join
        user u1_0 
            on u1_0.id=pl1_0.user_id 
    where
        pl1_0.post_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

쿼리문은 게시물 정보와 해당 게시물을 작성한 사용자 정보 조회 쿼리(게시물과 사용자 테이블 조인), 게시물의 총 개수 조회 쿼리, 특정 게시물들에 대한 좋아요 정보 조회 쿼리 3번의 쿼리가 실행되어 엄청나게 줄어든 것을 확인했다.

 

전 - median_727.9, p95_8520.7 / 후 - median_22, p95_407.5

 

내가 원하는 만큼의 결과가 나오진 않았지만 두 개의 결과값을 비교하면 응답 시간이 이전보다 많이 개선된 것을 확인할 수 있다. http.response_time 그래프를 보면 중앙값과 95th 백분위수의 차이가 좁을수록 시스템이 일관되게 안정되고 빠르게 동작한다고 평가할 수 있는데, median과 p95의 차이도 많이 줄어들었다. 초기 상태에서의 차이는 7792.8, 개선 후의 차이는 385.5로 개선율은 약 95.05%이다. ( ((7792.8 - 385.5) / 7792.8) * 100 = 약 95.05% - 이 계산법이 맞나요...)

 

전, 후 비교

 

마지막으로 AOP를 이용한 시간 측정도 많이 줄어든 것을 확인했고, 대략 54.8% 개선되었다고 볼 수 있다.