토니모리 공식몰 API 응답시간 개선기 1차(성능 최적화)

JUNHWAN JANG
7 min readNov 2, 2024

--

페이지 로드 및 API 속도 개선하는 네 가지 방법

들어가며

안녕하세요. 토니모리 개발운영팀 장준환입니다. 토니모리 공식몰 API 성능 개선을 진행하면서 겪은 경험을 공유하려고 합니다. 저희와 비슷한 고민을 가진 분들께 도움이 되었으면 합니다.

원인 정리

  • 페이지 로드에 매우 오랜 시간이 소요

개선점 정리

  1. API에 단일 책임 부여하기
  2. 배치 작업을 통한 테이블 병합으로 성능 최적화하기
  3. 부수 작업은 비동기로 분리하기
  4. SQL 실행 계획 최적화해보기(아래 링크)

=> 토니모리 공식몰 API 응답 시간 2차 개선기 (SQL 실행계획 최적화 및 index)

문제상황

상품 상세 페이지 혹은 픽스토어 최근이용 매장 페이지 등에 접속할 경우 로딩에만 3000ms 이상 소요되고 있었습니다.

문제는 두 페이지의 경우, 페이지 로드가 비정상적으로 길어 사용자는 응답시간만큼 기다려야한다는 점이있습니다. 이러한 대기시간은 UX(사용자 경험)에 직접적인 영향을 미친다고 생각하여 API 별 책임 분리가 필요하다 판단하였습니다.

페이지 로드 시간(초)과 고객 이탈률(%) 간의 상관관계

Amazon과 Walmart 같은 대형 전자상거래 사이트 연구에 따르면, 로딩 시간이 1초 지연될 때마다 전환율이 7% 이상 감소할 수 있다고 합니다.

공식몰 상품 상세 페이지 LCP
토니모리 공식몰 상폼 상세 페이지 응답시간(1.81s)
올**영 상폼 상세 페이지 응답시간(436.49ms)
쿠* 상폼 상세 페이지 응답시간(567.34ms)

다른 이커머스 플랫폼에 비해 3~5배 정도 느리게, 상품 상세 페이지를 제공하고 있습니다. 시스템 부하가 발생한다면 해당 격차는 더욱 더 벌어지는 상황입니다.

공식몰 응답시간 분석

평소 요청대비 응답시간

사용자 요청량에 따른 응답시간
응답시간이 오래 걸린 애플리케이션 목록

대부분 상품 상세 페이지 로드 요청에서 1초 이상 소요되는 것을 확인할 수 있었습니다. 해당 API는 동기 형태로 정보 조회와 HTML 랜더링까지 진행하고 있어 페이지 응답시간이 매우 길어지고 있었습니다.

기획전 당시 요청대비 응답시간

사용자 요청량에 따른 응답시간
응답시간이 오래 걸린 애플리케이션 목록

하루동안, 대략 수 천개 이상의 요청 응답이 2초 이상 소요되었습니다. 10시에는 최대 18초의 응답 지연이 발생하였습니다. 현재 구조에서 드라마틱한 변화는 어려웠기에 점진적인 개편이 필요하다 생각하였습니다.

문제 원인

문제 1. 하나의 API 요청에서 너무 많은 정보를 가져오고 있네!

토니모리 공식몰 상품 상세 페이지

현재 자사몰은 SSR 방식으로 상품 상세 페이지 로드 API를 호출할 경우, 페이지에 노출되는 모든 정보를 조회해서 HTML로 랜더링하여 완성된 HTML 값을 전달해주는 형태입니다.(상품 상세 정보, 사은품 정보, 최대 혜택가 정보, 상품 옵션 정보, 리뷰 정보, QnA 정보, 광고 정보 등)

상품 상세 페이지 로드 API를 통해 너무 많은 정보를 조회하고 있기에 매우 높은 응답시간이 소요되고 있었습니다.

문제 2. 도대체 몇 개의 테이블을 조인하는거야?

상품 상세 페이지 리뷰 영역

통합 리뷰 정보를 제공하기 위해 서로 다른 테이블에 있는 리뷰 데이터들을 JOIN 해서 가져오고 있었습니다. 리뷰 페이지에 여러 필터가 존재하여 데이터 조회 후 정렬까지 필요한 형태라 많은 자원을 사용하고 있었습니다.

문제 3. 특정 API의 경우, 외부 DB 서버에 의존하고 있네?

픽스토어 > 최근이용 매장 페이지 로드 시간

픽스토어 > 최근이용 매장 페이지 로드 시간으로 무려 4000ms 가 소요되고 있습니다. 최근이용 매장 정보는 외부 포스 DB에 존재하기에 조회 작업에 많은 자원이 사용되고 있었습니다. 현실적으로 포스 DB 이관은 불가능한 상황이기에 조금이라도 빠르게 소비자에게 서비스를 제공해야 한다 생각하였습니다.

목표

이러한 문제를 해결하기 위해서 아래와 같은 목표를 설정했습니다.

  1. 모든 API 응답속도를 400ms 이하로 줄이자!
    - 100ms 이하로 줄이는 것은 미래에
  2. 모든 API 응답 값은 가볍게!
    - 페이지 로드시 정보 제공 최소화!
    - 사용자 행위 정보 제공 빠르게!
  3. 동기/비동기 작업 분류하자!
    - API 응답 객체 생성 외 로직은 비동기로 처리!

(참고) 권장되는 API 응답 시간 기준 예시

1. 사용자 인터페이스(UI) 연동 API

- 100~200ms 이하: 즉각적인 피드백을 제공해야 하는 경우 (예: 검색, 추천 시스템).

- 200~500ms: 사용자가 느낄 수 있는 지연이 거의 없는 반응성.

- 500ms~1초: 수용 가능한 최대 지연으로, 일부 사용자 경험에 불편을 줄 수 있습니다.

2. 백엔드 간 서비스 API

- <200ms: 마이크로서비스와의 통신 시 응답 시간이 짧을수록 좋습니다. 특히 실시간 처리가 필요한 경우 권장됩니다.

- 200~500ms: 마이크로서비스 호출이 많아도 수용 가능한 지연 시간입니다.

- >500ms: 비동기 또는 대기열 처리로 전환하는 것이 권장됩니다.

3. 데이터 처리/배치 작업 API

- 1초 이상:
사용자가 직접 응답을 기다리지 않거나 비동기 요청으로 처리되는 경우, 수 초의 응답 시간도 허용됩니다.

  • API를 설계할 때 소비자에게 최소한의 데이터만 제공하도록 설계합니다. 데이터를 미리 제공할 필요는 없고 제공된 것처럼 빠르게 제공하면 됩니다. 사용하지 않는 데이터를 미리 제공하는 것은 자원 낭비이고 모든 것이 비용이기에 최적화를 해야 합니다.
  • API 응답 시간이 1초 이하가 소요되는 상황이 권장되기에, 응답시간이 1초 이상 소요되고 있는 API 를 분석 및 리팩토링 해야 합니다.

개선점 1. API에 단일 책임 부여하기

호출은 1번인데 과하게 정보를 가져오네?

AWS 클라우드에서 과도한 API 응답 데이터가 비용 증가에 미치는 영향

위 그래프에 따르면, 필요 이상으로 많은 데이터를 제공할수록 비용이 기하급수적으로 증가하는 경향이 있습니다. 빨간 선은 과도한 데이터 제공이 API 비용을 크게 높일 수 있음을 나타냅니다. 파란 점선은 최적의 데이터 제공 지점으로, 이 수준에서 불필요한 비용을 방지할 수 있습니다. 모든 데이터를 한꺼번에 제공하는 방식은 서버 자원과 네트워크 대역폭을 많이 소모하기 때문에 필요한 데이터만 제공하는 방식이 서버 자원을 효율적으로 사용하는데 도움이 됩니다. 이를 통해 서버는 더 많은 요청을 처리할 수 있고 불필요한 데이터를 전송하지 않아 트래픽 비용을 절약할 수 있습니다.

그렇기에 비용 최적화를 위해 페이지 로드 API는 최소한의 정보만 제공하도록 구성하고 그 외 API를 통해 요청에 알맞은 데이터를 제공해야 합니다.

과거 토니모리 공식몰 상폼 상세 페이지 응답시간(1.81s)

과거 공식몰 상품 상세 페이지 API는 상품 정보, 상품 상세, 쿠폰, 사은품, 리뷰, Q&A 등 페이지 구성에 필요한 모든 정보를 제공하고 있었습니다. 그렇기에 페이지 로드에 1.5s ~ 3.0s 가 소요되었고 느린 응답속도로 인해 많은 사용자분들이 불편을 느끼셨을거라 생각합니다.

현재 토니모리 공식몰 상폼 상세 페이지 응답시간(386ms)

따라서 페이지 로드 API에는 상품 상세 최소한으로 필요한 정보만 제공하도록 하였고, 그 외 추가 정보를 가져오는 API를 구성하여 데이터를 제공하도록 구성하였습니다.

토니모리 공식몰 상폼 상세 페이지 배포 전/후 응답시간

이러한 개선을 통해서 기존 1.5s ~ 3.0s 의 응답속도를 300ms ~ 800ms 로 대략 82% 정도 개선하였습니다.

개선점 2. 배치 작업을 통한 테이블 병합으로 성능 최적화

데이터베이스에서 복잡한 조인 쿼리를 자주 실행하면 시스템 리소스를 많이 소모하게 되어 성능 저하가 발생할 수 있습니다. 특히, 대용량 데이터를 가진 여러 테이블 간의 조인이 빈번하게 발생하는 경우, 이를 배치 작업을 통해 주기적으로 하나의 테이블에 병합하여 사용하는 것이 성능 면에서 유리할 수 있습니다. 이를 통해 실시간으로 조인을 실행하지 않아도 되므로, 조회 쿼리의 성능을 크게 향상할 수 있습니다.

과거 상품 리뷰 정보는 세 가지의 테이블로 구성되어 있었습니다. 상품 리뷰를 제공할 때마다 세 가지 테이블을 JOIN 하여 제공하기 때문에 많은 비용을 소요되었습니다. 그렇기에 스프링 배치를 통해 세 테이블의 데이터를 하나의 테이블로 통합하였고 리뷰 데이터를 하나의 테이블을 통해 제공함으로 시스템 자원 사용을 감소할 수 있었습니다.

리뷰 데이터 통합 배치 배포(빨간색 막대) 전, 후 API 응답시간

이러한 개선을 통해서 기존 50ms ~ 1000ms 의 응답속도를 50ms ~ 500ms 로 대략 50% 정도 개선하였습니다.

개선점 3. 부수 작업은 비동기로 분리하기

상품 상세 페이지 로드 API의 경우, 정보 제공에 있어 필수적이지 않은 작업(소비자 상품 방문 기록 저장)을 비동기로 실행함으로써 API 응답 시간을 단축했습니다. 기존에는 이러한 부수 작업들을 동기로 처리하고 있어 응답 시간이 지연되고 있었습니다. 해당 작업을 비동기로 전환함으로써 API 요청에 따른 작업시간을 최적화하여 사용자 경험을 개선할 수 있었습니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/* 컨트롤러: 상품 상세 페이지 로드 API */
@RestController
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping("/product/details")
public ProductDetailsResponse getProductDetails(@RequestParam("productId") Long productId) {

// 주요 비즈니스 로직: 상품 상세 정보 조회
ProductDetailsResponse productDetails = productService.getProductDetails(productId);

// 비동기 작업 실행: 방문 기록 로깅
productService.logProductClickAsync(productId);

return productDetails; // 주요 응답을 즉시 반환
}
}
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

/* 서비스: insertProductClickLog 비동기 처리 */
@Service
public class ProductService {

private final ShopProductDao shopProductDao;

public ProductService(ShopProductDao shopProductDao) {
this.shopProductDao = shopProductDao;
}

public ProductDetailsResponse getProductDetails(Long productId) {
// 상품 상세 정보 조회 로직
return shopProductDao.findProductDetailsById(productId);
}

public CompletableFuture<Void> logProductClickAsync(Long productId) {
// CompletableFuture를 사용하여 비동기 작업을 수행
return CompletableFuture.runAsync(() -> {
shopProductDao.insertProductClickLog(productId);
});
}
}

이러한 개선을 통해서 상품 상세 페이지 로드 API의 응답속도를 50ms 정도 감소시켰습니다.

마무리

아직 나아가야 할 길이 매우 멀다 생각하지만, 지금까지 토니모리 공식몰의 전반적인 API 응답속도를 개선하는 방법들에 대해 방법들에 대해서 소개해드렸습니다.

1.5s ~ 3.0s 를 보이던 응답 속도는 현재 300ms ~ 500ms 수준에서 안정적으로 유지되고 있습니다.

앞서 다룬 내용들이 기본적인 방법이지만 개인마다 그 안에서 얻으실 수 있는 관점은 매우 다양하다 생각합니다. 토니모리 개선 기록이 다른 분들의 성능 개선 작업에 조금이라도 도움이 되었으면 합니다.

과거의 코드는 모두 레거시라고 생각합니다. 이상적인 코드를 만들 수는 없겠지만 항상 긴장을 잃지 않고 임한다면, 조금이라도 이상향에 가까운 코드가 될 수 있다고 생각합니다. 여유를 즐기는 삶과 게으른 삶은 다른 것이다. 죽고 나면 그 때 실컷 잘 수 있다.(A life of leisure and a life of laziness are two things. There will be sleeping enough in the grave). 항상 긴장하시길,

--

--

JUNHWAN JANG
JUNHWAN JANG

No responses yet