성능 문제는 느리다는 느낌에서 시작하지 않는다
서비스가 느리다는 말은 문제의 출발점일 뿐이다. 개발자가 먼저 해야 할 일은 "무엇이 얼마나 느린가"를 숫자로 바꾸는 것이다. 성능 최적화는 감으로 코드를 고치는 일이 아니라, 측정하고 가설을 세우고 검증하는 과정이다.
예를 들어 사용자가 "검색이 느리다"고 말했을 때 원인은 다양하다. 검색 API 자체가 느릴 수도 있고, 데이터베이스 쿼리가 느릴 수도 있다. 외부 인증 서버 응답이 늦거나, 브라우저에서 큰 JSON을 렌더링하느라 시간이 걸릴 수도 있다. 단순히 코드 몇 줄을 바꾸는 것으로 해결되지 않는다.
성능을 다룰 때는 먼저 기준선을 잡아야 한다. 현재 평균 응답 시간은 얼마인지, p95 응답 시간은 얼마인지, 초당 요청 수는 어느 정도인지, 오류율은 증가했는지, 배포 전후 어떤 지표가 바뀌었는지를 확인한다. 기준선이 없으면 개선도 증명할 수 없다.
여기서 p95는 전체 요청 중 95%가 이 시간 안에 끝났다는 뜻이다. 평균 응답 시간이 100ms여도 일부 사용자가 3초씩 기다린다면 실제 체감은 나쁠 수 있다. 그래서 실무에서는 평균뿐 아니라 p95, p99 같은 꼬리 지연 시간을 함께 본다. 소수의 느린 요청이 결제, 로그인, 검색 같은 핵심 기능에서 발생하면 서비스 신뢰도에 큰 영향을 준다.
AI 생성 이미지
지연 시간, 처리량, 동시 접속 수는 서로 다르다
성능 지표는 비슷해 보이지만 의미가 다르다. 이를 구분하지 못하면 잘못된 최적화를 하게 된다.
지연 시간은 하나의 요청이 시작해서 끝날 때까지 걸리는 시간이다. 사용자가 버튼을 누른 뒤 응답을 받기까지의 시간으로 이해하면 쉽다. 로그인 API가 200ms 걸리는지, 2초 걸리는지가 여기에 해당한다.
처리량은 단위 시간 동안 시스템이 처리할 수 있는 작업량이다. 보통 초당 요청 수, 초당 메시지 처리 수, 초당 쓰기 건수 같은 형태로 표현한다. 지연 시간이 낮아도 처리량이 낮으면 많은 사용자가 몰릴 때 대기열이 쌓인다.
동시 접속 수는 같은 시점에 시스템과 연결되어 있거나 요청을 보내는 사용자 수다. 동시 접속자가 많다고 항상 처리량이 높은 것은 아니다. 채팅 서버처럼 오래 연결을 유지하는 서비스는 동시 접속 수가 중요하고, 짧은 API 요청을 빠르게 처리하는 서비스는 처리량과 지연 시간이 더 중요할 수 있다.
자원 사용률은 CPU, 메모리, 디스크, 네트워크 같은 시스템 자원이 얼마나 사용되고 있는지를 나타낸다. CPU 사용률이 95%라면 계산 병목을 의심할 수 있고, 메모리가 계속 증가한다면 누수나 캐시 증가를 의심할 수 있다. 디스크 I/O 대기가 높으면 파일이나 데이터베이스 접근이 병목일 수 있고, 네트워크 전송량이 한계에 가까우면 응답 크기나 외부 호출 구조를 봐야 한다.
중요한 점은 이 지표들이 서비스 목표와 연결되어야 한다는 것이다. 관리자용 배치 화면은 몇 초 느려도 괜찮을 수 있지만, 결제 승인 API는 짧은 지연 시간과 낮은 오류율이 중요하다. 실시간 알림 시스템은 지연 시간이 중요하고, 로그 수집 시스템은 높은 처리량과 안정적인 적재가 더 중요할 수 있다. 좋은 성능 목표는 "빠르게 만들자"가 아니라 "p95 응답 시간을 300ms 이하로 유지하고, 초당 1,000건의 요청에서 오류율 0.1% 이하를 유지한다"처럼 측정 가능해야 한다.
병목은 한 계층에만 있지 않다
성능 문제를 분석할 때 가장 먼저 할 질문은 "시스템이 무엇을 기다리고 있는가"다. CPU를 쓰며 계산 중인지, 메모리를 확보하지 못하는지, 디스크 응답을 기다리는지, 네트워크 너머의 다른 시스템을 기다리는지 구분해야 한다.
CPU 병목은 계산 작업이 많거나, 비효율적인 알고리즘이 실행되거나, 직렬화와 암호화 같은 비용이 큰 작업이 몰릴 때 발생한다. 이때는 CPU 사용률이 높고, 프로파일러에서 특정 함수가 많은 시간을 차지한다. 예를 들어 요청마다 큰 배열을 정렬하거나, 중첩 반복문으로 권한 목록을 비교하는 코드가 있다면 데이터가 늘면서 CPU 사용량이 급격히 증가할 수 있다.
메모리 병목은 객체를 너무 많이 만들거나, 필요 없는 참조가 남아 있거나, 캐시가 제한 없이 커질 때 발생한다. GC가 있는 언어에서는 할당량이 많아질수록 가비지 컬렉션 시간이 늘고, 일시 정지 시간이 응답 지연으로 드러날 수 있다. 메모리가 부족해 스왑이 발생하면 CPU가 빠르더라도 전체 서비스는 급격히 느려진다.
디스크 병목은 파일 읽기·쓰기, 데이터베이스 접근, 로그 기록, 임시 파일 생성에서 자주 발생한다. 특히 작은 랜덤 I/O가 많으면 비용이 커진다. 데이터베이스에서 인덱스 없이 큰 테이블을 스캔하거나, 요청마다 동기적으로 파일을 기록하면 디스크 대기 시간이 응답 시간에 그대로 반영된다.
네트워크 병목은 외부 API, 데이터베이스 연결, 캐시 서버, 메시지 브로커와 통신할 때 발생한다. 네트워크는 로컬 함수 호출보다 훨씬 느리고 실패 가능성도 높다. DNS 지연, TCP 연결 수립, TLS 핸드셰이크, 패킷 손실, 원격 서버 부하가 모두 지연 시간에 포함된다. 내부 코드가 빠르더라도 요청 경로에 외부 호출이 5개 있다면 전체 지연 시간은 그 호출들의 합과 대기 시간에 좌우된다.
실무에서는 한 번에 하나씩 범위를 좁힌다. 먼저 전체 응답 시간이 어디서 늘어나는지 본다. 애플리케이션 로그에서 처리 시간이 긴 구간을 찾고, 메트릭으로 CPU·메모리·디스크·네트워크 상태를 확인한다. 데이터베이스 쿼리 시간이 길면 실행 계획과 인덱스를 보고, 외부 호출이 길면 타임아웃과 재시도 정책을 확인한다. 병목을 찾기 전까지는 최적화하지 않는 것이 원칙이다.
근거를 수집하는 네 가지 도구
성능 분석에는 로그, 메트릭, 프로파일링, 트레이싱이 함께 사용된다. 각각 보는 관점이 다르다.
로그는 특정 사건을 기록한다. 어떤 요청이 들어왔는지, 어떤 사용자였는지, 어떤 쿼리를 실행했는지, 예외가 어디서 발생했는지를 확인할 수 있다. 성능 분석용 로그에는 시작 시간, 종료 시간, 처리 시간, 요청 ID를 남기는 것이 좋다. 요청 ID가 있으면 여러 서버와 컴포넌트에 흩어진 로그를 하나의 흐름으로 묶을 수 있다.
메트릭은 시간에 따른 숫자 흐름이다. CPU 사용률, 메모리 사용량, 요청 수, 오류율, 응답 시간, 큐 길이, 캐시 적중률 같은 값이 여기에 해당한다. 메트릭은 장애 순간의 전체 상태를 빠르게 보여 준다. 특정 배포 이후 p95 지연 시간이 증가했는지, 트래픽 증가와 함께 데이터베이스 연결 수가 포화되었는지 확인할 수 있다.
프로파일링은 프로그램 내부에서 시간이 어디에 쓰이는지 분석하는 방법이다. CPU 프로파일러는 어떤 함수가 실행 시간을 많이 차지하는지 보여 주고, 메모리 프로파일러는 어떤 객체가 많이 할당되고 오래 남는지 보여 준다. 프로파일링 없이 코드를 추측으로 고치면 실제 병목이 아닌 부분을 최적화하기 쉽다. 실행 시간의 대부분을 데이터베이스 대기에서 쓰고 있는데 문자열 처리 코드를 고치는 식이다.
트레이싱은 하나의 요청이 여러 서비스와 저장소를 거치는 경로를 추적한다. 마이크로서비스나 분산 시스템에서는 요청 하나가 API 서버, 인증 서버, 주문 서버, 결제 서버, 데이터베이스, 메시지 큐를 지나갈 수 있다. 트레이싱은 각 구간을 span이라는 단위로 나누어 어디서 시간이 쓰였는지 보여 준다. 전체 요청이 2초 걸렸을 때 그중 1.5초가 결제 서버 호출인지, 데이터베이스 쿼리인지, 큐 대기인지 확인할 수 있다.
이 네 가지는 서로 대체재가 아니다. 로그는 사건의 맥락을, 메트릭은 전체 추세를, 프로파일링은 코드 내부 비용을, 트레이싱은 요청 경로를 보여 준다. 성능 문제를 안정적으로 해결하려면 이 정보를 함께 읽어야 한다.
대표적인 최적화 방법들
캐싱은 자주 쓰는 결과를 더 빠른 저장소에 보관하는 방법이다. 같은 상품 정보를 매번 데이터베이스에서 읽지 않고 메모리 캐시나 Redis에 저장하면 응답 시간이 줄어든다. 하지만 캐시는 항상 무효화 문제가 따라온다. 원본 데이터가 바뀌었는데 캐시가 오래된 값을 반환하면 성능은 좋아져도 정확성이 깨진다. 따라서 캐시 만료 시간, 삭제 정책, 캐시 적중률, 데이터 일관성 요구사항을 함께 설계해야 한다.
배치 처리는 작은 작업을 모아 한 번에 처리하는 방식이다. 데이터베이스에 1,000번 insert하는 대신 bulk insert를 사용하거나, 외부 API를 건별로 호출하지 않고 묶어서 호출하면 왕복 비용을 줄일 수 있다. 로그 전송, 통계 집계, 알림 발송처럼 즉시 처리하지 않아도 되는 작업에 효과적이다. 다만 배치 크기가 너무 커지면 한 번 실패했을 때 재처리 비용이 커지고, 개별 작업의 지연 시간이 늘어날 수 있다.
비동기 처리는 사용자가 기다릴 필요가 없는 작업을 뒤로 미루는 방식이다. 주문 요청에서 주문 생성과 결제 승인은 즉시 처리해야 하지만, 이메일 발송이나 추천 데이터 갱신은 메시지 큐에 넣고 나중에 처리할 수 있다. 이렇게 하면 사용자 응답 시간은 줄어든다. 대신 작업이 중복 실행될 수 있으므로 멱등성, 재시도, 실패 큐, 모니터링이 필요하다. 비동기는 성능 도구이면서 동시에 운영 복잡도를 늘리는 선택이다.
자료구조와 알고리즘 변경은 가장 근본적인 최적화가 될 수 있다. 매 요청마다 리스트 전체를 순회해 사용자를 찾는 코드는 데이터가 적을 때는 문제없지만, 사용자가 늘면 병목이 된다. 해시 맵으로 바꾸면 조회 비용을 크게 줄일 수 있다. 데이터베이스에서도 마찬가지다. 자주 조회하는 조건에 맞는 인덱스를 추가하면 전체 테이블 스캔을 피할 수 있다. 하지만 인덱스는 쓰기 비용과 저장 공간을 늘린다. 최적화는 항상 비용을 다른 곳으로 옮기는 행위다.
응답 크기를 줄이는 것도 중요하다. 필요 없는 필드를 제거하고, 페이지네이션을 적용하고, 압축을 사용하면 네트워크 전송 시간과 클라이언트 처리 비용이 줄어든다. 특히 모바일 환경에서는 네트워크 상태와 기기 성능이 다양하므로 서버 내부 처리 시간만 빠르다고 충분하지 않다.
최적화는 반복 가능한 절차여야 한다
성능 개선은 다음 순서로 진행하는 것이 안전하다. 먼저 문제를 수치로 정의한다. 다음으로 병목 위치에 대한 가설을 세운다. 그다음 로그, 메트릭, 프로파일러, 트레이싱으로 근거를 수집한다. 변경은 가능한 한 한 번에 하나씩 적용한다. 마지막으로 같은 조건에서 다시 측정해 개선 여부를 확인한다.
이 절차가 중요한 이유는 성능 문제가 쉽게 착시를 만들기 때문이다. 개발 환경에서는 빨랐던 코드가 운영 데이터에서는 느릴 수 있다. 로컬에서는 네트워크 지연이 없지만 운영에서는 외부 API 지연이 크다. 평균 응답 시간은 좋아졌지만 p99가 나빠질 수도 있다. CPU 사용률은 낮아졌지만 메모리 사용량이 늘어 장애 위험이 커질 수도 있다.
최적화에는 우선순위가 필요하다. 전체 실행 시간의 1%를 차지하는 코드를 10배 빠르게 만들어도 사용자는 거의 차이를 느끼지 못한다. 반대로 전체 시간의 70%를 차지하는 데이터베이스 쿼리를 절반으로 줄이면 서비스 체감은 크게 달라진다. 그래서 병목을 찾아야 한다.
성능 최적화의 목표는 모든 코드를 가장 빠르게 만드는 것이 아니다. 서비스 목표를 만족하는 수준까지 안정적으로 만드는 것이다. 지나친 최적화는 코드 복잡도를 높이고 버그 가능성을 키운다. 캐시를 많이 넣으면 일관성 문제가 생기고, 비동기를 늘리면 실패 처리와 추적이 어려워진다. 고성능 자료구조를 직접 구현하면 유지보수 비용이 커질 수 있다.
좋은 개발자는 빠른 코드를 쓰는 사람이라기보다, 어떤 비용이 어디서 발생하는지 설명할 수 있는 사람이다. 지연 시간, 처리량, 자원 사용률을 보고 병목을 좁히고, 측정 가능한 목표를 세우며, 개선 결과를 숫자로 확인한다. 성능 분석은 특별한 순간에만 하는 작업이 아니라 설계와 운영 전반에 필요한 사고법이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.