장미정원
교내 프로젝트 느려터진 조회성능 향상기 (preflight, N+1, QueryDSL) 본문
배경설명
교내에서 진행한 GSM GOGO라는 프로젝트에서 적용한 조회성능 향상에 대한 내용입니다.
GSM GOGO는 교내 체육대회 관리 서비스이며 체육대회에 관련된 여러 편의기능과 경기에 포인트를 배팅하여 얻거나 잃을 수 있는 서비스입니다.
1차 릴리즈 기간이 종료된 후 해당 릴리즈에 필요한 요구사항들을 배포하였습니다. 그 중 문제가 발생한 부분은 팀 조회 기능이였습니다.
팀 조회 기능은 팀 리스트 조회 페이지에서 헤더를 통해 종목(축구, 배구, ..)별 팀을 조회할 수 있습니다.
또한 상세조회를 원하는 팀을 클릭시 팀 상세 조회 페이지로 이동되어 해당 팀에 참가하는 선수들을 표시해줍니다.
문제상황: 요청마다 발생하는 OPTIONS 요청
팀 리스트 조회 페이지에서 헤더를 통해 종목을 이동하거나 다른 페이지로 이동하면 해당 페이지에 필요한 데이터들을 백엔드 서버로 요청합니다. 이때 페이지에 데이터가 표시되는데 너무 오래걸리는 현상을 발견하여 네트워크 탭을 켜서 확인해보았습니다.
확인한 결과 위 사진과 같이 모든 각각의 요청마다 OPTIONS 메서드로 사전요청을 보내는것을 확인하였습니다.
OPTIONS 메서드
OPTIONS 메서드는 CORS에서 서버에 대해 허용된 통신 옵션을 요청할때 사용되는 메서드입니다. 해당 요청의 응답 헤더로 서버에 요청할 수 있는 메서드, 헤더 등의 정보를 받아옵니다.
해당 메서드는 위의 사진과 같이 캐시가 가능한 메서드입니다. 그리고 해당 사전요청은 처음 한 번만 보낸 후에는 더 이상 요청을 보낼 필요가 없이 캐시된 값을 사용하면 되어 요청마다 불필요하게 계속 요청할 필요가 없다고 생각하였습니다.
문제 해결: CORS preflight request 캐싱
OPTIONS 메서드는 Access-Control-Max-Age 헤더를 설정하여 캐시 시간을 설정할 수 있습니다. (최대 86,400초) 설정하지 않았을 경우 기본 값은 5초이므로 해당 값을 늘려 캐시시간을 늘릴 수 있습니다.
스프링 시큐리티 CORS 설정 부분에서 MaxAge의 시간을 늘려서 캐시시간을 길게두어 OPTIONS 요청이 여러번 발생하지 않게 하였습니다.
그 이후로는 첫 요청시에만 사전요청을 보내고 그 이후로는 발생하지 않는 것을 볼 수 있습니다.
해당 PR: https://github.com/GSM-GOGO/GSM-GOGO-Server/pull/131
문제상황: N+1 문제 발생
OPTIONS 메서드 성능저하 문제는 해결하였지만, 실제 요청 레이턴시가 오래걸리는 문제가 발생하였습니다.
팀 상세보기 페이지에서 데이터를 받아올 때 평균 레이턴시가 900ms까지 늦어지며 성능저하가 발생하는 것을 확인하였습니다.
엔티티 연관관계
팀 관련 엔티티의 연관관계는 팀, 팀 참여자, 유저 엔티티가 각각 1 : N, 1 : 1로 연관관계가 매핑되어있으며 각 연관관계의 fetch type은 LAZY로 설정되어있습니다.
팀 상세 조회시 팀 조회 -> 팀 참여자들 전체 조회 -> 팀 참여자들을 순회하며 각 유저 조회 와 같은 과정이 발생합니다.
fetch type이 LAZY로 설정되어있기 때문에 팀을 조회시 팀 조회 쿼리 1건, 팀 참여자들 조회 쿼리 1건, 그리고 팀 참여자들을 순회하면서 각 유저에 대한 참조를 하여 유저 데이터를 받아온다면 각 유저에 대한 조회 쿼리 N건이 발생하게 됩니다.
위와 같이 만약 20명의 선수가 참여하는 팀을 조회시 총 22건의 단건쿼리가 나가는 N + 1 문제가 발생한다는 것을 확인하였습니다.
문제해결: QueryDSL - fetch join
팀, 팀 참여자, 유저 엔티티의 연관관계의 fetch type이 LAZY로 설정되어있기 때문에 연관된 엔티티 속성에 접근 시 마다 각각의 조회쿼리가 발생하므로 조회에 필요한 연관된 엔티티들을 한번의 쿼리로 조회하면 불필요하게 발생하는 쿼리가 개선될 것입니다.
이러한 문제를 QueryDSL을 활용하여 fetch join으로 조회하고자 하는 팀, 팀 참여자, 유저의 연관된 엔티티를 한방쿼리로 조회하여 N+1 문제를 해결하였습니다.
개선 후 더 이상 N + 1 문제는 발생하지 않았으며 연관된 엔티티를 한꺼번에 조회하는 한방쿼리가 발생하였습니다.
Jmeter를 사용하여 성능테스트시 5000쿼리 표본 기준으로 평균 880ms -> 550ms로 레이턴시를 감소시켰으며, 실제 서비스에서 레이턴시를 980ms -> 160ms로 성능이 약 80%이상 향상되었습니다.
해당 PR: https://github.com/GSM-GOGO/GSM-GOGO-Server/pull/134
느낀점
성능개선이라는 작업을 처음 진행해보았는데 서비스를 운영하면서 문제점을 파악하고 해결방식을 도출해 나가는 과정에서 평소 해보지 못한 고민들을 하게 되었고 문제점이 개선 되었을 때 큰 뿌듯함을 느꼈습니다.
앞으로 더 넒은 시각에서 다양한 고민을 하며 품질 높은 서비스를 개발할 수 있도록 노력해야겠다는 생각을 하였습니다.
'Back-end' 카테고리의 다른 글
✨ 스프링을 사용하는 이유 (0) | 2024.07.29 |
---|---|
✨ JDBC와 DataSource (0) | 2024.07.01 |
교내 프로젝트 운영중 발생한 동시성 문제와 IntegerOverFlow 문제 해결기 (0) | 2024.06.16 |
✨ JVM 가상머신 (0) | 2024.05.28 |
✨ Servlet, Dispatcher Servlet - 서블릿과 디스패처 서블릿 (0) | 2024.02.21 |