첫 백엔드 채용 전에 하는 Go 부하 테스트
Go 부하 테스트로 현실적인 트래픽을 재현하고 p95 지연 시간, DB 압력, 메모리, 실패율을 측정한 뒤 채용을 결정하세요.

월간 활성 사용자 1만 명은 용량 요구 사항이 아닙니다. 결제나 분석에 쓰는 수치입니다. 요청이 가볍고 한 달 내내 분산되어 있으면 Go 서비스는 그보다 훨씬 많은 사용자를 처리할 수 있습니다. 반대로 각 세션이 느린 쿼리, 업로드, 외부 서비스 호출을 연달아 일으킨다면 몇백 명만으로도 무너질 수 있습니다.
쓸모 있는 질문은 생성된 백엔드가 예상 피크 트래픽에서 정해진 서비스 목표를 충족하고, 성장과 평범한 장애 하나를 감당할 여유가 있는가입니다. 백엔드 엔지니어를 채용하기 전에도 답할 수 있지만, 부하 테스트가 실제 제품과 닮아야 하며 API, Go 런타임, PostgreSQL의 움직임을 동시에 기록해야 합니다. 평균 지연 시간 그래프가 녹색이라는 사실은 거의 아무것도 증명하지 못합니다.
생성된 Go 백엔드가 월간 활성 사용자 10,000명을 감당할 준비가 됐다고 창업자에게 말하기 전에 저는 이 테스트를 요구합니다. 반복 가능한 합격 또는 실패 결과를 만들고, 첫 번째 병목을 드러내며, 용량 문제와 정확성 문제를 분리하는 테스트입니다.
월간 사용자를 피크 요청량으로 바꿔야 한다
부하 수준을 고르기 전에 사용자 예측을 초당 요청 수로 바꾸세요. 월간 활성 사용자 수는 백엔드를 움직이는 두 변수를 감춥니다. 가장 바쁜 구간에 몇 개의 세션이 들어오는지, 세션 하나가 얼마만큼의 작업을 만드는지입니다.
비공개 베타 트래픽이 있다면 관측 데이터부터 사용합니다. 가장 바쁜 15분의 세션 수, 세션당 요청 수, 경로 구성을 세세요. 아직 트래픽이 없다면 누구나 반박할 수 있도록 가정을 적으세요. 예를 들어 활성 사용자 10,000명이 한 달에 여덟 번 세션을 만들고, 세션당 API 요청이 15번이며, 일일 트래픽의 20퍼센트가 가장 바쁜 한 시간에 몰린다고 가정해 보겠습니다. 평범하게 바쁜 날에는 약 8 requests/s입니다. 출시, 알림, 급여 마감, 공통 시간대는 실제 피크를 몇 배로 높일 수 있습니다.
이 계산을 거짓 정밀도로 만들지 마세요. 세 가지 테스트 수준을 정하는 데 사용합니다.
- 예상 피크: 현재 예측하는 가장 바쁜 부하.
- 성장 피크: 사업 측의 더 나은 전망이 없다면 예상 피크의 두 배.
- 스트레스 수준: 서비스 목표가 실패하거나 리소스가 포화될 때까지 트래픽을 높이는 수준.
예상 피크와 성장 테스트는 계획한 출시에 여유가 있는지 답합니다. 스트레스 테스트는 무엇이 먼저 깨지고 사용자에게 실패가 어떻게 보이는지 알려 줍니다. 초과 작업을 빠르게 거부하는 서비스는 모든 DB 연결을 소모하고 무관한 경로까지 멈추는 서비스보다 운영하기 쉽기 때문에 마지막 답이 중요합니다.
용량 테스트에는 열린 워크로드 모델을 사용하세요. 열린 모델은 이전 요청이 느려져도 고정된 도착률로 새 요청을 시작합니다. 고정된 가상 사용자 수를 쓰는 닫힌 모델은 붕괴를 자주 숨깁니다. 응답이 느려지면 가상 사용자가 새 요청을 덜 보내므로, 서비스가 힘들어지는 바로 그 순간에 제공 부하도 낮아집니다. Grafana의 k6 문서는 도착률 executor로 이 차이를 구분하고, 생성기가 예정된 작업을 시작하지 못하면 dropped_iterations를 보고합니다. 누락된 iteration은 서버 성공이 아니라 테스트 생성기의 실패로 취급하세요.
준비 구간이 끝난 뒤 각 안정 수준을 최소 30분 유지하세요. 5분 테스트는 연결 교체, garbage collection 주기, cache 축출, background job, 점진적인 메모리 증가를 놓칩니다. 짧은 테스트가 통과하면 예상 피크에서 별도의 2시간 soak test를 실행하세요.
작은 계산표에 변환 과정을 쓰고 모든 단위를 남기세요. 월간 사용자 수에 사용자당 세션과 세션당 요청을 곱하면 월간 요청이 나옵니다. 트래픽을 운영일과 가장 바쁜 시간에 배정한 뒤 나누세요. 사용자 분석에서 빠질 수 있는 retry 트래픽, background job, webhook, polling도 더합니다. 10초마다 polling하는 frontend는 페이지를 연 click보다 더 많은 API 작업을 만들 수 있습니다.
순간 급증은 안정 피크와 별도로 모델링하세요. 알림 뒤의 login, import 완료, 짧은 장애 뒤의 client retry는 1분에 작업을 집중시킬 수 있습니다. 예상 급증률에 빠르게 도달하고, queue가 채워질 만큼 유지한 뒤, 정상으로 돌아오는 단계를 추가하세요. 서비스는 쌓이는 backlog나 수동 restart 없이 회복해야 합니다. 회복 시간도 결과로 기록하세요. 짧은 급증이 pool이나 worker를 막아 버리면 안정 테스트를 통과해도 안전하지 않습니다.
최종 비율에 임의의 안전 계수를 곱한 뒤 현실적이라고 부르지 마세요. 예측 오차, 계획된 campaign, 한 replica의 중단, 용량 추가에 걸리는 시간처럼 사업 불확실성과 여유를 연결하세요. 의존하려는 가정은 모두 테스트해야 합니다. 계산표를 결과와 함께 보관하세요. 어떤 전망과 급증 가정이 목표를 만들었는지 아무도 기억하지 못한다면 합격 결과는 의미를 잃습니다.
트래픽 구성은 실제 세션과 닮아야 한다
현실적인 부하 테스트는 경로 빈도, payload 크기, 인증, 데이터 분포, 사용자 대기 시간, 쓰기 경합을 유지합니다. /health를 초당 100번 호출하면 health handler만 측정할 뿐 애플리케이션은 측정하지 않습니다.
가능하다면 access log로 구성을 만드세요. raw URL이 아니라 업무 행동으로 경로를 묶습니다. /projects/123과 /projects/456은 같은 형태입니다. 초기 SaaS의 가능한 구성은 목록과 상세 조회 45퍼센트, 검색 20퍼센트, 생성 또는 수정 15퍼센트, login과 token refresh 10퍼센트, export와 다른 무거운 작업 10퍼센트입니다. 실제 수치는 이 예시가 아니라 제품 흐름에서 나와야 합니다.
많은 테스트 계정과 레코드를 사용하세요. 한 계정을 재사용하면 비현실적으로 뜨거운 cache가 생기거나, 한 row의 update가 직렬화되거나, 실제 트래픽이라면 분산될 rate limit이 걸릴 수 있습니다. 소형, 중형, 대형 tenant를 seed하세요. 누락된 record, 잘못된 input, authorization failure도 포함합니다. error path는 성공 path와 다른 DB query를 실행하거나 response body를 다르게 할당할 수 있습니다.
대용량 upload와 긴 export의 서비스 목표가 다르면 별도 scenario에 넣으세요. 그래도 정상 트래픽과 동시에 실행해야 합니다. 그렇지 않으면 사용자가 실제로 알아차리는 사고를 놓칩니다. 한 종류의 export가 pool을 차지하는 동안 단순한 설정 페이지가 뒤에서 기다리는 상황입니다.
최종 용량 테스트에서는 PostgreSQL, object storage, queue, outbound service를 mock으로 바꾸지 마세요. mock은 handler 비용을 분리하는 데 유용하지만 용량을 정할 가능성이 높은 의존성을 제거합니다. production과 같은 instance size, DB 설정, index, connection limit, network path를 가진 staging stack을 사용하세요. 똑같은 seed row 천 개보다 정리한 production 형태의 데이터가 낫습니다.
API가 보통 cache되지 않는다면 content delivery cache를 거쳐 테스트하지 마세요. 반대로 production이 실제 cache를 사용한다면 경로에 남겨 두세요. 백엔드를 바빠 보이게 만드는 것이 목적이 아닙니다. 사용자 요청이 실제로 만드는 작업을 재현하는 것이 목적입니다.
실행 가능한 k6 테스트에 계약을 담아야 한다
threshold와 트래픽 단계를 version control에 저장해서 테스트 결과가 나중에 해석하는 screenshot이 되지 않게 하세요. k6는 threshold를 합격 또는 실패 기준으로 취급하고, 실패하면 0이 아닌 종료 code를 반환합니다. 따라서 release check로 사용할 수 있습니다.
다음 뼈대는 도착률로 혼합 세션을 실행하고, response의 의미를 검사하며, 일반 조회와 무거운 export에 서로 다른 latency limit을 둡니다. route, payload, target은 제품에서 합의한 값으로 바꾸세요. 실패한 check를 사용자 행동과 연결할 수 있도록 코드는 일부러 단순하게 작성했습니다.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const businessErrors = new Rate('business_errors');
export const options = {
scenarios: {
expected_peak: {
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 40,
maxVUs: 200,
stages: [
{ target: 10, duration: '5m' },
{ target: 10, duration: '30m' },
{ target: 20, duration: '10m' },
{ target: 20, duration: '30m' },
],
},
},
thresholds: {
'http_req_duration{name:project_list}': ['p(95)<300'],
'http_req_duration{name:project_create}': ['p(95)<500'],
'http_req_duration{name:export}': ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
business_errors: ['rate<0.005'],
dropped_iterations: ['count==0'],
},
};
export function setup() {
const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), { headers: { 'Content-Type': 'application/json' } });
check(response, { 'login succeeds': r => r.status === 200 });
return { token: response.json('token') };
}
export default function (data) {
const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
headers,
tags: { name: 'project_list' },
});
businessErrors.add(!check(list, {
'list status is 200': r => r.status === 200,
'list has items': r => Array.isArray(r.json('items')),
}));
if (Math.random() < 0.25) {
const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
name: `load-${__VU}-${__ITER}`,
}), { headers, tags: { name: 'project_create' } });
businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
}
if (Math.random() < 0.03) {
const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
headers,
tags: { name: 'export' },
});
businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
}
sleep(Math.random() * 2 + 1);
}
예시 target은 출발점이지 보편적인 약속이 아닙니다. 사용자가 견딜 수 있는 지연과 제품 요구 사항에 따라 경로 종류별 p95를 정하세요. 비동기 export와 autocomplete 요청을 하나의 300 ms global threshold로 판단해서는 안 됩니다.
애플리케이션을 host하지 않는 machine에서 script를 실행하세요. generator에 spare CPU가 있고 dropped iteration이 없는지 확인합니다. 정확한 commit, environment configuration, data snapshot identifier, command, raw test output을 보관하세요. 이것이 없으면 나중 비교는 기억과 낙관에 의존합니다.
긴 실행을 신뢰하기 전에 generator를 교정하세요. DB 작업이 없는 작은 handler로 향하게 하고, 요청 도착률을 계획한 테스트보다 높인 뒤 자체 CPU, socket, network를 소진하지 않고 유지하는지 확인합니다. k6가 virtual user를 추가하거나 dropped iteration을 보고하면 load machine이 한계일 수 있습니다. 한 machine이 필요한 작업을 제공하지 못할 때만 여러 machine으로 분산하고, server와 client graph가 맞도록 clock을 동기화하세요.
각 실행에 조용한 baseline을 주세요. 실제 peak에서도 실행되는 작업이 아니라면 migration, data import, unrelated staging job을 멈춥니다. 그다음 진짜 background work를 활성화한 두 번째 테스트를 예약하세요. 이 두 결과가 깨끗한 API 용량과 사용자가 받을 운영 용량을 보여 줍니다. 조용한 실행만 통과하면 출시 계획은 가짜 staging 상태에 의존합니다.
사용자 여정 하나를 위한 두 번째 script를 만들고 부하를 넣기 전에 virtual user 한 명으로 실행하세요. 모든 response, 생성된 record, cleanup action을 검사합니다. 잘못된 token, 항상 true인 check, 첫 iteration 뒤 충돌하는 test data를 잡아냅니다. script가 error page를 시험하거나 같은 cached object만 읽고 있다면 용량 결과에 의미가 없습니다.
p95에는 경로별 맥락이 필요하다
평균은 느린 소수를 숨기므로 p95 latency를 사용하되, 단독으로 읽지 마세요. p95가 800 ms라면 요청 20개 중 하나는 적어도 그만큼 걸립니다. 여러 요청을 쓰는 페이지는 계속 느리게 느껴질 수 있습니다. 샘플이 적은 경로에서는 percentile도 불안정하므로 request count를 함께 보고하세요.
이름을 붙인 각 업무 행동마다 p50, p95, p99, maximum, throughput, error rate를 기록합니다. p50은 평상시 행동을, p95는 실용적인 service gate를, p99는 단일 maximum이 논의를 지배하지 않으면서 꼬리를 보여 줍니다. status code별로 결과를 분리하세요. 빠른 500 response가 latency를 좋게 보여서는 안 됩니다.
client-observed duration뿐 아니라 server-side handler duration도 측정합니다. 둘의 차이에는 connection setup, proxy, network time, response transfer가 포함됩니다. client p95가 오르는데 handler p95가 일정하면 handler 밖을 찾으세요. 둘 다 오르고 DB wait time도 증가한다면 요청이 connection이나 query를 기다리고 있을 가능성이 높습니다.
warm-up과 steady-state 결과를 분리하세요. 배포된 Go binary에서 compilation은 일어나지 않지만 cold cache, 새 DB connection, lazy initialization, autoscaling이 첫 몇 분을 왜곡할 수 있습니다. 사용자도 cold behavior를 겪으므로 지우지 말고 별도 결과로 남깁니다.
평균은 resource accounting에 여전히 유용합니다. total database time을 calls로 나누면 적당히 느리고 매우 빈번한 query를 찾을 수 있습니다. 하지만 percentile service target을 대신하지는 못합니다. 현장에서는 latency와 capacity를 자주 혼동합니다. latency는 완료된 작업의 시간을, capacity는 queue나 error가 늘지 않게 감당할 수 있는 offered work를 뜻합니다. 낮은 achieved request rate에서 좋은 latency는 capacity를 증명하지 않습니다.
실행 전에 실패를 정의하세요. critical route가 p95 target을 놓치거나, unexpected HTTP failure가 합의한 rate를 넘거나, business check가 실패하거나, 예정 iteration이 누락되거나, resource가 계속 saturated되면 저는 launch test를 실패 처리합니다. 다섯 gate 중 네 개를 통과했다면 useful diagnostic data를 얻은 failed run입니다.
연결 대기가 숨은 DB 큐를 드러낸다
application latency만으로는 PostgreSQL이 느린지, application이 접근을 기다리는지 알 수 없으므로 test 전에 database/sql을 계측하세요. Go의 DB.Stats()는 closure counter와 함께 OpenConnections, InUse, Idle, WaitCount, WaitDuration을 제공합니다. 몇 초마다 metrics system으로 export합니다.
func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s := db.Stats()
g.Set("db_open_connections", float64(s.OpenConnections))
g.Set("db_in_use_connections", float64(s.InUse))
g.Set("db_idle_connections", float64(s.Idle))
g.Set("db_wait_count_total", float64(s.WaitCount))
g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
}
}
}
안정 구간 동안 누적 counter의 변화를 계산하세요. WaitCount가 증가하면 요청이 빈 connection을 기다린 것입니다. WaitDuration 변화량을 WaitCount 변화량으로 나누면 그 구간의 평균 pool wait가 나옵니다. InUse와 MaxOpenConnections를 함께 그리세요. limit에서 평평한 선을 유지하며 wait가 증가한다면 pool saturation입니다.
graph를 좋아 보이게 하려고 SetMaxOpenConns를 늘리지 마세요. 이 인기 있는 해결책은 queue를 PostgreSQL로 옮기고 contention, memory use, query latency를 높일 수 있습니다. connection이 오래 사용되는 이유부터 찾으세요. slow query, network call 중에도 열린 transaction, row를 하나씩 읽는 코드, 누락한 Rows.Close()가 원인일 수 있습니다. 그런 다음 모든 application replica와 worker의 database connection budget 안에서 pool size를 정합니다.
Go documentation은 0 이하의 SetMaxOpenConns가 pool을 무제한으로 둔다고 설명합니다. 여러 replica가 동시에 connection을 열 수 있는 production에서는 위험한 default입니다. explicit limit을 설정하고, idle과 lifetime behavior를 의도적으로 정하며, migration, administration, background work에 쓸 capacity를 남겨 두세요.
transaction duration도 따로 추적하세요. handler가 200 ms에 돌아와도 deferred cleanup이나 leaked transaction이 connection을 더 오래 잡을 수 있습니다. pool metrics는 압력을 보여 주고 trace나 transaction timing은 원인을 보여 줍니다.
느린 쿼리의 증거는 PostgreSQL에서 얻는다
test environment에서 pg_stat_statements를 활성화하고 각 run 전후로 snapshot을 찍으세요. PostgreSQL documentation은 정규화된 statement의 planning과 execution statistics를 추적하는 기능이라고 설명합니다. view에는 calls, rows, total 및 mean execution time, block activity, temporary block activity가 있습니다. 한 trace에 나타난 query를 보고 추측하는 것보다 훨씬 좋은 증거입니다.
view는 누적되므로 snapshot 차이를 사용하세요. reset은 다른 작업의 증거를 지우므로 isolated test database에서만 해야 합니다. 다음 query는 깨끗한 test window에서 execution time을 가장 많이 사용한 statement를 찾습니다.
SELECT
queryid,
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
shared_blks_read,
temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;
total time은 자주 실행되어 DB 작업을 지배하는 query를 찾습니다. mean time은 개별적으로 느린 statement를 찾습니다. pg_stat_statements는 calls를 aggregate하므로 둘 다 query p95를 제공하지 않습니다. 특정 statement의 tail latency가 중요하다면 tracing이나 duration histogram을 사용하세요. slow query라는 말은 높은 평균 실행 시간, 높은 tail time, 빈번한 call로 인한 거대한 total cost를 섞기 쉽습니다. 원인마다 고치는 방법이 다릅니다.
상위 statement에는 시간 측정 load test 밖에서 안전하고 대표적인 parameter로 EXPLAIN (ANALYZE, BUFFERS)를 실행하세요. ANALYZE는 statement를 실제로 실행하므로 data-changing statement는 rollback할 transaction으로 감싸거나 버릴 수 있는 copy에서 조사합니다. estimated row와 actual row의 큰 차이, repeated loop, 큰 selective table의 sequential scan, disk로 넘치는 sort, 많은 shared block read를 찾으세요.
generated code는 seed data에서 멀쩡해 보이는 N+1 pattern을 자주 만듭니다. project 25개를 가져온 뒤 project마다 owner query와 count query를 실행합니다. 초당 요청 10개라면 다른 route가 실행되기 전에 이 endpoint만 초당 500개가 넘는 DB statement를 만들 수 있습니다. join, batched WHERE id = ANY($1), precomputed count가 해결책일 수 있습니다. pool limit을 높여도 낭비는 남습니다.
lock wait도 기록하세요. 혼자서는 빠른 query가 같은 tenant, account, sequence에 대한 concurrent update에서 멈출 수 있습니다. write scenario에서만 latency가 뛰면 반사적으로 index를 추가하지 말고 active wait와 transaction boundary를 확인하세요.
일정한 부하에서 메모리가 안정되어야 한다
한 번의 peak가 아니라 시간에 따른 모양으로 memory를 판단하세요. warm-up 중 증가한 뒤 stable level 주변을 오가는 Go process와 2시간 soak 내내 post-GC baseline이 오르는 process는 다릅니다.
process resident memory, Go heap allocation, heap object, goroutine count, garbage collection frequency, pause time, allocation rate를 기록하세요. OS는 Go heap만이 아니라 container limit을 기준으로 process를 kill하므로 container memory가 중요합니다. 비슷한 load에서 GC 직후 memory를 비교하면 정상 sawtooth를 상당 부분 제거하고 retention을 볼 수 있습니다.
standard Go runtime metrics나 protected profiling endpoint를 staging에 노출하세요. soak 시작과 끝 근처에서 heap profile을 찍고 go tool pprof로 retained allocation site를 비교합니다. goroutine profile도 찍으세요. 증가하는 goroutine은 channel에 멈춘 request, 닫히지 않은 response body, cancellation 없이 시작한 background work를 드러낼 수 있습니다.
GOMEMLIMIT을 container limit과 정확히 같게 두지 마세요. process는 goroutine stack, executable mapping, database driver buffer, 다른 non-heap allocation에도 memory가 필요합니다. 여유를 남기고 가장 큰 payload로 증명하세요. 작은 JSON body의 test는 20 MB upload를 memory로 읽는 endpoint에 대해 거의 알려 주지 않습니다.
불편한 경우를 강제로 실행하세요. 최대 accepted request body, 큰 query result, cancelled client, timeout, repeated export입니다. 작업 완료 뒤 memory가 돌아오는지 확인합니다. heavy garbage collection은 memory를 limit 아래로 유지하면서 latency를 망칠 수 있으므로 CPU도 감시하세요.
실용적인 합격 조건은 ceiling과 trend를 결합합니다. growth peak에서 resident memory가 deployment limit보다 안전하게 낮아야 하고, soak 중 post-collection baseline과 goroutine count가 더는 오르지 않아야 합니다. universal safe percentage는 없습니다. platform restart behavior, traffic variance, 다른 replica가 restart를 감당할 수 있는지에 따라 여유를 고르세요.
실패에는 잘못된 답과 과부하 동작도 포함된다
transport failure, HTTP status failure, timeout, panic, wrong response를 따로 세세요. http_req_failed는 k6 response callback에 따라 failed HTTP request를 잡지만, empty list, duplicate charge, missing record를 담은 200도 실패입니다. sample이 content check로 business_errors를 만드는 이유입니다.
invalid input이나 intentional rate limit 같은 expected rejection에 tag를 붙여 unexpected error rate를 오염시키지 마세요. 그 계약도 assert합니다. 올바른 status, bounded body, 빠른 rejection입니다. overloaded system이 503을 반환하는 데 30초를 써서는 안 됩니다.
server log에서 panic recovery, context deadline error, connection acquisition delay, PostgreSQL serialization failure, cancelled query를 찾으세요. identifier가 수천 category를 만들지 않도록 full message가 아닌 stable cause로 error를 group합니다. 첫 occurrence와 high-latency tail의 representative trace를 저장하세요.
clean capacity test 뒤에 degradation test 하나를 실행하세요. available database connection을 줄이거나, outbound dependency에 controlled latency를 추가하거나, traffic 중 application replica 하나를 restart합니다. isolated test environment에서만 해야 합니다. timeout, cancellation, health check가 failure를 가두고 queue가 모든 resource를 소비하지 못하게 하는지 확인하는 것이 목표입니다.
HTTP server를 명시적으로 설정하세요. Go의 net/http documentation에 따르면 field에 따라 0 또는 음수의 ReadTimeout, WriteTimeout, IdleTimeout은 timeout이 없다는 뜻일 수 있습니다. generated service는 종종 default 값으로 http.ListenAndServe를 호출하고 결정하지 않습니다. custom http.Server, request-scoped deadline, bounded body size는 느린 client와 멈춘 dependency가 resource를 영원히 잡지 못하게 합니다.
실행 뒤 정확성을 검토하세요. 생성된 record를 세고, client retry가 있었을 때 idempotency를 확인하고, background job이 정확히 한 번 끝났는지 확인하며, failed request의 partial state가 남지 않았는지 봅니다. 제 프로젝트에서 load test는 영리한 code review보다 duplicate-work bug를 더 많이 찾았습니다.
첫 번째 한계가 채용 결정을 만든다
system이 expected와 growth test를 반복해서 통과하고, soak가 stable memory baseline에 도달하고, database queue가 통제되고, team이 첫 stress failure를 설명할 수 있으면 backend engineer 없이 출시할 수 있습니다. 운 좋게 나온 green run 하나는 증거가 아닙니다. 같은 commit을 최소 세 번 실행하고 큰 variance를 조사하세요.
각 run의 간단한 결과 기록을 남기세요.
- replica size와 database configuration을 포함한 commit과 environment.
- dataset scale, traffic mix, arrival rate, test duration.
- route p95와 p99, achieved throughput, business failure, dropped iteration.
- peak CPU와 memory, post-collection memory trend, goroutine trend.
- pool wait, total time 기준 top SQL, lock wait, 관측한 breaking point.
rising pool wait, retained memory, lock contention, inconsistent write를 아무도 설명하지 못한다면 launch 전에 backend help를 고용하거나 계약하세요. test를 실행할 수 있는 유일한 사람이 generated code를 안전하게 바꿀 수 없어도 채용해야 합니다. 이것은 requests-per-second threshold가 아니라 ownership gap입니다.
failed test가 곧바로 full-time hire를 정당화하지는 않습니다. missing index, N+1 query, unbounded export는 제한된 수리일 수 있습니다. transaction design, observability, cancellation, deployment behavior 전반의 반복 실패는 지속적인 engineering work를 뜻합니다. 한 defect를 찾은 것인지, system behavior를 소유한 사람이 없다는 사실을 발견한 것인지가 차이입니다.
Koder.ai는 Go backend를 생성하고 source를 export하며, deploy하고 rollback용 snapshot을 보관할 수 있습니다. 하지만 generation이 capacity planning을 없애지는 않습니다. load script와 observability change를 source와 함께 두어 중요한 backend change가 같은 contract를 통과하게 하세요.
사업팀에 10,000 monthly user가 안전하다고 약속하지 마세요. measured arrival rate, route mix, latency target, error budget, resource envelope를 약속하세요. product가 바뀌면 input을 바꾸고 test를 다시 실행합니다.
자주 묻는 질문
Go 백엔드는 월간 활성 사용자 10,000명을 처리할 수 있나요?
그런 경우가 많지만 monthly user는 backend load를 설명하지 않습니다. 예측을 peak requests per second와 route mix로 바꾸고 명확한 latency, error, database, memory limit에 맞춰 시험하세요.
월간 사용자 10,000명은 초당 몇 요청인가요?
고정 변환식은 없습니다. 사용자당 session, session당 request, 가장 바쁜 구간의 traffic share, 사용자를 동시에 불러오는 event를 알아야 합니다.
Go API에 허용할 p95 latency는 얼마인가요?
language나 framework가 아닌 user action에 따라 target을 정하세요. interactive read는 수백 ms, accepted background work는 다른 target이 필요할 수 있지만 결과를 보기 전에 수치를 정해야 합니다.
backend load test는 얼마나 오래 실행해야 하나요?
warm-up 뒤 각 steady load를 최소 30분 유지하고 expected peak에서 더 긴 soak를 실행하세요. 짧은 test는 connection churn, retained memory, background work, gradual queue growth를 놓칠 수 있습니다.
virtual user와 arrival rate 중 무엇을 써야 하나요?
capacity claim에는 service가 느려져도 작업을 계속 제공하는 arrival-rate model을 쓰세요. fixed virtual-user model은 slowdown에서 request rate를 낮추고 queue growth를 숨길 수 있습니다.
Go DB connection pool 고갈은 어떻게 찾나요?
DB.Stats()를 export하고 InUse, OpenConnections, WaitCount, WaitDuration을 감시하세요. in-use connection이 maximum에 머물며 wait counter가 늘면 request가 pool을 기다립니다.
request가 기다리면 Go SQL pool을 늘려야 하나요?
connection이 계속 사용되는 이유와 PostgreSQL capacity를 알기 전에는 안 됩니다. 큰 pool은 queue를 database로 옮기고 contention을 더 나쁘게 만들 수 있습니다.
load test 중 느린 PostgreSQL query를 어떻게 찾나요?
pg_stat_statements의 before-and-after snapshot을 찍고 query delta를 total 및 mean execution time으로 정렬하세요. aggregate view는 per-query p95를 주지 않으므로 tail latency에는 trace나 histogram을 씁니다.
Go service의 memory leak은 어떻게 판단하나요?
steady soak를 실행하고 비슷한 load에서 GC 후 memory를 비교하세요. heap object나 goroutine과 함께 baseline이 계속 오르면 profile 비교가 필요합니다.
startup은 언제 backend engineer를 채용해야 하나요?
performance failure가 database design, observability, concurrency, correctness, operations의 지속적인 ownership을 요구할 때 채용하세요. index 하나는 full-time role이 필요 없을 수 있지만 load에서 설명 못 하는 동작은 담당자가 필요합니다.