ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 호텔 상세 페이지 성능 개선 경험 : DB 데이터 패치 크기를 줄이자
    개발 나누고 더하기/DB 2023. 3. 3. 23:05

    1년 전 간단한 쿼리 수정으로 호텔 상세 정보 조회 API 응답속도를 15% 가량 개선한 적이 있다. APM으로 핀포인트를 쓰는데 아래와 같이 API에서의 메서드별 처리시간이 나온다.

    핀포인트라 생각하자

    어디서 시간이 오래 걸리는지 병목지점을 찾기 위해 봤는데, 아래와 같이 단순한 쿼리가 전체 응답시간의 60%정도를 차지하고 있었다. 

    SELECT ha.hotel_id, a.name, ha.is_using FROM hotel_amenity ha 
    JOIN amenity a ON ha.amenity_id  = a.id 
    WHERE 
    hotel_id = 77;

    아주 단순한 쿼리이고, 해당 쿼리를 가져와 실행계획을 보면 인덱스도 잘 타는 것을 확인할 수 있다.

    이전 애플리케이션에서 실행하던 쿼리 실행계획

    일단 여기까지 중간 정리하면, 호텔에는 아기 침대, 피트니스, 사우나 등 해당 업장에서 서비스하는 시설이나 제공물품이 있는데 이를 어메니티라고 한다. 여러 호텔에서 제공하는 어메니티는 비슷비슷하여 많아봤자 50개면 모두 표준화해 분류할 수 있다. 그리고 몇 천개되는 호텔과 이 어메니티를 묶어 호텔별 어메니티를 데이터로 관리해야 되므로, 호텔과 어메니티 테이블 관계 사이에는 다대다 매핑 테이블이 필요하다. 이를 ERD로 나타내면 아래와 같다.

    호텔과 어메니티 관계도

    이 글에서 필요한 필드만 뽑아서 정리한 것으로 hotel_id에는 인덱스도 걸려있다. 왜 이렇게 오래 걸리나 이상했는데, 한 가지 이상한 점이 있었다. 호텔 상세 페이지니 호텔의 id는 정해져있고, 이를 키로 삼아 hotel_amenity와 amenity를 조인하여 해당 호텔에 매핑된 유효한 어메니티 데이터를 가져오는 것이 목표이다. 예를 들어 호텔A가 조식, 카페테리아, 발렛파킹 이 3개 서비스를 제공한다고 치면 이 3개 항목만 가져오면 되는 것이다. 그런데 아래와 같이 574개나 들고 온다. is_using(사용여부)라는 필드를 보면 알겠지만 모두 0(false)이다. 그리고 빨간 네모로 표시해 놓은 곳이 DB에서 데이터 추출 후 실제 클라이언트로 전송되기까지의 시간인데, 실제 애플리케이션 서버 - DB 서버 간의 패치 속도는 80ms에 가까울 정도로 컸다. 아래 그림은 PC내 로컬 DB에서 재현하느라 인터넷 회선이 아닌 디스크에서 바로 읽어온 정보라 작게 나오지만, 네트워크를 탄다면 비약적으로 증가한다.

    데이터가 왜이리 많은지 처음엔 그저 몰랐었다

    그럼 왜 이런 불필요한 데이터를 리포지터리를 통해 가져오게 되었는지 살펴보니, 데이터 이관과 관련된 일회성 배치가 참조하던 기능인 것을 알게되었다. 저렇게 많은 데이터를 애플리케이션으로 들고와 is_using이 true인 데이터만 필터링하여 호텔에서 제공하는 어메니티 정보로 사용하도록 도메인 코드가 짜여져 있었다. 왜 저런 삽질을 하나 싶지만 뭔가 그 당시에 이유가 있었겠지... 아무튼 호텔 상세정보 조회 API에서 원하는 것은 현재 호텔에서 제공하는 어메니티 목록이고, 해당 도메인 객체에서 제공하는 오퍼레이션의 목적도 현재 유효한 어메니티 집합이다. 굳이 모든 데이터를 가져와 필터링하지 않고 쿼리 단에서 필요한 정보인 hotel_id와 매칭되고 is_using = true인 조건의 행들만 조회하면 된다. 이 동작의 일관성을 보장하기 위해 테스트 코드도 (없다면) 붙이고 해야 하는데, 테스트까지 다루기엔 내 블로깅 실력이 부족하다ㅠ

    개선한 쿼리

    SELECT a.name FROM hotel_amenity ha 
    JOIN amenity a ON ha.amenity_id  = a.id 
    WHERE 
    hotel_id = 77
    AND is_using = true;

    불필요한 조회 필드를 제거하고, 관심없는 데이터인 is_using = false 행들도 결과에 나오지 않게 하였다.

    쿼리 튜닝?으로 속도 개선

    로컬 환경이 아닌 원격 DB에 접속하여 실행해보면 5~10ms 정도의 패치 속도는 떴었던 것 같다. 어쨌든 중요한 건 쿼리 실행속도는 거의 변함 없지만, 조회할 필드를 줄이고 행을 줄일때마다 네트워크 패치 속도가 크게 감소하였고 전체 조회속도가 크게 개선되었다.

    dev 환경에 적용하고 jmeter로 약식 성능테스트를 통해 API 응답 시간이 많이 단축된 것을 확인하고, 운영서버에도 적용하였다. 이틀 정도 모니터링하며 핀포인트의 여러 성능 지표들이 좋아졌음을 확인하였고, 더 이상 문제의 쿼리로 API 처리 시간을 다 잡아먹지도 않았다.

    개선하여 느낀점

    이번 리팩터링으로 3가지 교훈을 얻었다.

    1. 기본기에 충실해야 한다. 가끔 불필요한 필드 몇개 더 쓴다고 데이터 몇개 더 얹는다고 기능적으로 결함이 없으면 개의치 않았던 적이 있다. 하지만 성능적으로 차이가 크게 벌어질 수 있는 일이고, 이는 고객에게 좋지 않은 사용 경험을 제공하고 그만큼 돈 벌 기회를 놓칠 수 있다.
    2. 메서드 재사용이 편한 만큼 다른 부작용이 나타날 수 있다. 블랙박스 테스트처럼 인터페이스에 따른 결과만 믿고 내부 구현을 무시한채 오퍼레이션을 가져다 쓰면 이번처럼 대가를 치를 수 있다.
    3. 2번과 비슷한데 쿼리 또한 실행계획만 믿어서는 안된다. 실제 조회되는 데이터가 무엇인지 속도가 얼만큼인지 확인해야 한다. 물론 개발/테스트 환경과 운영 환경에서의 차이도 확인해야 한다. 개발 환경에서 빛의 속도로 답을 주던 쿼리가 운영 환경에서 슬로우 쿼리가 될 수 있기 때문이다.

    댓글

Designed by Tistory.