성능은 빠른 코드보다 빠른 경험이다
성능을 이야기할 때 가장 먼저 정리해야 할 것은 "무엇이 느린가"이다. 개발자는 종종 코드 실행 속도만 떠올리지만, 사용자가 느끼는 성능은 더 넓다. 버튼을 눌렀을 때 화면이 멈추지 않는지, API 응답이 일정한지, 많은 사용자가 몰려도 주문이 실패하지 않는지까지 포함한다. 결국 성능은 개발자 관점의 실행 시간이 아니라 사용자 관점의 경험에 가깝다.
성능 개선은 감으로 하는 작업이 아니다. "아마 데이터베이스가 느릴 것 같다", "캐시를 넣으면 빨라질 것 같다"는 추측만으로 코드를 바꾸면 문제를 해결하지 못하거나 더 복잡한 장애를 만들 수 있다. 올바른 순서는 다음과 같다.
성능 목표를 정한다.
현재 상태를 측정한다.
병목을 찾는다.
한 가지 개선을 적용한다.
다시 측정해 효과를 확인한다.
예를 들어 "상품 목록 API가 느리다"는 말은 기준이 되기 어렵다. "상품 목록 API의 p95 응답 시간을 800ms 이하로 유지한다"처럼 측정 가능한 목표가 필요하다. 여기서 p95 응답 시간이 800ms라는 말은 전체 요청 중 95%가 800ms 안에 응답하고, 나머지 5%는 그보다 오래 걸릴 수 있다는 뜻이다. 평균 응답 시간만 보면 일부 사용자가 겪는 긴 지연을 놓칠 수 있으므로 p95, p99 같은 백분위 지표를 함께 보는 것이 좋다.
AI 생성 이미지
지연 시간, 처리량, 동시 사용자, 리소스 사용률
성능 지표는 서로 다른 질문에 답한다. 하나의 숫자로 서비스 상태를 판단하려 하면 원인을 잘못 짚기 쉽다.
지연 시간은 요청 하나가 완료되기까지 걸린 시간이다. API 서버라면 요청을 받은 시점부터 응답을 돌려준 시점까지의 시간이 된다. 사용자가 체감하는 속도와 가장 직접적으로 연결된다. 지연 시간은 평균뿐 아니라 p50, p95, p99를 함께 봐야 한다. p50은 보통 사용자의 경험이고, p95와 p99는 느린 사용자의 경험을 보여준다. 평균은 몇 개의 매우 느린 요청에 영향을 받거나, 반대로 일부 사용자의 심각한 지연을 숨길 수 있기 때문에 단독으로 보기 어렵다.
처리량은 일정 시간 동안 얼마나 많은 일을 처리하는지 나타낸다. 웹 서버에서는 RPS(Requests Per Second), 결제 시스템에서는 TPS(Transactions Per Second)처럼 표현한다. 처리량이 높아도 지연 시간이 길면 사용자는 느리다고 느낀다. 반대로 지연 시간이 짧아도 처리량 한계가 낮으면 트래픽이 늘 때 금방 막힌다.
동시 사용자는 같은 시점에 서비스를 사용하는 사용자 수다. 중요한 점은 동시 사용자 수와 요청 수가 같지 않다는 것이다. 사용자 1,000명이 접속해 있어도 모두가 매초 요청을 보내는 것은 아니다. 성능 테스트를 할 때는 "동시 접속자 수"뿐 아니라 사용자가 어떤 간격으로 어떤 요청을 보내는지까지 모델링해야 한다. 로그인만 해 둔 사용자와 검색·주문·결제를 계속 수행하는 사용자는 서버에 주는 부담이 다르다.
리소스 사용률은 CPU, 메모리, 디스크 I/O, 네트워크 I/O 같은 시스템 자원이 얼마나 사용되고 있는지 보여준다. CPU 사용률이 95%에 가깝다면 계산 작업이 병목일 수 있다. 메모리가 부족하면 GC 증가나 스와핑으로 지연이 커질 수 있다. 디스크 I/O가 높다면 로그 쓰기, 파일 처리, 데이터베이스 저장이 원인일 수 있다. 네트워크 지연이 크다면 외부 API, 데이터베이스, 다른 마이크로서비스 호출을 살펴봐야 한다.
중요한 것은 이 지표들을 함께 보는 것이다. 응답 시간이 늘었는데 CPU는 낮고 데이터베이스 커넥션 풀이 꽉 차 있다면, 애플리케이션 코드의 계산 속도보다 DB 연결 대기가 원인일 수 있다. 반대로 DB는 여유로운데 CPU가 계속 포화 상태라면 직렬화, 압축, 이미지 처리, 복잡한 계산 로직을 의심해 볼 수 있다.
병목을 완화하는 대표적인 방법
성능 문제의 해결책은 병목의 종류에 따라 달라진다. 모든 문제에 캐시를 붙이는 방식은 위험하다. 캐시는 읽기 성능을 높일 수 있지만 데이터 불일치와 무효화 문제를 만든다.
캐싱은 자주 읽고 자주 바뀌지 않는 데이터를 빠른 저장소에 보관하는 방법이다. 예를 들어 상품 카테고리, 공지 목록, 사용자 권한 정보처럼 반복 조회되는 데이터를 메모리나 Redis에 저장할 수 있다. 캐시를 사용할 때는 만료 시간, 갱신 시점, 원본 데이터와 달라졌을 때의 허용 범위를 정해야 한다. 결제 금액이나 재고처럼 정확성이 중요한 데이터는 캐시 사용에 더 신중해야 한다. "조금 오래된 값이 보여도 되는가?"라는 질문에 답할 수 있어야 한다.
배치 처리는 많은 작업을 한 번에 묶어 처리하는 방식이다. 매 요청마다 통계 테이블을 갱신하는 대신 일정 시간마다 집계하면 데이터베이스 쓰기 부하를 줄일 수 있다. 다만 실시간성이 떨어질 수 있으므로 "몇 분 정도 늦어도 되는 데이터인가"를 먼저 판단해야 한다. 운영 리포트처럼 지연을 허용할 수 있는 데이터와 결제 승인처럼 즉시 처리해야 하는 데이터는 같은 방식으로 다루면 안 된다.
비동기 큐는 요청 처리 흐름에서 즉시 끝내지 않아도 되는 작업을 분리한다. 회원가입 후 이메일 발송, 이미지 리사이징, 로그 분석, 알림 전송이 대표적이다. API는 작업 요청만 큐에 넣고 빠르게 응답하며, 별도 워커가 큐에서 작업을 꺼내 처리한다. 단, 큐가 쌓이는 속도가 처리 속도보다 빠르면 지연이 뒤로 밀린다. 큐 길이, 재시도 횟수, 실패 작업 보관 방식도 함께 설계해야 한다. 큐를 도입하면 사용자 응답은 빨라질 수 있지만, 작업 완료 시점은 늦어질 수 있다는 점도 함께 설명되어야 한다.
커넥션 풀은 데이터베이스나 외부 서비스 연결을 매번 새로 만들지 않고 재사용하는 기법이다. 연결 생성 비용을 줄이고 동시 연결 수를 제한하는 데 도움을 준다. 하지만 풀 크기를 무작정 키우면 데이터베이스가 감당할 수 없는 연결이 몰릴 수 있다. 애플리케이션 서버 수, 요청당 쿼리 수, 데이터베이스 최대 연결 수를 함께 고려해야 한다.
로드 밸런싱은 여러 서버에 요청을 나누어 보내는 방식이다. 서버 한 대의 한계를 넘어서기 위해 사용한다. 수평 확장의 기본이지만, 애플리케이션이 상태를 서버 내부 메모리에만 저장하고 있다면 문제가 생긴다. 로그인 세션, 임시 파일, 작업 상태처럼 여러 서버가 공유해야 하는 정보는 데이터베이스, 캐시, 객체 스토리지 같은 외부 저장소로 분리하는 것이 좋다.
실무에서 자주 만나는 성능 문제
성능 문제는 복잡한 알고리즘보다 평범한 코드에서 자주 발생한다. 특히 데이터베이스와 외부 호출은 API 응답 시간을 크게 좌우한다.
대표적인 문제는 N+1 쿼리다. 주문 목록 100개를 조회한 뒤 각 주문의 상품을 가져오려고 주문마다 한 번씩 추가 쿼리를 실행하면 총 101번의 쿼리가 발생한다. 개발 환경에서는 데이터가 적어 티가 나지 않지만 운영 데이터가 많아지면 응답 시간이 급격히 늘어난다. 해결 방법은 JOIN, 일괄 조회, ORM의 eager loading, 필요한 필드만 선택하는 방식 등이다. 중요한 것은 "반복문 안에서 DB 조회가 일어나고 있지 않은가"를 습관적으로 확인하는 것이다.
인덱스 설계 문제도 흔하다. 인덱스가 없어서 느린 경우도 있고, 인덱스가 있어도 쿼리 조건과 맞지 않아 사용되지 않는 경우도 있다. WHERE status = 'PAID' ORDER BY created_at DESC 같은 쿼리가 자주 실행된다면 조건과 정렬을 함께 고려한 인덱스가 필요할 수 있다. 다만 인덱스는 쓰기 비용과 저장 공간을 늘리므로 실행 계획과 실제 호출 빈도를 보고 결정해야 한다. 인덱스를 많이 만든다고 항상 빨라지는 것은 아니다.
과도한 직렬화도 무시하기 어렵다. 서버 객체를 JSON으로 바꾸거나, 큰 응답을 압축하고 전송하는 과정도 CPU와 메모리를 사용한다. 필요 없는 필드까지 내려보내면 네트워크 비용과 클라이언트 렌더링 비용도 늘어난다. API 응답은 "있으면 좋을 데이터"가 아니라 "이 화면이나 기능에 필요한 데이터" 중심으로 설계해야 한다.
외부 API 지연은 내 코드가 빠른데도 전체 요청을 느리게 만든다. 결제사, 지도 API, 인증 서버, 사내 다른 서비스가 늦게 응답하면 사용자는 우리 서비스가 느리다고 느낀다. 이런 호출에는 타임아웃, 제한적인 재시도, 장애 시 대체 응답, 회로 차단기 같은 전략이 필요하다. 특히 재시도는 조심해야 한다. 장애 상황에서 모든 서버가 동시에 재시도하면 상대 시스템에 더 큰 부하를 줄 수 있다. 재시도를 하더라도 횟수, 간격, 포기 기준을 정해야 한다.
확장성은 더 많은 자원을 붙일 수 있는 구조다
확장성은 지금 빠르다는 뜻이 아니다. 사용자가 늘거나 데이터가 많아졌을 때 비교적 예측 가능한 방식으로 처리 능력을 늘릴 수 있는 성질이다.
가장 단순한 방법은 수직 확장이다. 더 좋은 CPU, 더 큰 메모리, 더 빠른 디스크를 가진 서버로 바꾸는 방식이다. 적용은 쉽지만 한계가 있고 비용이 빠르게 증가할 수 있다.
다른 방법은 수평 확장이다. 서버 대수를 늘려 요청을 나누어 처리한다. 웹 API 서버는 상태를 외부 저장소로 분리하면 수평 확장하기 쉽다. 반면 데이터베이스는 쓰기 정합성, 트랜잭션, 복제 지연 때문에 수평 확장이 더 어렵다. 그래서 확장성을 고려할 때는 애플리케이션 서버만 볼 것이 아니라 데이터베이스, 캐시, 큐, 외부 API까지 전체 흐름을 봐야 한다.
확장 가능한 구조를 만들려면 먼저 병목이 어디로 이동하는지 이해해야 한다. API 서버를 3대에서 10대로 늘렸는데 모든 서버가 같은 데이터베이스에 더 많은 쿼리를 보내면, 다음 병목은 데이터베이스가 된다. 확장은 문제를 없애기보다 병목의 위치를 바꾸는 경우가 많다. 따라서 서버를 늘린 뒤에도 응답 시간, 오류율, DB 부하, 큐 길이 같은 지표가 함께 좋아졌는지 확인해야 한다.
로그, 메트릭, 트레이싱으로 관측하기
장애가 발생했을 때 가장 위험한 상황은 "무슨 일이 일어났는지 모르는 것"이다. 관측 가능성은 시스템 내부를 직접 들여다볼 수 없는 상황에서도 외부 신호를 통해 상태를 이해할 수 있게 만드는 능력이다. 핵심 도구는 로그, 메트릭, 트레이싱이다.
로그는 특정 시점에 발생한 사건의 기록이다. 예를 들어 "주문 생성 실패", "결제 API 타임아웃", "사용자 권한 없음" 같은 이벤트를 남긴다. 좋은 로그는 문장만 있는 로그가 아니라 검색 가능한 구조를 가진 로그다.
{
"level": "error",
"requestId": "req-123",
"userId": 42,
"path": "/orders",
"message": "payment api timeout"
}requestId나 traceId를 함께 남기면 한 요청이 어떤 서버와 함수들을 거쳤는지 추적하기 쉬워진다. 단, 로그에는 비밀번호, 토큰, 주민등록번호, 카드 번호 같은 민감 정보를 남기면 안 된다. 로그는 문제 해결을 돕기 위한 기록이지, 민감한 데이터를 보관하는 장소가 아니다.
메트릭은 시간에 따라 변하는 숫자다. 요청 수, 오류율, 응답 시간, CPU 사용률, 메모리 사용량, 큐 길이, DB 커넥션 사용률 등이 여기에 속한다. 메트릭은 대시보드와 알림에 적합하다. 예를 들어 "5분 동안 결제 API 오류율이 5%를 넘으면 알림"처럼 운영 기준을 만들 수 있다.
트레이싱은 하나의 요청이 여러 시스템을 거치는 흐름을 추적한다. 사용자의 주문 요청이 API 서버, 재고 서비스, 결제 서비스, 데이터베이스를 차례로 거친다면 각 구간에서 시간이 얼마나 걸렸는지 확인할 수 있다. 마이크로서비스처럼 호출 경로가 복잡한 환경에서는 트레이싱이 특히 중요하다. 단일 서버 애플리케이션에서도 외부 API와 데이터베이스 호출이 많다면 트레이싱은 병목을 찾는 데 도움이 된다.
로그는 사건의 맥락을, 메트릭은 전체 추세를, 트레이싱은 요청의 경로를 보여준다. 셋 중 하나만으로는 부족하다. 메트릭으로 오류율 증가를 발견하고, 트레이싱으로 느린 구간을 찾고, 로그로 구체적인 실패 원인을 확인하는 식으로 함께 사용해야 한다.
성급한 최적화와 필요한 최적화 구분하기
최적화는 비용이 든다. 코드가 복잡해지고, 캐시 무효화 같은 새로운 문제가 생기며, 테스트해야 할 경우의 수도 늘어난다. 그래서 모든 느린 가능성을 미리 제거하려고 하면 개발 속도와 유지보수성이 나빠진다.
성급한 최적화는 기준선 없이 시작된다. "언젠가 느려질 수 있으니 미리 분산 구조로 만들자", "모든 조회에 캐시를 붙이자" 같은 결정이 그렇다. 반면 필요한 최적화는 목표와 측정 결과에서 출발한다.
좋은 습관은 기능을 만들 때 최소한의 성능 기준을 함께 정하는 것이다.
핵심 API의 p95 응답 시간은 얼마 이하여야 하는가?
하루 또는 초당 어느 정도 요청을 예상하는가?
오류율은 어느 수준을 넘으면 장애로 볼 것인가?
데이터는 몇 건까지 늘어날 수 있는가?
실시간성이 필요한가, 몇 분 지연을 허용할 수 있는가?
이 기준이 있어야 개선 여부를 판단할 수 있다. 캐시를 적용한 뒤 응답 시간이 줄었는지, 큐를 도입한 뒤 사용자 응답은 빨라졌지만 작업 완료 지연이 늘지는 않았는지 확인해야 한다. 최적화의 목적은 기술을 더 많이 붙이는 것이 아니라, 사용자가 겪는 지연과 실패를 줄이는 것이다.
성능, 확장성, 관측 가능성은 운영 단계에서만 필요한 지식이 아니다. 개발자가 API를 설계하고, 데이터베이스 쿼리를 작성하고, 외부 서비스를 호출하는 모든 순간에 영향을 준다. 빠른 서비스를 만드는 핵심은 특별한 기술을 많이 붙이는 것이 아니라, 측정 가능한 목표를 세우고 병목을 확인하며 작은 개선을 검증하는 습관이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.