
1. 서론
이번 회고는 저번 1편에 이어 모니터링 과정에 대해 정리하고, 리소스를 효율적으로 사용하게된 과정을 정리합니다. 사실 1편 회고에서는 몇가지 오류가 발생하거나 아무리 안좋은 인스턴스를 사용하더라도 TPS 가 저조한 상황이 있었습니다. 이번 2편 회고에서는 모니터링을 통한 TPS 측정 및 최적화 과정을 설명합니다.
저희팀은 컨텐츠에 대한 정보와 연관 명소 등 품질 높은 정보를 제공하기 위해 노력했습니다. 이렇게 구축된 DB를 활용하여 만들 수 있는 기능은 매우 많았고, 무슨 기능을 만들어야 킥일까? 생각하게 되었습니다. 사실 신규 서비스에선 물론, 금융업 서비스에서도 AI를 사용하는 등 AI는 선택이 아닌 필수가 되었다고 생각합니다. 따라서 저희 서비스 데이터를 AI에게 학습시켜 딸깍 만으로도 동선을 만들 수 있게한 서비스를 간단히 소개해 보려고합니다.
Nextjs가 고질적으로 TPS, RPS 지표가 좋지 않더라도 얼마든지 엔지니어링을 통해 극복할 수 있다고 생각합니다. 사실 저희팀은 많은 기능보단 클린 아키텍처와 포트폴리오에서 다룰 수 있을 정도의 디테일을 추구하자가 목표였습니다. vite개발과 병행했기 때문에 시간이 많지 않았고, 이렇게 하면 더 개선할 수 있겠다고 생각한 부분들을 정리하며 마무리 하겠습니다.
어쩌다 보니 서론은 존댓말로 작성했지만, 아래 회고 부분부터는 일기처럼 편한 말투로 정리하겠습니다. -_-
2. 페이지별 렌더링 전략

초기에 우당탕탕 nextjs로 마이그레이션한 당시 모든 컴포넌트가 SSR 방식으로 돌게 되었다. 사용자가 페이지를 이동할때마다 요청이 발생하고, 같은 페이지를 요청하더라도 모든 사용자들에게 페이지를 각각 생성하여 제공해줘야했다. 당연하게도 서버의 CPU부하가 관측되었고, 28TPS 에서 성능이 막히는 등의 문제가 있었다.
그렇다면 같은 페이지라면 미리 만들어둔 페이지를 그대로 제공해주면 되지않을까? 라는 생각으로 페이지 캐싱을 도입했다. Nextjs에는 여러 랜더링 전략을 제공하는데, 각 페이지별로 목적에 맞는 전략을 수립했다.
홈페이지 : 모달같은 사용자 동작 상태관리가 필요한 컴포넌트는
use-client키워드를 통해CSR로 동작하게 했다. 다만SEO가 중요했기 때문에CSR로 동작하게 할 순 없었고, 매번 업데이트되는 인기컨텐츠 순위가 있었기에 단순 캐싱된 페이지를 제공할 순 없었다. 따라서 아래같은 흐름으로 구현했다.TMDB API는 하루에 한번 데이터를 업데이트 한다. → 백엔드에서 스케줄러를 통해 DB를 하루마다 업데이트한다 → 인기컨텐츠 순위도 하루마다 업데이트된다. → 그럼 하루단위로 캐싱하고 revalidate 하자.
콘텐츠 페이지 : 동적 페이지인 콘텐츠 페이지는 우리 서비스에서 다루는 800개 가량의 콘텐츠를 표시해야했다. 사실 콘텐츠라는 요소는 시간이 지나도 변하지 않는 정보이고, 따라서
SSG방식으로 빌드시간에 캐싱해두는 방법도 좋다고 생각한다. 다만 우리팀은 인기콘텐츠를 위주로 사용자의 접근을 추천하고, 상위 60가지 콘텐츠를 사용자가 검색을 하지않더라도 포스터를 띄워 즉시 제공하고 있다. 모든 페이지를 빌드시간에 캐싱해두는 것은 빌드시간이 길어지고 저장공간이 낭비된다고 생각했다.따라서 사용자가 한번 요청하면 그때 생성하고, 그 이후부턴 생성된 페이지를 캐싱해두고 즉시 제공한다.
- 장소 페이지 :
SSR방식으로 동작하게 만들어, 8000개가 넘는 방대한 양의 페이지를ISR로 모두 캐시할 경우 발생하는 막대한 서버 저장 공간 낭비와 캐시 관리의 복잡성 문제를 근본적으로 피할 수 있었다. 리뷰같은 부분은CSR방식으로 동작하게 만들어 사용자의 실시간성을 보장했다. "매번 동적으로 만들고 싶다"는 명확한 요구사항을 완벽하게 충족시켜, 캐시된 데이터가 아닌 항상 최신의 장소 정보를 사용자에게 제공한다.

3. TPS 측정 과정

1편에서도 다룬적 있지만 지표를 개선하고 싶다면 인스턴스의 사양을 높이면된다. 하지만 엔지니어로서 모니터링을 통해 값싼 노드풀을 사용해도 안정적인 서비스를 구현하고 싶었다.
나는 초기엔 리소스를 이렇게 할당했었다.
- 워커 노드는 항상 2개를 띄워두고 pod 또한 항상 2개를 유지한다.
- pod당 cpu requests/limits = 170m/300m
- 트래픽이 몰리면 노드를 3개까지 확장하고, pod은 3개까지 확장한다.
나는 k8s를 돌리기 위한 최소의 인스턴스를 선택했다. 당연하겠지만 CPU리소스가 부족했고, k8s설정 pod들이 각 노드에서 차지하는 양만해도, 총 940m 에서 500m 의 리소스를 요청했고, 내 애플리케이션에 요청가능한 리소스는 440m 이 한계였다. 따라서 많은 모니터링을 해보았고, 사실 너무 많이 해서, 가장 마지막에 했던 모니터링과정을 다루고자 한다.
최종 구조
- 워커 노드는 항상 2개를 띄워두고 pod또한 항상 2개를 유지한다.
- pod당 cpu requests/limits = 200m/750m
- 트래픽이 몰리면 노드를 3개까지 확장하고, pod은 6개까지 확장한다. 노드당 2개 pod
150TPS를 목표로 400명의 가상유저가 1초에 150회의 강제적인 요청을 준다.
- 서비스가 놀고 있는상태

2. pod이 3개로 확장된 상태

3. pod이 6개까지 확장된 모습

4. 모니터링 결과

모니터링 결과
- 비록 9분의 테스트 였지만 모든 테스트를 통과했고 0.07초 내에 응답에 성공했다.
- 현실적인 1000명의 가상유저가 3~10초의 요청을 보낼때 0.05초 내에 응답에 성공했다.
4. 서버리스 AI동선추천 서비스 개발기

사실 AI동선 기능에 대한 인프라 구축 측면에서 많은 생각을 해보았다. 사실 백엔드에서 만들었다면 편했겠지만 프론트인 내가 만들어야 했기에 여러 제약 사항들이 있었다. 아래는 내가 고민했던 부분들이다.
- 직접 백엔드 저장소에서 구현한다.
- k8s환경내의 Nextjs에서 mysql 커넥션풀을 형성후 node로 구현한다.
- k8s환경내에 python pod을 띄워서 운영한다.
- 서버리스 함수로 구현한다.
선택에 대한 이유
- 프론트 인프라에서 백엔드의 db를 활용한 서비스는 괜찮지만 부트캠프 과정에서 백엔드의 영역에 직접적으로 관여하는게 맞을까? 하는 생각이 있어서 선택하지 않았다.
- 사실 페이지 랜더링과 k8s 실행만으로도 Nextjs풀에선 한계가 있었다.
- 비록 프론트를 위한 클러스터이지만 MSA구조 처럼 만들기 좋았지만, 인스턴스를 추가해야한다는 부담이 있었다.
- 서버리스 함수로 DB에 접근하기 위해선 여러 네트워크 설정이 필요했다. 또한 네트워크 지연시간들을 생각해보면 효율적이진 않지만 인프라 비용을 절약할 수 있고, 사실 가장 시간이 오래 걸리는 AI처리를 제외하곤 미미한 시간이었기 때문에 선택하게 되었다.
구현 기술 및 흐름
- 최적화
- Cold Start: 전역 변수를 활용한 JSON 데이터 로딩 및 AI 클라이언트 재사용.
- DB 쿼리:
IN절을 사용한 N+1 문제 해결 및 파라미터화 쿼리로 SQL Injection 방어. - 지리 계산: 2단계 필터링(예비 필터링 → 정밀 계산)으로 불필요한 Haversine 계산을 방지.
- 안정적인 AI 연동 설계
- Gemini AI에 명확한 JSON 스키마를 강제하고, 마크다운 블록 제거 등 예외 처리 수행
- Exponential Backoff (5s, 10s, 15s) 재시도 로직을 구현하여 일시적인 API 오류 대응
- 비용 효율적인 서버리스 아키텍처
- Cloud Functions의 오토스케일링(1-10)을 활용하여 유휴 비용을 최소화

15초 정도의 긴 시간이 걸렸기 때문에 사용자의 이탈을 방지하기 위한 다계층 progress bar를 도입했다.

5. 향후 확장 가능성 생각해보기
- AI 동선에서 15초란 시간은 프로그래스바가 있어도 꽤 긴시간이다. 또한 api사용량, 네트워크 비용등 모든 지표를 개선할 수 있는 방법으로 Redis를 도입하는 것을 생각해보았다. 서버리스 함수에서 DB IO를 줄이고, 빠른 응답시간을 확보할 수 있을 것이라 생각한다.
- Nextjs에서 페이지를 캐싱했다? 정도이지 디테일한 부분을 신경쓰지 못했다. vite또한 cloudfront를 사용한 정적배포를 하고 운영,관리 중에 있었기 때문에 시간이 부족했던것이 원인이었다. 사실 데이터가 거의 불변하는 페이지들을 캐싱했기 때문에 문제가 없다고 생각하지만 생각해보면 좋았겠다라는 부분들을 정리해보겠다.
- 페이지를 캐싱한다 → 컨테이너를 여러개 띄워서 운영중에 있는데, 그럼 사용자가 보게되는 캐싱 페이지도 차이가 생길 수 있겠다. → Redis를 도입해서 전역 store느낌으로 일관적인 캐싱 페이지를 사용자들에게 제공해주자.
- Nextjs에서 페이지를 뿌려주거나 캐싱 페이지를 응답하는 시간과 비용을 발생시키는것 보단 nginx등에서 만들어진 html을 서빙하는것이 훨씬 빠를것이라 생각한다. SSG, ISR로 캐싱된 페이지를 CDN에 올려두고 사용자들에게 제공하는것이 바람직하겠다 라는 생각을 했다.