백엔드는 숫자가 지나가는 길이다
프론트엔드가 사용자의 손끝에 가까운 세계라면, 백엔드는 사용자의 행동이 데이터와 요청으로 바뀌어 흐르는 세계다. 로그인 버튼을 누르면 인증 요청이 생기고, 상품을 클릭하면 조회 쿼리가 실행되며, 결제 버튼을 누르면 주문, 재고, 쿠폰, 포인트, 알림 시스템이 줄줄이 움직인다. 겉으로는 API 하나처럼 보이지만, 안쪽에서는 데이터베이스, 캐시, 메시지 큐, 외부 서비스, 로그 시스템이 서로 연결되어 있다.
백엔드 개발에서 수학이 필요한 이유는 이 흐름을 감으로만 다루기 어렵기 때문이다. 요청이 하루 1만 건일 때 잘 돌던 시스템이 하루 100만 건이 되면 왜 느려질까? 캐시를 붙였는데도 왜 데이터베이스 부하는 줄지 않을까? 재시도를 추가했는데 왜 장애가 더 커질까? 평균 응답 시간은 괜찮은데 왜 사용자는 느리다고 말할까?
이 질문들은 모두 숫자의 질문이다. 더 정확히 말하면 집합, 확률, 통계, 그래프, 큐잉의 질문이다.
백엔드의 성능과 안정성은 대단한 공식을 많이 아는 것보다, 시스템을 다음과 같이 바라보는 감각에 달려 있다. 데이터는 어떤 집합에 속하는가. 요청은 어떤 경로를 따라 이동하는가. 실패는 어느 정도의 확률로 발생하는가. 큐에는 얼마나 많은 일이 쌓이는가. 로그에 보이는 숫자는 정상적인 흔들림인가, 위험 신호인가.
이 장에서는 앞에서 배운 수학을 백엔드 개발자의 일상으로 끌고 온다. 데이터베이스의 테이블과 조인을 집합으로 보고, 캐시와 장애를 확률로 계산하며, 큐와 처리량으로 병목을 이해하고, 로그와 통계로 시스템의 건강 상태를 읽어 보자.

데이터베이스는 집합이고, 조인은 관계다
데이터베이스를 처음 배울 때는 테이블을 엑셀 표처럼 이해하기 쉽다. users 테이블에는 사용자 목록이 있고, orders 테이블에는 주문 목록이 있으며, products 테이블에는 상품 목록이 있다. 이 설명도 틀리지는 않다. 하지만 백엔드 개발자가 성능과 모델링까지 생각하려면 한 걸음 더 들어가야 한다.
테이블은 행의 집합이다. users 테이블은 사용자 행들의 집합이고, orders 테이블은 주문 행들의 집합이다. SQL의 WHERE 조건은 이 집합에서 조건을 만족하는 원소만 골라내는 필터다. 예를 들어 "최근 30일 동안 결제 완료된 주문"을 찾는 쿼리는 전체 주문 집합에서 created_at과 status라는 조건을 만족하는 부분집합을 만드는 일이다.
이렇게 보면 SQL은 갑자기 낯선 문법이 아니라 집합을 다루는 언어가 된다. 조회는 부분집합을 만드는 일이고, 삭제는 집합에서 원소를 제거하는 일이며, 삽입은 집합에 원소를 추가하는 일이다. 업데이트는 어떤 조건을 만족하는 원소들의 속성을 바꾸는 일이다.
조인은 관계를 따라 두 집합을 연결하는 연산이다. users와 orders를 조인한다는 것은 사용자 집합과 주문 집합 사이에 있는 "사용자가 주문을 했다"라는 관계를 따라 데이터를 붙이는 일이다. 한 명의 사용자가 여러 주문을 가질 수 있다면 이것은 일대다 관계다. 하나의 상품이 여러 카테고리에 들어갈 수 있고, 하나의 카테고리에 여러 상품이 들어갈 수 있다면 다대다 관계다. 이 경우에는 보통 중간 테이블을 둔다. 수학적으로 말하면 관계 자체를 하나의 집합으로 저장하는 셈이다.
예를 들어 user_roles 테이블을 생각해 보자. 이 테이블에는 사용자 ID와 역할 ID가 들어 있다. 이 테이블의 각 행은 "이 사용자는 이 역할을 가진다"라는 관계 하나를 뜻한다. 권한 시스템은 결국 사용자 집합, 역할 집합, 권한 집합 사이의 관계를 어떻게 설계하느냐의 문제다. 권한 버그가 자주 생기는 시스템은 대개 코드가 복잡해서만이 아니라, 관계 모델이 흐릿하기 때문에 위험해진다.
인덱스도 집합 사고로 이해할 수 있다. 데이터베이스가 어떤 조건을 만족하는 행을 찾으려면 전체 집합을 하나씩 훑을 수도 있다. 행이 1천 개라면 괜찮을 수 있지만, 1억 개라면 이야기가 달라진다. 인덱스는 특정 속성을 기준으로 데이터를 더 빨리 찾을 수 있도록 만든 별도의 구조다. 집합 전체를 뒤지는 대신, 후보 집합을 빠르게 좁히는 장치라고 볼 수 있다.
여기서 중요한 개념이 선택도다. 선택도는 조건을 걸었을 때 전체 데이터 중 얼마나 적은 데이터가 남는지를 나타내는 감각이다. gender = 'M' 같은 조건은 대개 전체의 절반 가까이를 남길 수 있다. 반면 user_id = 12345는 한 명의 사용자만 남긴다. 후자가 훨씬 선택도가 높다. 인덱스가 특히 강력한 순간은 이렇게 많은 데이터 중 아주 작은 부분집합을 빠르게 골라낼 때다.
복합 인덱스는 여러 조건을 함께 사용해 후보 집합을 줄이는 방식이다. 예를 들어 주문 테이블에서 user_id와 created_at을 함께 자주 조회한다면, "특정 사용자의 최근 주문"이라는 부분집합을 빠르게 찾을 수 있다. 하지만 모든 컬럼에 무작정 인덱스를 만들면 쓰기 성능과 저장 공간이 나빠진다. 인덱스는 공짜가 아니다. 읽기 속도를 얻는 대신, 삽입과 수정 때 인덱스 구조도 함께 갱신해야 한다.
백엔드 개발자가 쿼리를 볼 때 던져야 할 질문은 단순하다. 이 쿼리는 전체 집합에서 얼마나 많은 원소를 남기는가. 조인은 어떤 관계를 따라 얼마나 많은 조합을 만들어 내는가. 인덱스는 후보 집합을 충분히 줄여 주는가. 이 세 질문만 제대로 해도 느린 쿼리를 보는 눈이 크게 달라진다.
관계가 복잡해지면 그래프가 보인다
데이터베이스의 관계가 많아지면 시스템은 자연스럽게 그래프처럼 보인다. 사용자는 주문과 연결되고, 주문은 결제와 배송으로 이어지며, 상품은 카테고리, 쿠폰, 리뷰, 재고와 연결된다. 마이크로서비스 구조에서는 이 그래프가 더 노골적으로 드러난다. API 게이트웨이는 인증 서비스와 연결되고, 주문 서비스는 재고 서비스와 결제 서비스에 의존하며, 알림 서비스는 메시지 큐를 통해 뒤따라 움직인다.
그래프 관점은 "무엇이 무엇에 의존하는가"를 보여 준다. 백엔드 장애 분석에서 이 관점은 매우 중요하다. 결제 서비스가 느려졌을 뿐인데 주문 API 전체가 느려지고, 주문 API가 느려지자 프론트엔드의 결제 완료 화면도 느려진다. 알림 서비스는 핵심 결제 흐름과 직접 관련이 없어 보이지만, 만약 주문 트랜잭션 안에서 동기 호출로 묶여 있다면 알림 서비스 장애가 주문 장애로 번질 수 있다.
그래프에서 중요한 것은 연결 자체만이 아니다. 방향과 가중치도 중요하다. A 서비스가 B 서비스를 호출한다면 방향은 A에서 B로 향한다. 호출 시간이 길거나 호출 빈도가 높다면 그 엣지의 가중치는 크다. 장애 전파 가능성이 높다면 위험한 엣지다. 시스템 다이어그램을 그릴 때 단순히 박스를 나열하는 것보다, 요청이 어느 방향으로 흐르고 어느 연결이 무거운지 표시하는 편이 훨씬 유용하다.
의존성 그래프에서 사이클은 특히 조심해야 한다. A가 B를 부르고, B가 C를 부르고, C가 다시 A를 부르는 구조는 이해하기도 어렵고 장애를 끊어 내기도 어렵다. 소프트웨어 설계에서 순환 의존성을 줄이려는 이유가 여기에 있다. 방향 비순환 그래프, 즉 DAG 구조에 가까울수록 흐름을 추적하기 쉽고 배포 순서나 장애 영향도 계산하기도 쉬워진다.
데이터 모델에서도 그래프 감각은 유용하다. 친구 관계, 팔로우 관계, 추천 관계, 조직도, 카테고리 트리, 댓글의 대댓글 구조는 모두 그래프 또는 트리로 이해할 수 있다. 특정 사용자와 가까운 사용자를 찾는 문제는 그래프 탐색이고, 특정 카테고리 아래 모든 상품을 찾는 문제는 트리 순회다. 백엔드 개발에서 그래프는 특별한 알고리즘 문제에만 등장하는 것이 아니라, 데이터가 서로 연결되는 모든 곳에 숨어 있다.
캐시는 확률로 판단해야 한다
캐시는 백엔드 성능 개선의 단골 도구다. 느린 데이터베이스 조회 결과를 빠른 저장소에 잠시 보관해 두고, 같은 요청이 들어오면 데이터베이스까지 가지 않고 바로 응답한다. 개념은 단순하다. 하지만 캐시를 제대로 쓰려면 확률적으로 생각해야 한다.
가장 먼저 볼 숫자는 캐시 적중률이다. 캐시 적중률은 전체 요청 중 캐시에서 바로 처리된 요청의 비율이다. 적중률이 90%라면 요청 100개 중 90개는 캐시에서 처리되고, 10개만 원본 데이터베이스로 간다. 이 숫자 하나만으로도 시스템의 평균 응답 시간이 크게 달라진다.
예를 들어 캐시 조회는 5ms, 데이터베이스 조회는 100ms가 걸린다고 하자. 캐시 적중률이 80%라면 평균 응답 시간의 단순 기대값은 다음과 같이 계산할 수 있다.
평균 응답 시간 = 0.8 × 5ms + 0.2 × 100ms = 24ms
적중률이 95%로 올라가면 어떻게 될까.
평균 응답 시간 = 0.95 × 5ms + 0.05 × 100ms = 9.75ms
캐시 조회 시간은 그대로인데 평균 응답 시간이 절반 이하로 줄었다. 이것이 캐시 적중률이 중요한 이유다. 캐시는 "있으면 좋은 최적화"가 아니라, 요청 분포가 맞아떨어질 때 시스템 전체 비용을 바꾸는 확률 장치다.
하지만 평균만 보고 안심하면 위험하다. 캐시가 비는 순간, 즉 캐시 미스가 몰리는 순간에는 데이터베이스로 요청이 한꺼번에 쏟아질 수 있다. 인기 상품 하나의 캐시가 만료되었는데 동시에 수천 명이 그 상품을 조회하면, 모든 요청이 원본 데이터베이스로 향한다. 이것을 캐시 스탬피드라고 부른다. 수학적으로 보면 평소에는 원본으로 가는 확률이 낮았지만, 특정 시점에 미스 사건이 강하게 상관되어 한꺼번에 발생하는 문제다.
캐시 전략은 그래서 확률과 시간의 문제다. TTL을 너무 길게 잡으면 오래된 데이터를 보여 줄 위험이 커지고, 너무 짧게 잡으면 미스가 자주 발생한다. 인기 데이터와 비인기 데이터의 접근 확률도 다르다. 모든 데이터를 같은 방식으로 캐시하면 효과가 낮을 수 있다. 자주 읽히고 자주 바뀌지 않는 데이터는 캐시 효과가 크지만, 거의 읽히지 않거나 매번 바뀌는 데이터는 캐시에 넣어도 이득이 작다.
장애 확률도 비슷하게 계산할 수 있다. 어떤 외부 API의 호출 실패 확률이 1%라고 하자. 이 API를 한 요청 안에서 한 번만 호출하면 실패 확률은 대략 1%다. 그런데 서로 독립적으로 실패한다고 가정할 수 있는 외부 호출을 5번 연속으로 해야 한다면, 전체 요청이 모두 성공할 확률은 0.99⁵이다. 계산하면 약 95.1%다. 반대로 말하면 전체 요청 실패 확률은 약 4.9%가 된다. 각각의 실패 확률은 작아 보여도, 여러 단계를 거치면 전체 실패 확률은 생각보다 커진다.
이 감각은 백엔드 설계에서 매우 중요하다. 하나의 API 응답을 만들기 위해 너무 많은 동기 호출을 이어 붙이면, 전체 성공률은 각 호출 성공률의 곱으로 줄어든다. 성능도 마찬가지다. 각 서비스의 지연 시간이 더해지면 전체 응답 시간은 길어진다. 그래서 중요한 요청 경로에서는 호출 수를 줄이고, 꼭 필요하지 않은 작업은 비동기로 분리하며, 실패해도 핵심 기능이 유지되도록 설계한다.
큐는 기다림을 숫자로 보여 준다
백엔드 시스템은 항상 즉시 처리할 수 있는 일만 다루지 않는다. 이메일 발송, 푸시 알림, 이미지 리사이징, 정산, 로그 적재, 추천 모델 업데이트처럼 시간이 걸리거나 나중에 처리해도 되는 일은 큐에 넣어 비동기로 처리한다. 큐는 요청을 잠시 줄 세우는 장치다.
큐를 이해할 때 핵심 숫자는 도착률과 처리율이다. 도착률은 단위 시간당 일이 들어오는 속도이고, 처리율은 단위 시간당 일을 처리하는 속도다. 1초에 작업이 100개 들어오는데 워커가 1초에 120개를 처리할 수 있다면 큐는 안정적으로 유지될 가능성이 높다. 반대로 1초에 100개가 들어오는데 80개밖에 처리하지 못한다면 큐 길이는 계속 늘어난다.
이때 중요한 것은 평균적으로 처리율이 조금 더 높다고 해서 항상 안전한 것은 아니라는 점이다. 요청은 균등하게 오지 않는다. 점심시간, 퇴근 시간, 이벤트 시작 시점, 푸시 발송 직후처럼 특정 순간에 몰린다. 평균 도착률은 1초에 80개인데 특정 5분 동안 1초에 500개가 들어올 수 있다. 큐는 이런 순간적인 폭주를 흡수하지만, 처리율이 충분하지 않으면 대기 시간이 빠르게 커진다.
큐잉 감각에서 자주 쓰는 값이 이용률이다. 이용률은 처리 능력 중 얼마나 사용하고 있는지를 나타낸다. 단순화하면 이용률 = 도착률 / 처리율로 볼 수 있다. 도착률이 초당 80개이고 처리율이 초당 100개라면 이용률은 0.8이다. 아직 여유가 있어 보인다. 하지만 이용률이 0.95, 0.98처럼 1에 가까워지면 대기 시간은 급격히 불안정해진다. 작은 흔들림만 있어도 일이 쌓이기 때문이다.
계산 예를 들어 보자. 어떤 이미지 처리 워커가 초당 50개의 이미지를 처리할 수 있다. 평소에는 초당 30개가 들어온다. 이때 큐는 대체로 안정적이다. 그런데 이벤트로 초당 200개의 이미지 업로드가 10분 동안 이어진다면 어떻게 될까. 10분은 600초이므로 들어오는 작업은 200 × 600 = 120,000개다. 같은 시간 동안 처리할 수 있는 작업은 50 × 600 = 30,000개다. 이벤트가 끝난 시점에 큐에는 약 90,000개의 작업이 밀려 있다. 이벤트가 끝난 뒤 도착률이 다시 초당 30개로 내려가도, 워커가 실제로 줄일 수 있는 속도는 초당 50 - 30 = 20개다. 90,000개를 줄이려면 4,500초, 즉 75분이 걸린다.
이 계산을 해 보면 "이벤트 10분"이 "지연 75분"으로 바뀌는 이유가 보인다. 사용자는 이벤트가 끝났는데도 한참 뒤에야 이미지 처리가 완료되는 것을 경험한다. 장애가 아닌 것처럼 보이지만, 실제 사용자 경험은 장애에 가깝다.
큐를 다룰 때 백엔드 개발자는 다음 숫자를 함께 봐야 한다.
현재 큐 길이: 지금 밀려 있는 작업이 얼마나 많은가
도착률: 단위 시간당 새 작업이 얼마나 들어오는가
처리율: 워커가 단위 시간당 얼마나 처리하는가
대기 시간: 작업이 큐에 들어온 뒤 처리되기까지 얼마나 기다리는가
실패율과 재시도율: 처리 실패로 다시 큐에 들어오는 작업이 얼마나 되는가
큐의 무서운 점은 실패가 다시 일을 만든다는 것이다. 작업 처리에 실패한 메시지를 즉시 재시도하도록 만들면, 장애 상황에서 큐에 같은 작업이 반복해서 들어온다. 처리율은 이미 낮아졌는데 재시도 작업이 도착률을 더 키운다. 결국 큐는 스스로를 더 바쁘게 만드는 구조가 된다. 그래서 재시도에는 지수 백오프, 최대 재시도 횟수, 데드 레터 큐 같은 안전장치가 필요하다. 어려운 말처럼 들리지만 원리는 단순하다. 실패한 일을 무한히, 즉시, 같은 방식으로 반복하지 말라는 뜻이다.
처리량과 지연 시간은 같은 말이 아니다
성능 이야기를 할 때 처리량과 지연 시간을 혼동하기 쉽다. 처리량은 단위 시간당 얼마나 많은 일을 처리하는지를 뜻한다. 지연 시간은 요청 하나가 완료되기까지 걸린 시간을 뜻한다. 서버가 초당 1,000건을 처리할 수 있다고 해서 모든 사용자가 빠르게 응답을 받는 것은 아니다. 반대로 요청 하나의 응답 시간이 짧아도 동시에 많은 요청을 처리하지 못하면 전체 시스템은 쉽게 막힌다.
식당을 떠올려 보자. 주방이 1분에 음식 100개를 만들 수 있다면 처리량은 높다. 하지만 주문을 받는 창구가 하나뿐이고 손님이 길게 줄을 서야 한다면 각 손님의 체감 지연 시간은 길다. 백엔드도 마찬가지다. 데이터베이스 커넥션 풀이 작거나, 특정 락을 오래 잡거나, 외부 API 응답을 기다리는 동안 스레드가 묶이면 처리량과 지연 시간이 서로 다르게 움직인다.
이때 유용한 직관이 리틀의 법칙이다. 아주 단순하게 말하면, 시스템 안에 머무는 작업 수는 도착률과 머무는 시간의 곱으로 볼 수 있다.
시스템 안의 작업 수 = 도착률 × 평균 머무는 시간
초당 100개의 요청이 들어오고, 요청 하나가 평균 0.2초 동안 시스템 안에 머문다면, 동시에 처리 중이거나 대기 중인 요청은 평균적으로 약 20개다. 그런데 평균 응답 시간이 2초로 늘어나면 같은 도착률에서도 시스템 안의 요청 수는 200개가 된다. 지연 시간이 늘어나면 서버 안에 머무는 작업이 많아지고, 이것이 다시 자원 경쟁을 키워 더 느려지는 악순환이 생길 수 있다.
그래서 백엔드 성능 개선은 단순히 "서버를 더 늘리자"로 끝나지 않는다. 어느 구간에서 시간이 늘어나는지 나누어 봐야 한다. 네트워크 대기인지, 데이터베이스 조회인지, 직렬화인지, 락 대기인지, 큐 대기인지, 외부 API 호출인지 구분해야 한다. 병목은 전체 흐름에서 가장 좁은 구간이다. 다른 구간을 아무리 넓혀도 병목이 그대로면 전체 처리량은 크게 좋아지지 않는다.
분산 시스템에서는 실패가 확률로 온다
단일 서버에서 모든 일을 처리하던 시절에는 실패를 비교적 단순하게 생각할 수 있었다. 서버가 살아 있으면 성공하고, 죽으면 실패한다. 하지만 현대 백엔드 시스템은 여러 서버, 여러 데이터베이스, 여러 캐시, 여러 외부 API로 구성된다. 이 세계에서는 실패가 흑백이 아니라 확률로 온다.
어떤 요청은 성공하고, 어떤 요청은 타임아웃된다. 같은 API도 한 번 호출하면 실패하지만 다시 호출하면 성공할 수 있다. 네트워크는 완전히 끊긴 것이 아니라 가끔 느려진다. 데이터베이스는 죽지 않았지만 특정 쿼리만 오래 걸린다. 메시지 큐는 동작하지만 소비자가 밀려 있다. 이런 상태를 부분 실패라고 부른다.
분산 시스템에서 재시도는 강력하지만 위험한 도구다. 실패 확률이 일시적으로 10%인 API가 있다고 하자. 한 번 호출했을 때 실패할 확률은 0.1이다. 독립적인 재시도라고 단순 가정하면 두 번 모두 실패할 확률은 0.1 × 0.1 = 0.01이다. 세 번 모두 실패할 확률은 0.001이다. 재시도는 성공 확률을 크게 높인다.
문제는 재시도가 트래픽을 늘린다는 점이다. 원래 요청 1,000개 중 10%가 실패해서 재시도된다면 추가 요청은 100개다. 그런데 장애가 커져 실패율이 50%가 되면 추가 요청은 훨씬 늘어난다. 모든 클라이언트가 즉시 재시도하면 이미 힘든 서버에 더 많은 요청이 몰린다. 이것이 재시도 폭풍이다. 선의의 재시도가 장애를 증폭하는 것이다.
타임아웃도 확률적 판단이다. 타임아웃을 너무 짧게 잡으면 조금만 느린 정상 요청도 실패로 처리된다. 타임아웃을 너무 길게 잡으면 이미 응답 가능성이 낮은 요청을 붙잡고 자원을 낭비한다. 좋은 타임아웃은 사용자의 기대 시간, 하위 서비스의 응답 시간 분포, 재시도 비용을 함께 고려해 정한다. 평균 응답 시간만 보고 타임아웃을 정하면 안 된다. p95나 p99 같은 상위 지연 시간을 봐야 한다.
일관성도 백엔드에서 수학적 사고가 필요한 영역이다. 단일 데이터베이스 안에서는 트랜잭션으로 강한 일관성을 유지하기 쉽다. 하지만 여러 서비스와 저장소에 데이터가 나뉘면 모든 곳의 상태가 항상 동시에 같기는 어렵다. 주문 서비스에는 결제 완료로 표시되었지만, 알림 서비스에는 아직 이벤트가 도착하지 않았을 수 있다. 검색 인덱스에는 새 상품 정보가 몇 초 뒤에 반영될 수 있다.
이것은 단순한 버그가 아니라 시스템 설계의 선택일 때가 많다. 즉시 일관성을 강하게 요구하면 지연 시간이 늘고 가용성이 낮아질 수 있다. 반대로 최종적 일관성을 받아들이면 사용자에게 잠시 오래된 데이터를 보여 줄 수 있지만 시스템은 더 유연하게 동작한다. 중요한 것은 어느 데이터가 반드시 즉시 맞아야 하고, 어느 데이터는 잠시 늦어도 되는지 구분하는 일이다. 결제 금액과 재고 차감은 강하게 관리해야 하지만, 추천 목록이나 검색 결과는 몇 초 늦어도 괜찮을 수 있다.
분산 시스템의 수학은 거창한 정리가 아니라 선택의 비용을 숫자로 보는 태도다. 성공률을 높이기 위해 재시도를 몇 번 할 것인가. 사용자 응답 시간을 지키기 위해 타임아웃을 어디에 둘 것인가. 데이터 일관성을 위해 얼마나 기다릴 것인가. 장애 전파를 막기 위해 어느 연결을 끊을 것인가. 이 질문들에 답하려면 시스템을 확률과 흐름으로 보아야 한다.
로그는 시스템이 남긴 통계 데이터다
백엔드 시스템은 매 순간 흔적을 남긴다. 요청 로그, 에러 로그, 쿼리 로그, GC 로그, 큐 처리 로그, 배포 로그, 사용자 행동 로그가 쌓인다. 로그는 단순한 기록이 아니다. 잘 모으면 시스템이 스스로 남긴 통계 데이터가 된다.
성능 분석에서 가장 흔한 실수는 평균만 보는 것이다. 평균 응답 시간이 200ms라고 해서 대부분의 사용자가 200ms 안팎의 응답을 받는다는 뜻은 아니다. 99명의 사용자는 50ms에 응답을 받고, 1명의 사용자는 15초를 기다려도 평균은 그럭저럭 괜찮아 보일 수 있다. 하지만 그 1명에게는 심각한 장애다.
그래서 백엔드 모니터링에서는 백분위수를 자주 본다. p50은 전체 요청 중 절반이 이 시간 이하로 끝났다는 뜻이다. p95는 95%의 요청이 이 시간 이하로 끝났다는 뜻이고, p99는 99%의 요청이 이 시간 이하로 끝났다는 뜻이다. 평균이 평온해도 p99가 치솟는다면 일부 사용자는 매우 나쁜 경험을 하고 있다.
예를 들어 평균 응답 시간은 180ms인데 p99가 5초라면 무엇을 의심해야 할까. 일부 요청만 느려지는 조건이 있을 수 있다. 특정 사용자만 많은 데이터를 가지고 있거나, 특정 쿼리만 인덱스를 타지 않거나, 특정 외부 API 호출이 가끔 오래 걸릴 수 있다. 평균은 이 꼬리를 숨기지만, p99는 꼬리를 드러낸다.
로그 분석은 대개 다음 순서로 진행하면 좋다.
정상 상태의 기준선을 잡는다. 평소 트래픽, 응답 시간, 에러율, 큐 길이, CPU, 메모리, 데이터베이스 부하를 시간대별로 파악한다.
문제가 발생한 시점을 찾는다. 배포, 이벤트, 외부 장애, 데이터 증가, 설정 변경과 겹치는지 확인한다.
지표를 쪼갠다. 전체 평균이 아니라 API별, 사용자 그룹별, 지역별, 버전별, 데이터 크기별로 나누어 본다.
분포를 확인한다. 평균뿐 아니라 p50, p95, p99, 최댓값, 이상치를 함께 본다.
원인 후보를 실험으로 좁힌다. 쿼리 실행 계획, 캐시 적중률, 큐 대기 시간, 외부 API 응답 시간 등을 하나씩 확인한다.
이 과정은 통계적 추론과 닮아 있다. 로그는 표본이고, 우리는 그 표본을 통해 시스템 전체의 상태를 추정한다. 일부 서버에서만 에러가 늘었는지, 전체적으로 지연 시간이 증가했는지, 특정 배포 이후에만 문제가 나타나는지 판단한다. 이때 상관관계와 인과관계를 구분해야 한다. 배포 직후 에러가 늘었다고 해서 항상 배포가 원인은 아니다. 같은 시각에 트래픽 이벤트가 있었을 수도 있고, 외부 API 장애가 시작되었을 수도 있다. 하지만 배포와 에러 증가가 강하게 함께 움직인다면 중요한 단서다.
이상 징후를 찾을 때도 통계 감각이 필요하다. 트래픽은 원래 흔들린다. 월요일 오전과 토요일 새벽의 요청 수는 다를 수 있다. 단순히 어제보다 20% 증가했다고 해서 이상이라고 볼 수 없다. 계절성, 요일 패턴, 이벤트, 마케팅 캠페인, 배포 일정을 함께 봐야 한다. 정상적인 변동 범위를 알고 있어야 진짜 이상을 알아차릴 수 있다.
병목은 가장 좁은 통로에서 생긴다
백엔드 성능 문제를 해결할 때 가장 먼저 해야 할 일은 병목을 찾는 것이다. 병목은 전체 처리 흐름에서 가장 제한적인 구간이다. API 서버를 10대로 늘렸는데 데이터베이스 커넥션 풀이 꽉 차 있다면 성능은 크게 좋아지지 않는다. 캐시 서버를 붙였는데 캐시 적중률이 낮다면 데이터베이스 부하는 그대로일 수 있다. 워커를 늘렸는데 외부 API 호출 제한에 걸린다면 큐는 계속 밀린다.
병목을 찾는 일은 파이프의 가장 좁은 부분을 찾는 일과 비슷하다. 넓은 구간을 더 넓혀도 물은 더 빨리 흐르지 않는다. 가장 좁은 구간을 찾아 넓히거나, 물이 그 구간을 덜 지나가도록 흐름을 바꿔야 한다.
수학적으로는 전체 시간을 구성 요소로 분해해 보는 방식이 유용하다. 하나의 요청 시간이 다음과 같이 나뉜다고 하자.
전체 응답 시간 = 인증 시간 + 비즈니스 로직 시간 + DB 조회 시간 + 외부 API 시간 + 직렬화 시간 + 네트워크 시간
전체 응답 시간이 800ms인데 그중 DB 조회가 600ms라면, 인증 로직을 10ms 줄이는 최적화는 큰 효과가 없다. 반대로 DB 조회를 600ms에서 100ms로 줄이면 전체 성능이 크게 좋아진다. 이것은 너무 당연해 보이지만, 실제 개발 현장에서는 자주 잊힌다. 눈에 잘 보이는 코드부터 최적화하고 싶어지기 때문이다.
병목 분석에서는 비율을 보는 감각도 중요하다. 어떤 작업이 전체 시간의 5%를 차지한다면, 그 작업을 아무리 빠르게 만들어도 전체 개선 폭은 제한적이다. 반대로 전체 시간의 70%를 차지하는 작업은 조금만 개선해도 전체 응답 시간이 크게 줄 수 있다. 최적화는 노력의 문제가 아니라 영향도의 문제다.
이런 관점은 장애 대응에도 적용된다. 에러율이 1%에서 5%로 올랐을 때 전체 장애처럼 보일 수 있지만, 실제로는 특정 API 하나에서만 80%의 에러가 발생할 수 있다. 데이터베이스 CPU가 높아졌을 때 모든 쿼리가 문제처럼 보이지만, 실제로는 특정 배치 작업 하나가 테이블을 잠그고 있을 수 있다. 전체 숫자에서 시작하되, 반드시 쪼개서 보아야 한다.
백엔드 설계는 기대값과 최악의 경우 사이에서 균형 잡기다
백엔드 시스템은 평균적인 날에도 잘 돌아가야 하지만, 이상한 날에도 버텨야 한다. 평소 트래픽만 보고 설계하면 이벤트나 장애에 무너지고, 최악의 경우만 보고 설계하면 비용이 지나치게 커진다. 그래서 백엔드 개발자는 기대값과 최악의 경우 사이에서 균형을 잡아야 한다.
기대값은 평소의 비용을 보여 준다. 캐시 적중률이 90%일 때 평균적으로 데이터베이스 요청이 얼마나 줄어드는지, 요청 하나당 외부 API 비용이 얼마나 드는지, 하루 로그 저장 비용이 얼마나 되는지 계산할 수 있다. 기대값은 운영 비용과 평균 성능을 판단하는 데 유용하다.
하지만 최악의 경우는 안정성을 보여 준다. 캐시가 한꺼번에 만료되면 어떻게 되는가. 외부 API가 5초 동안 응답하지 않으면 스레드가 얼마나 묶이는가. 메시지 큐 소비자가 절반으로 줄면 대기 시간이 얼마나 늘어나는가. 특정 사용자가 비정상적으로 많은 데이터를 가지고 있을 때 쿼리는 얼마나 느려지는가. 평균에서는 보이지 않던 위험이 최악의 경우에서 드러난다.
이 균형을 잡기 위해 백엔드에서는 제한을 둔다. 페이지 크기를 제한하고, 요청당 처리할 수 있는 항목 수를 제한하며, 타임아웃을 설정하고, 큐 재시도 횟수를 제한한다. 제한은 사용자를 불편하게 하려는 장치가 아니다. 시스템 전체가 무너지지 않도록 만드는 수학적 안전장치다. 무한 스크롤도 실제로는 한 번에 유한한 개수만 가져오고, 파일 업로드도 크기 제한이 있으며, API 호출도 rate limit을 둔다. 무한한 입력을 허용하는 시스템은 유한한 자원 위에서 오래 버티기 어렵다.
백엔드 개발자가 숫자를 보는 습관
백엔드에서 수학은 칠판 위의 공식보다 운영 화면의 숫자에 가깝다. 데이터베이스의 행 수, 인덱스 선택도, 캐시 적중률, 큐 길이, 처리량, 에러율, p99 응답 시간, 재시도 횟수, 타임아웃 비율이 모두 수학이다. 이 숫자들은 따로 존재하지 않는다. 서로 영향을 주고받으며 시스템의 상태를 만든다.
좋은 백엔드 개발자는 장애가 난 뒤에야 숫자를 보는 사람이 아니라, 설계할 때부터 숫자를 예상하는 사람이다. 이 API는 초당 몇 건까지 들어올 수 있을까. 이 쿼리는 데이터가 10배가 되어도 괜찮을까. 캐시가 없을 때 데이터베이스가 버틸 수 있을까. 큐가 1시간 밀리면 사용자에게 어떤 일이 생길까. 외부 API가 1% 실패하면 우리 요청은 몇 % 실패할까.
이 질문에 처음부터 정확히 답할 필요는 없다. 중요한 것은 대략이라도 계산해 보는 태도다. 추정이 있어야 측정할 수 있고, 측정이 있어야 개선할 수 있다. 수학은 완벽한 예언 도구가 아니라 더 나은 질문을 던지게 해 주는 도구다.
백엔드 개발을 수학적으로 본다는 것은 시스템을 차갑게만 본다는 뜻이 아니다. 오히려 사용자의 경험을 더 구체적으로 상상하는 일에 가깝다. p99 지연 시간은 누군가가 결제 완료 화면에서 기다린 시간이고, 큐 대기 시간은 누군가가 업로드한 이미지가 언제 보이는지에 대한 약속이며, 에러율은 누군가가 다시 시도해야 했던 횟수다. 숫자는 사용자 경험의 다른 이름이다.
이 장에서 다룬 집합, 확률, 통계, 그래프, 큐잉 감각은 백엔드 시스템의 뼈대를 읽는 언어다. 테이블은 집합이고, 조인은 관계이며, 서비스 호출은 그래프다. 캐시는 확률이고, 큐는 기다림이며, 로그는 통계다. 이 언어에 익숙해질수록 시스템은 더 이상 검은 상자가 아니다. 어디서 느려지는지, 어디서 실패가 커지는지, 어디를 바꾸면 효과가 큰지 보이기 시작한다.
다음 장에서는 이 감각을 사용자의 화면 가까이로 옮겨 간다. 프론트엔드 개발에서는 좌표, 비율, 보간, 그래프, 실험 지표가 어떻게 사용자 경험을 만드는지 살펴볼 것이다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
