최근 서버 성능 관련 책을 읽기 시작했는데, 개발자에게 꼭 필요한 기본 지식들이 많아 정리하며 공유하고자 합니다. 2장의 핵심 내용인 서버 성능 지표와 기본적인 개선 전략에 대해 이야기해 보겠습니다.
1. 우리 서버의 건강 진단서: 처리량과 응답 시간
서버 성능을 이야기할 때 막연히 "빠르다" 또는 "느리다"라고 표현하는 대신, 구체적인 지표를 사용해야 합니다. 가장 기본이 되는 두 가지 지표는 바로 '응답 시간'과 '처리량'입니다.
응답 시간 (Response Time)
말 그대로 사용자의 요청을 처리하는 데 걸리는 시간입니다. 클라이언트가 API를 호출하고 전체 응답을 받기까지 소요된 시간이죠. 응답 시간은 더 구체적으로 두 가지로 나누어 측정할 수 있습니다.
- TTFB (Time to First Byte): 요청 후 응답 데이터의 첫 번째 바이트가 도착할 때까지 걸린 시간
- TTLB (Time to Last Byte): 요청 후 응답 데이터의 마지막 바이트가 도착할 때까지 걸린 시간
파일 전송처럼 데이터 양이 많거나 네트워크 속도가 느릴수록 TTFB와 TTLB의 차이는 벌어집니다. 따라서 서버의 성능을 올바르게 평가하려면, 우리 서비스의 데이터 특성과 네트워크 환경을 고려해 적절한 지표를 선택해야 합니다.
서버 개발자는 주로 서버 내부의 처리 시간에 집중하게 되는데, 이 시간은 주로 아래와 같은 작업들로 구성됩니다.
- 애플리케이션 로직 수행 (if, for 등)
- DB 연동 (SQL 실행)
- 외부 API 연동
- 응답 데이터 생성 및 전송
이 중에서 성능에 가장 큰 영향을 미치는 부분은 대부분 DB 연동과 외부 API 연동입니다.
처리량 (Throughput)
단위 시간당 시스템이 처리하는 작업량을 의미하며, 보통 TPS와 RPS로 표현합니다.
- TPS (Transactions Per Second): 초당 처리하는 트랜잭션 수
- RPS (Requests Per Second): 초당 처리하는 요청 수
예를 들어, 최대 TPS가 5인 서버에 동시에 7개의 요청이 들어오면, 5개는 바로 처리되지만 나머지 2개는 앞선 작업이 끝날 때까지 대기해야 합니다.
핵심 포인트: 막연히 "성능을 개선하자"가 아니라, "트래픽이 많은 시간대의 TPS와 응답 시간을 측정하고, 목표 TPS와 목표 응답 시간을 설정하자" 와 같이 명확한 목표를 세우는 것이 중요합니다. 명확한 목표는 성능 개선의 방향을 잡아주는 등대가 됩니다.
2. 성능 개선의 시작: 병목 지점 찾기와 기본 전략
서비스 초기에는 사용자가 적어 성능 문제를 체감하기 어렵습니다. 하지만 사용자가 늘고 트래픽이 증가하면서 시스템이 수용할 수 있는 최대 TPS를 초과하게 되면, 문제가 발생하기 시작합니다.
병목 지점 (Bottleneck) 찾기
성능 문제 해결의 첫걸음은 어디서 시간이 가장 오래 걸리는지(병목)를 찾는 것입니다. 모니터링 툴을 활용하면 각 작업에 소요되는 시간을 쉽게 파악할 수 있으며, 만약 툴이 없다면 중요한 작업 지점에 로그라도 남겨서 시간을 측정해야 합니다.
문제를 찾았다면, 아래와 같은 기본 전략들을 고려할 수 있습니다.
급한 불 끄기: 수직 확장 (Scale-up)
서버의 CPU, 메모리, 디스크 같은 하드웨어 자원을 더 좋은 사양으로 업그레이드하는 방법입니다. 가장 빠르고 간단하게 성능을 높일 수 있는 방법입니다.
근본적인 대응: 수평 확장 (Scale-out)
서버 자체의 수를 늘려 전체 처리량을 높이는 방법입니다. 트래픽 증가에 유연하게 대응할 수 있습니다.
주의! 무턱대고 서버를 늘리는 것은 위험합니다. 만약 DB가 병목의 원인인데 애플리케이션 서버만 늘린다면, 오히려 DB에 가해지는 부하만 커져서 상황이 악화될 수 있습니다. 반드시 병목 지점을 먼저 파악해야 합니다.
3. 알아두면 힘이 되는 성능 최적화 기법
서버 확장 외에도 애플리케이션 레벨에서 시도해볼 수 있는 여러 최적화 기법이 있습니다.
DB 커넥션 풀 (Connection Pool)
DB와 통신하려면 '커넥션'을 맺어야 하는데, 이 과정은 비용이 비싼 작업입니다. 커넥션 풀은 미리 일정 개수의 커넥션을 만들어두고, 필요할 때마다 빌려 쓴 뒤 반납하는 방식입니다. 이를 통해 매번 커넥션을 맺고 끊는 비용을 줄일 수 있습니다.
- 커넥션 풀 크기: 너무 크면 DB에 부하를 주고, 너무 작으면 서버의 처리량을 감당하지 못하므로 적절한 크기 설정이 중요합니다.
- 커넥션 대기 시간 (Timeout): 풀이 가득 찼을 때 커넥션을 얻기 위해 기다리는 최대 시간입니다. 너무 길면 사용자가 무한정 기다리게 되므로, 가능한 짧게 설정하는 것이 좋습니다.
서버 캐시 (Cache)
자주 요청되지만 잘 변하지 않는 데이터를 메모리에 저장해두고, DB까지 가지 않고 바로 응답하는 기술입니다. 캐시가 얼마나 유효하게 사용되었는지는 **적중률(Hit Ratio)**로 평가합니다.
- 로컬 캐시: 서버 프로세스와 동일한 메모리를 사용합니다. 속도는 매우 빠르지만, 서버가 재시작되면 캐시도 함께 사라지는 단점이 있습니다.
- 리모트 캐시 (e.g., Redis): 별도의 서버에 캐시 저장소를 둡니다. 네트워크 통신으로 인해 로컬 캐시보다는 느리지만, 서버가 재시작되어도 데이터가 유지되는 장점이 있습니다.
가비지 컬렉터 (GC)와 메모리 관리
Java와 같은 언어에서는 가비지 컬렉터(GC)가 더 이상 사용하지 않는 메모리를 자동으로 정리해줍니다. 하지만 GC가 동작하는 동안에는 애플리케이션의 실행이 일시적으로 중단(Stop-the-world)될 수 있습니다. 메모리 사용량이 많고, 생성되는 객체가 많을수록 GC 실행 시간이 길어져 응답 시간에 영향을 줄 수 있습니다.
응답 데이터 압축
HTML, CSS, JSON 같은 텍스트 기반의 응답 데이터는 압축해서 전송하면 네트워크 트래픽을 줄일 수 있습니다. 이를 통해 클라이언트가 데이터를 더 빨리 내려받게 할 수 있습니다. (단, 클라이언트가 해당 압축 알고리즘을 지원해야 합니다.)
정리하며
이번 장을 통해 서버 성능 개선은 '측정 -> 목표 설정 -> 병목 분석 -> 개선' 이라는 체계적인 과정을 통해 이루어져야 함을 배웠습니다. 막연한 추측이 아닌 데이터 기반의 접근이 핵심이며, 오늘 다룬 DB 커넥션 풀, 캐시 등의 기본 전략을 잘 이해하고 상황에 맞게 적용하는 것이 성능 좋은 서버를 만드는 첫걸음이 될 것입니다.