본문 바로가기
IT/기타

[IT] 주니어 백엔드 개발자가 반드시 알아야 할 실무지식 - 2장: 느려진 서비스, 어디부터 봐야 할까

by 저당단 2026. 8. 23.

서비스가 느려졌을 때

  • 응답 시간은 2가지로 나누어 측정
    • TTLB(Time to First Byte): 응답 데이터 중 첫 번째 바이트가 도착할 때까지 걸린 시간
    • TTLB: 마지막 바이트가 도착할 때까지 걸린 시간
    • 데이터 크기가 크거나 네트워크 속도가 느리면 두 값의 차이가 커질 수 있음
  • 전체 처리 시간: API 연동 (외부 + 내부) + DB 연동 + 로직 수행
    • 보통 API와 DB 연동이 대부분이므로 최적화할 땐 여기에 집중한다.
  • 처리량: TPS(transition per second), RPS(request per second)
    • TPS: 시스템이 처리할 수 있는 최대 요청 수
    • 서버가 1초에 5개의 요청을 성공적으로 처리했다면 TPS는 5.
    • 개발자 도구에서는 단일 요청의 지연 시간만 확인 가능하므로, 외부 도구(오픈소스)를 이용해야 함.
    • Scouter: 가장 흔하게 쓰이고 가벼움. 실시간 그래프 기능으로 모니터링 가능.
    • Pinpoint: 네이버에서 만들었으며 MSA 환경에서 좋음.
    • 더 정확한 TPS를 알고 싶다면 엘라스틱서치에 접근 로그를 수집한 뒤 집계.
※ 엘라스틱서치(Elasticsearch): 분산형 오픈소스 검색 및 분석 엔진.
리눅스의 locate, 윈도우의 everything과 같은 역색인(Inverted Index) 구조로,
데이터를 추가할 때마다 미리 색인 데이터를 만들어 둔다.
이 색인을 통해 데이터를 빠르게 검색한다.
단순 텍스트 일치 검색뿐만 아니라 형태소 분석, 오타 교정, 연관도 순위, 초성 검색도 지원.
  • 성능을 개선
    • 모니터링 도구와 로그를 이용해 병목 지점을 찾아냄. 보통 DB나 API 연동 과정에서 발생함.
    • 수직 확장
      • 급한 불을 끄는 방법. 서버의 스펙을 늘리는 것이다.
      • CPU, 메모리, 디스크(HDD->SSD) 등의 자원을 확장시킨다.
        • 필자의 회사에서도 최근 온프레미스 서버에서 문제가 발생했는데, OS가 HDD에 설치되어 있는 게 원인이었다.
      • 즉각적인 효과를 얻을 수 있지만 트래픽이 지속적으로 증가하면 또 성능 문제가 발생함.
      • 비용이 많이 들고 아무리 좋은 스펙이라도 한계는 있기 때문에, 이 경우 서버 대수 자체를 늘려야 한다.
    • 수평 확장
      • 서버를 추가로 투입해 늘리는 것.
      • 서버가 두 대 이상이면 로드 밸런서가 필요하다.
        • 로드 밸런서: 서버의 트래픽을 골고루 분산시켜 주는 장치 또는 기술.
        • 정적 방식에는 라운드 로빈(순차적으로 분배), IP 해시(동일한 클라이언트는 동일한 서버로) 방식이 있다.
        • 동적 방식은 현재 상태에 따라 트래픽을 분산한다.
      • 수평 확장을 하기 전에 병목 지점을 정확히 파악해야 한다.
        • DB에서 성능 문제가 발생하는데 서버를 추가 투입하면, DB를 사용하는 서버가 더 늘어나므로 DB 커넥션 풀만 더 잡아먹고 따라서 부하는 더 악화된다.
    • DB 커넥션 풀 크기
      • DB 연결을 관리하는 커넥션 풀 크기를 잘못 설정하면 성능에 큰 영향을 줌.
      • 필자의 회사에서 개발하는 서비스도 트래픽이 몰린 날 이것 때문에 서버가 터졌음.
      • 커넥션 풀 크기가 최대 5고, 쿼리를 실행하는 데 0.1초가 걸린다면
        • 1초에 처리할 수 있는 요청 수는 50.
        • 1~5번째 요청까지는 바로 처리하고 6번째 요청부터는 큐에 들어감.
        • 50개의 요청까지는 처리시간이 0.1초부터 1초 사이에 걸리고, 그 이상으로 들어오면 대기한다.
        • HikariPool 디폴트 값 기준으로 30초를 넘게 대기하면 커넥션 타임아웃 에러.
        • 이때 커넥션 풀 크기를 50으로 늘린다면, 50개까지는 모든 요청을 0.1초 안에 처리 가능.
      • 트래픽에 따른 조절
        • 트래픽이 높은 시간대에는 최대 크기로 확장하고 낮은 시간대에는 최소 크기로 유지하기
        • 수평 확장과 비슷하게, 풀 크기를 무턱대고 높이면 DB에 가해지는 부하가 더 커져 오히려 실행 시간이 증가함.
    • 커넥션 대기 시간
      • 대기 시간을 짧게 설정하면 커넥션 풀이 모두 사용 중일 때 '일시적 오류' 같은 응답을 빠르게 보여줄 수 있음
      • 긴 시간 무응답보단 빠르게 에러를 보여주는 게 나을 수 있음
      • 또한 빠르게 응답하면 서버의 부하가 증가하는 걸 방지
    • 최대 유휴 시간
      • 30분이면 30분 이상 사용되지 않은 커넥션은 종료되어 풀에서 제거됨
      • DB 비활성화 유지 시간보다 짧게 설정하면 DB가 연결을 끊기 전에 커넥션 제거 가능
      • 무한대로 설정하지 않는 게 좋음
    • 서버 캐시
      • DB 서버를 확장하지 않고도 응답 시간과 처리량을 개선할 때 씀
      • 필자의 회사도 서비스 홈 추천 화면에서 조회가 느려서 캐시를 사용, 성능을 개선했음.
      • 프론트의 TanStack Query 등에서 쓰는 캐시와 구별하기.
      • 적중률(hit rate)
        • 캐시에 존재한 건수/캐시에서 조회를 시도한 건수 로 계산.
        • 캐시에 최대한 많은 데이터를 저장하면 적중률이 높아짐.
        • 하지만 메모리 용량에도 한계가 있기에 캐시도 삭제가 필요. 
        • 캐시 삭제 규칙은 LRU, LFU, FIFO 등이 있다.
      • 로컬 캐시/리모트 캐시
        • 로컬 캐시(인-메모리 캐시)
          • 서버 프로세스(앱)와 동일한 메모리를 캐시 저장소로 사용.
          • Caffeine(자바), go-cache(고랭), node-cache(Node.js) 등이 있음.
          • 속도가 빠르고 단순하지만 데이터 크기에 제한이 있고 서버 프로세스를 재시작하면 캐시도 초기화됨.
        • 리모트 캐시: 별도 프로세스
          • 로컬 캐시와 정반대의 장단점.
          • Redis가 대표적.
          • 많은 상품 상세 데이터를 저장해야 하는 쇼핑몰 사이트라든가 배포 빈도가 높은 서비스라면 사용해야 함.
      • 캐시 사전 적재
        • 푸시 알림 등으로 사용자가 동시에 몰릴 상황을 대비해 미리 캐시에 저장해둔다.
        • 요금 푸시 알림을 발송했을 때 사용자의 요금 정보들을 캐시에 저장해두면, 캐시 적중률은 99%에 달하게 된다.
      • 캐시 무효화
        • 사용자가 갱신되기 전 데이터를 확인할 수 있으므로 캐시 무효화 시간을 적절히 설정해 두어야 한다.
        • 가격 정보나 게시글 내용처럼 민감한 데이터는 즉시 캐시 무효화.
        • Spring 기준으로는 @CacheEvict, FastAPI 기준으로는 fastapi-cache2를 사용해 캐시를 무효화할 수 있다.
        • 그러나 위 방법으로는 멀티서버라면 자신의 RAM의 데이터는 지웠지만 다른 서버의 메모리는 못 지운다. 이때는 Redis를 연동해야 함.
      • 가비지 컬렉터와 메모리 사용
        • 자바, Go, 파이썬 등은 GC를 사용
          • GC는 메모리를 관리해줌. 이 때문에 직접 메모리를 관리하지 않아도 돼서 보안 측면에서도 좋음.
          • 그러나 GC가 실행되는 동안 앱의 실행이 일시 중단됨 (Stop-The-World)
          • 디스코드는 서버 구현에 Go를 쓰다가 성능 문제로 GC가 없는 러스트로 갈아탐.
        • 스트림을 활용하면 파일 처리 과정에서 필요한 메모리 크기를 줄일 수 있음
          • ex) 한 번에 읽지 않고 수KB씩 끊어서 읽게 하기
          • 필자도 1GB 짜리 데이터를 한번에 힙에 올려서 OOM이 발생한 코드를 본 경험이 있음
      • 응답 데이터 압축
        • HTML, CSS, JS, JSON 등 텍스트로 구성된 응답은 gzip으로 압축하면 데이터 전송량을 크게 줄일 수 있음
        • jpeg 등 이미 압축한 데이터는 다시 압축해도 효과가 없다.
        • 웹 브라우저는 Accept-Encoding 헤더를 통해 사용 가능한 압축 알고리즘을 서버에게 알림.
      • 정적 자원 캐시
        • 클라이언트 브라우저 캐시를 이용해 정적인 페이지(상품 상세 등)의 데이터를 저장한다.
        • Cache-Control이나 Expires 헤더를 이용해 클라이언트가 응답 데이터를 저장해둘 수 있도록 설정 가능.
        • 만약 이미지의 응답 헤더에 Cache-Control: max-age=60 이 있다면 60초 이내엔 다시 불러오지 않음.
      • 정적 자원 CDN(Content Delivery Network)
        • 브라우저 캐시는 브라우저 단위로 동작하기 때문에 동시에 많은 사용자가 한번에 접속하면 많은 양의 JS, CSS를 전송하는 것이 불가피하다.
        • 이를 해결하기 위해 CDN을 사용.
        • 사용자는 오리진 서버 대신 CDN 서버에 접속하고, CDN 서버에 요청한 콘텐츠가 없으면 오리진 서버에서 읽어와 제공. 오리진에서 읽어온 데이터는 캐시에 저장
        • CDN은 여러 지역에 서버를 둠. 사용자는 가까운 서버에 연결해 콘텐츠를 다운로드함.
      • 대기 처리
        • 티켓팅처럼 대규모 트래픽이 몰리는 경우
        • 대규모 트래픽 처리 비용은 높으므로 한 번에 처리하는 트래픽을 제한한다.
        • 동시에 수용할 사용자 수를 제한하고 나머지 사용자를 대기 처리하는 합리적인 방식.