GET
서버 상태를 바꾸지 않고 리소스를 조회한다. 캐싱과 재시도에 가장 친화적이다.
목록 조회, 상세 조회, 검색 조건이 URL로 표현될 때분류: Layer 1 - 백엔드 기초 | 작성일: 2026-03-21
HTTP(HyperText Transfer Protocol)는 클라이언트와 서버가 데이터를 주고받기 위한 요청-응답 기반의 통신 규약이다.
BackOps에서 다루는 거의 모든 서비스는 HTTP 위에서 동작한다. API 호출이 실패했을 때, 배포 후 서비스가 안 될 때, 외부 연동이 안 될 때 — 원인을 파악하려면 HTTP가 어떻게 동작하는지 알아야 한다. 이걸 모르면 에러 로그를 봐도 뭐가 문제인지 판단이 안 된다.
요청(Request)과 응답(Response)
클라이언트가 요청을 보내면 서버가 응답을 돌려준다. 항상 이 순서. 요청에는 메서드, URL, 헤더, 바디가 있고 응답에는 상태 코드, 헤더, 바디가 있다.
HTTP 요청이 실제로 어떻게 도달하는가 — 내부 동작 원리
“브라우저에서 URL을 치면 서버가 응답한다”는 사실 뒤에 이런 단계가 숨어 있다:
api.example.com → 203.0.113.5 IP 주소로 변환 (전화번호부를 찾는 것과 같다)sequenceDiagram participant Client as Browser or Client participant DNS participant Edge as LB or Web Server participant App as API Server participant DB Client->>DNS: hostname 조회 DNS-->>Client: IP 주소 반환 Client->>Edge: TCP 3-way handshake Client->>Edge: TLS handshake Client->>App: HTTP request App->>DB: 필요 시 데이터 조회 DB-->>App: rows App-->>Client: HTTP status + headers + body
📖 더 보기: Life Cycle of an HTTP Request — 위 1~6단계 흐름을 다이어그램과 함께 설명하는 입문 가이드 (입문)
curl로 실제 HTTP 요청 확인하기
# -v 옵션으로 전체 요청/응답 헤더를 볼 수 있다curl -v https://httpbin.org/get# 예상 출력 (핵심 부분만)* Trying 54.91.xxx.xxx:443...* Connected to httpbin.org (54.91.xxx.xxx) port 443* SSL handshake done> GET /get HTTP/1.1 ← 요청 메서드 + 경로 + 버전> Host: httpbin.org ← 요청 헤더> User-Agent: curl/8.5.0> Accept: */*>< HTTP/1.1 200 OK ← 응답 상태 코드< Content-Type: application/json< Content-Length: 340<{ "headers": { "Host": "httpbin.org", "User-Agent": "curl/8.5.0" }, "url": "https://httpbin.org/get"}NestJS에서 HTTP 상태 코드 제어
// 기본값: @Get()은 200, @Post()는 201을 자동으로 반환@Controller("users")export class UsersController { @Post() @HttpCode(201) // 명시적으로 설정 가능 create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); }
@Delete(":id") @HttpCode(204) // 삭제 성공: 바디 없이 204만 반환 remove(@Param("id") id: string) { return this.usersService.remove(id); }}# 예상 출력 (DELETE 요청 시)HTTP/1.1 204 No ContentHTTP 메서드
메서드는 요청의 의도를 서버에 전달한다. 실무에서는 “조회인지, 생성인지, 전체 교체인지, 일부 변경인지, 삭제인지”를 먼저 나누고 캐싱·재시도 가능성을 함께 본다.
서버 상태를 바꾸지 않고 리소스를 조회한다. 캐싱과 재시도에 가장 친화적이다.
목록 조회, 상세 조회, 검색 조건이 URL로 표현될 때서버에 새 처리를 요청한다. 생성뿐 아니라 명령 실행처럼 결과가 매번 달라질 수 있는 작업에도 쓴다.
회원가입, 결제 요청, 복잡한 검색 body가 필요할 때리소스 전체 표현을 교체한다. 같은 요청을 여러 번 보내도 최종 상태가 같아야 한다.
프로필 전체 수정처럼 클라이언트가 완전한 새 상태를 알고 있을 때리소스 일부 필드만 변경한다. 전체 상태를 모를 때도 작은 변경을 표현할 수 있다.
이름만 변경, 알림 설정 하나만 토글할 때리소스 삭제를 요청한다. 성공 시 바디 없이 204 No Content를 돌려주는 패턴이 흔하다.
사용자 탈퇴, 세션 제거, 임시 리소스 삭제처럼 제거 의도가 명확할 때상태 코드 (외워야 할 것들)
200 OK: 성공201 Created: 생성 성공204 No Content: 성공이지만 응답 바디 없음 (DELETE 성공 시 자주 사용)304 Not Modified: 캐시된 리소스가 아직 유효함 (ETag 검증 통과)400 Bad Request: 요청이 잘못됨 (클라이언트 실수)401 Unauthorized: 인증 안 됨 (로그인 안 함)403 Forbidden: 권한 없음 (로그인은 했지만 접근 불가)404 Not Found: 리소스 없음429 Too Many Requests: 요청 횟수 초과 (Rate Limiting)500 Internal Server Error: 서버 내부 오류502 Bad Gateway: 중간 서버(프록시/로드밸런서)가 뒤에 있는 서버로부터 잘못된 응답을 받음503 Service Unavailable: 서버 과부하 또는 점검 중504 Gateway Timeout: 중간 서버가 뒤에 있는 서버의 응답을 기다리다 타임아웃퀴즈
힌트: HTTP 상태 코드는 디버깅 출발점을 좁히는 신호다.
401은 신원을 증명하지 못한 상태라 토큰 존재와 만료를 먼저 보고, 403은 신원은 확인됐지만 권한이 부족한 상태라 역할과 정책을 봐야 한다.
헤더(Header)
요청/응답에 대한 메타데이터. Content-Type(데이터 형식), Authorization(인증 정보), Cache-Control(캐싱 규칙) 등이 자주 쓰인다.
Stateless(무상태)
HTTP는 각 요청이 독립적이다. 서버는 이전 요청을 기억하지 않는다.
그래서 인증이 필요한 모든 요청마다 Authorization 헤더를 포함해야 한다.
Session/Cookie가 필요한 이유도 이 stateless 성질 때문이다.
용어
HTTP 서버가 이전 요청을 기억하지 않는다는 뜻이다. 그래서 인증, 추적 ID, 멱등성 키처럼 다음 판단에 필요한 정보는 매 요청마다 다시 전달해야 한다.
실무에서는 Authorization 헤더, Cookie, Idempotency-Key, X-Request-Id가 이 한계를 보완한다.HTTPS
HTTP + TLS 암호화. 데이터가 중간에 탈취당해도 읽을 수 없게 한다. 현재 거의 모든 서비스가 HTTPS를 사용한다.
HTTP/2 — 왜 HTTP/1.1보다 빠른가
비유하자면, HTTP/1.1은 “한 줄짜리 창구”이다. 요청 하나가 끝나야 다음 요청을 할 수 있다. HTTP/2는 “여러 창구를 동시에 열어두는 것”이다.
HTTP/1.1의 문제점: 하나의 TCP 연결에서 요청을 직렬로 처리한다. 브라우저는 이를 우회하려고 보통 6~8개의 TCP 연결을 병렬로 열지만, 이것은 서버 리소스를 낭비한다.
HTTP/2가 이를 해결하는 방법:
HTTP/1.1 연결 흐름 (직렬):클라이언트 ──요청1──► 서버클라이언트 ◄──응답1─── 서버클라이언트 ──요청2──► 서버 ← 응답1 올 때까지 기다림클라이언트 ◄──응답2─── 서버
HTTP/2 연결 흐름 (멀티플렉싱):클라이언트 ──요청1(stream1)──► 서버클라이언트 ──요청2(stream2)──► 서버 ← 동시에 보냄클라이언트 ──요청3(stream3)──► 서버클라이언트 ◄──응답2(stream2)─── 서버 ← 먼저 처리된 것 먼저 옴클라이언트 ◄──응답1(stream1)─── 서버NestJS는 기본적으로 HTTP/1.1로 동작하며, AWS ALB(Application Load Balancer)가 클라이언트와 HTTP/2로 통신하고 내부적으로 HTTP/1.1로 NestJS에 전달하는 방식이 일반적이다.
📖 더 보기: HTTP/2 vs HTTP/1.1 — Cloudflare — 멀티플렉싱과 헤더 압축을 다이어그램으로 설명하는 공식 가이드 (입문)
HTTP Keep-Alive — 연결을 재사용해서 성능을 높이는 방법
비유하자면, Keep-Alive는 “전화 통화를 끊지 않고 여러 용건을 처리하는 것”이다. 매번 전화를 새로 거는(TCP 연결을 새로 맺는) 비용을 절약한다.
TCP 연결을 새로 맺을 때마다 3-way handshake + TLS handshake가 일어나고, 이것이 수십~수백 ms의 지연을 만든다. Keep-Alive를 사용하면 한 번 맺은 연결을 여러 요청에 재사용할 수 있다.
NestJS에서 마이크로서비스 간 HTTP 호출을 반복할 때 Keep-Alive를 사용하면 응답 속도가 크게 개선된다:
// NestJS HttpModule에서 Keep-Alive 설정import * as http from "http";import * as https from "https";
@Module({ imports: [ HttpModule.register({ httpsAgent: new https.Agent({ keepAlive: true }), httpAgent: new http.Agent({ keepAlive: true }), timeout: 5000, }), ],})export class AppModule {}# Keep-Alive 비교 (같은 서버에 10회 연속 요청)Without Keep-Alive: 각 요청마다 TCP+TLS 핸드셰이크→ 평균 응답 시간: ~250ms
With Keep-Alive: 연결 재사용→ 평균 응답 시간: ~50ms (첫 요청 이후 80% 단축)Node.js 서버의 기본 Keep-Alive 타임아웃은 5초로, AWS ALB의 기본 60초보다 짧다. ALB가 연결을 유지하려는데 NestJS 서버가 먼저 끊으면 502가 발생할 수 있다. NestJS 서버 타임아웃을 ALB보다 길게 설정해야 한다:
// main.ts — NestJS Keep-Alive 타임아웃을 ALB보다 길게 설정async function bootstrap() { const app = await NestFactory.create(AppModule); const server = app.getHttpServer(); server.keepAliveTimeout = 65000; // ALB 60초보다 길게 (65초) server.headersTimeout = 66000; // keepAliveTimeout보다 1초 더 길게 await app.listen(3000);}HTTP 캐싱 — 왜 304 응답이 중요한가
서버가 동일한 데이터를 계속 보내지 않도록 하는 메커니즘이다. 클라이언트와 서버가 합의한 기준으로 “이미 가진 데이터를 재사용해도 된다”고 판단하면 네트워크 전송을 건너뛸 수 있다.
ETag: "abc123" 포함 → 클라이언트가 다음 요청에 If-None-Match: "abc123" 포함 → 서버가 변경이 없으면 304 Not Modified 반환 (바디 없음)flowchart TD
A["첫 GET /users/1"] --> B["200 OK + ETag v1"]
B --> C["다음 요청에 If-None-Match v1 포함"]
C --> D{"서버 리소스가 바뀌었나?"}
D -->|아니오| E["304 Not Modified - 바디 없음"]
D -->|예| F["200 OK + 새 ETag + 새 바디"] HTTP/3(QUIC) 프로덕션 현황 — 2025년 35% 채택률의 의미
HTTP/3는 더 이상 미래 기술이 아니다. 2025년 10월 Cloudflare 데이터 기준으로 전 세계 트래픽의 35%가 HTTP/3로 전송된다. AWS CloudFront와 Cloudflare는 이미 HTTP/3를 기본 지원하므로, NestJS 백엔드 개발자도 이 계층의 동작을 이해해야 한다.
HTTP/2에 남아있던 문제: TCP 계층에서 하나의 패킷이 손실되면 모든 스트림이 기다려야 한다(TCP 수준의 Head-of-Line Blocking). HTTP/3의 QUIC는 각 스트림이 독립적이라서 하나의 패킷 손실이 다른 스트림에 전혀 영향을 주지 않는다.
HTTP/3 성능 이점 (2025년 실측 데이터):- TTFB(첫 바이트 수신 시간) 중앙값 41.8% 감소- 고패킷 손실(5%) 환경에서 로드 시간 47% 개선- 모바일 → Wi-Fi 전환 시 연결 유지 (Connection ID 기반)
적합한 환경:✅ 모바일 사용자가 많은 서비스 (네트워크 전환 잦음)✅ 불안정한 네트워크 환경 (이동 중 사용)✅ 고지연 국제 트래픽
효과 제한적인 환경:⚠️ 안정적인 내부망 마이크로서비스 간 통신⚠️ 동일 리전 내 서버-서버 통신BackOps에서 실용적인 시사점: NestJS 앱 자체는 여전히 HTTP/1.1 또는 HTTP/2로 동작하고, 엣지(ALB, CloudFront)가 클라이언트와 HTTP/3로 통신한다. 백엔드 코드 변경 없이 ALB/CloudFront 설정만으로 HTTP/3 혜택을 받을 수 있다.
# CloudFront에서 HTTP/3 지원 여부 확인curl -I --http3 https://your-cloudfront-domain.cloudfront.net/
# 응답 헤더에 다음이 있으면 HTTP/3 지원 중:# alt-svc: h3=":443"; ma=86400
# ALB가 HTTP/2를 지원하는지 확인curl -I --http2 https://api.example.com/health# 응답에 HTTP/2 200 이면 HTTP/2 활성화됨HTTP에서 배운 원리는 gRPC, WebSocket, 메시지 큐 등 다른 기술에도 그대로 적용된다. 원리를 이해하면 새 기술을 접할 때 “이게 왜 이렇게 동작하는가”를 빠르게 파악할 수 있다.
| HTTP 원리 | HTTP에서의 역할 | 다른 기술에서의 적용 |
|---|---|---|
| Stateless | 각 요청이 독립적, 서버가 이전 요청 기억 안 함 | gRPC Unary: 각 RPC 호출이 독립적 (Stateless RPC). 메시지 큐(SQS/Kafka): 각 메시지가 자기완결적, 브로커가 상태 보관 안 함. 이벤트 소싱: 이벤트마다 충분한 컨텍스트를 포함 |
| 멀티플렉싱 | HTTP/2: 하나의 TCP 연결에서 여러 스트림 동시 처리 | gRPC (HTTP/2 기반): 하나의 gRPC 채널에서 수백 개 동시 RPC 호출 처리. HTTP/3 QUIC: UDP 기반 독립 스트림으로 HOL 블로킹 완전 제거 |
| 연결 재사용 (Keep-Alive) | TCP+TLS 핸드셰이크 비용을 여러 요청에 분산 | DB 커넥션 풀: 연결 수립 비용 분산 (동일 원리). gRPC 채널 재사용: 채널 1개로 수백 동시 호출 처리, 채널 생성마다 TCP+TLS 비용 발생하므로 재사용 필수 (출처: gRPC Performance — Microsoft Learn) |
| 헤더 기반 메타데이터 | Content-Type, Authorization 등으로 메타데이터 전달 | gRPC 메타데이터(Metadata): HTTP/2 헤더와 동일 메커니즘. Kafka 레코드 헤더: 메시지 본문 외 메타데이터 전달 |
핵심 통찰: WebSocket은 HTTP 업그레이드 핸드셰이크로 시작하지만, 연결 후에는 Stateful 양방향 채널이 된다. HTTP/2 멀티플렉싱과 달리 WebSocket은 자체 멀티플렉싱이 없어 연결당 하나의 스트림만 처리한다. (출처: Communication Protocols — GetStream)
새 통신 기술을 분석하는 4질문 (HTTP에서 추출한 일반 공식)
위 표의 4축은 단순 매핑이 아니라 새 프로토콜을 만났을 때 가장 먼저 던져야 할 질문이다.
이 4질문을 gRPC에 그대로 적용해 보면: ① Unary RPC는 stateless ② 채널 1개에 동시 RPC N개 (HTTP/2 멀티플렉싱) ③ 채널 재사용 필수 (연결 비용 분산) ④ Metadata API가 헤더 역할. 그래서 “gRPC 채널을 매 호출 새로 만들면 안 된다”는 운영 규칙이 HTTP Keep-Alive 운영 규칙과 같은 원리에서 나온다. WebSocket에 적용해 보면 ①에서 끊긴다 — 연결 후 Stateful이라 재연결 시 상태 복원 책임이 애플리케이션으로 넘어가고, 이것이 HTTP API와 가장 큰 운영 차이를 만든다.
HTTP/2를 도입할 때 “NestJS 서버에서 직접 HTTP/2를 활성화할 것인가, ALB에 위임할 것인가”를 선택해야 한다.
ALB HTTP/2 위임이 적합한 경우 (대부분의 백엔드 서비스)
NestJS에서 HTTP/2 직접 활성화가 고려되는 경우
측정 데이터로 본 결정 기준 — HTTP/3 도입을 켜야 할까
Cloudflare가 2024년 자사 트래픽으로 HTTP/3 vs HTTP/2를 비교 측정한 결과는 다음과 같다 (출처: Comparing HTTP/3 vs HTTP/2 — Cloudflare):
이 데이터로 결정한 사례: “주 트래픽이 1MB 이상 이미지·동영상 페이로드인 미디어 API”라면 HTTP/3 도입의 TTFB 개선 효과가 큰 페이로드의 전송 시간에 묻혀 사용자 체감 차이가 거의 없다. 따라서 CloudFront에서 HTTP/3 활성화는 작은 응답이 많은 API용 디스트리뷰션에 우선 적용하고, 대용량 미디어용 디스트리뷰션은 HTTP/2 유지가 합리적이다. 반대로 모바일 비중이 높은 헤드리스 커머스 API(응답 평균 5~50KB)는 TTFB 12% 개선이 직접 전환율 지표로 이어지므로 HTTP/3 활성화가 우선순위 높다.
Keep-Alive 부작용 — 서버 리소스 고갈 조건
Keep-Alive는 성능을 높이지만 과도하게 열린 연결이 서버 파일 디스크립터와 메모리를 점유한다.
위험 조건:- 짧은 요청이 많은 API + 긴 Keep-Alive 타임아웃 → 연결이 빨리 닫히지 않아 열린 연결 수가 급증- OS 기본 파일 디스크립터 한도(1024)에 근접하면 새 연결 거부- Node.js keepAliveTimeout < ALB idle timeout 이면 ALB가 재사용하려는 순간 앱이 이미 연결을 닫아 502 발생
안전한 설정 (AWS ALB 환경):- ALB idle timeout: 60초 (기본값)- NestJS keepAliveTimeout: 65,000ms (ALB보다 5초 길게)- NestJS headersTimeout: 66,000ms (keepAliveTimeout보다 1초 길게)시나리오
ALB 앞단의 NestJS API에서 트래픽이 늘 때만 502가 보인다. 앱 로그에는 명확한 예외가 없고, 요청 재시도는 대부분 성공한다.
먼저 HTTPCode_ELB_502_Count와 NestJS keepAliveTimeout 값을 확인하고, Node.js keepAliveTimeout이 ALB idle timeout보다 짧은지 판단한다.명령 단위 진단 → 복구 절차 (간헐적 502 발생 시)
# 1. ALB 측에서 502가 어느 쪽 원인인지 먼저 확인aws cloudwatch get-metric-statistics \ --namespace AWS/ApplicationELB \ --metric-name HTTPCode_ELB_502_Count \ --dimensions Name=LoadBalancer,Value=app/my-alb/abc123 \ --start-time 2026-05-01T00:00:00Z --end-time 2026-05-01T01:00:00Z \ --period 300 --statistics Sum# 예상 출력: Sum > 0 이면 ALB 측에서 502 카운트됨 → 백엔드 timeout 의심
# 2. 현재 NestJS 컨테이너의 keepAliveTimeout 실제 값 확인kubectl exec -n production deploy/api-server -- \ node -e "console.log(require('http').globalAgent.keepAliveMsecs)"# 예상 출력: 5000 이면 기본값 그대로 (수정 필요), 65000 이면 적용됨
# 3. 복구: 환경변수 또는 main.ts에서 server.keepAliveTimeout = 65000 설정 후 롤링 재시작kubectl rollout restart deploy/api-server -n productionkubectl rollout status deploy/api-server -n production --timeout=180s# 예상 출력: deployment "api-server" successfully rolled out
# 4. 검증: 5분간 502 카운트가 0으로 떨어지는지 watchwatch -n 30 'aws cloudwatch get-metric-statistics ... --start-time $(date -u -v-5M +%FT%TZ) ...'# 다음 단계: 5분간 502 카운트 0이면 종료. 여전히 발생하면 ALB idle timeout(60초)이 변경되었거나 idle_timeout > 65초인지 의심(출처: Reverse Proxy Keep-Alive 502 분석 — iximiuz, Check your server.keepAliveTimeout — Shuhei Kagawa)
CORS preflight 비용과 캐싱 전략
OPTIONS preflight 요청은 실제 API 요청보다 먼저 발생해 레이턴시를 추가한다.
preflight 캐싱 전략:- Access-Control-Max-Age: 86400 → Firefox 최대 24시간 캐시- Access-Control-Max-Age: 7200 → Chrome/Chromium 최대 2시간 캐시- 기본값(설정 안 하면): 5초 → 매 10초 polling API는 preflight를 2배로 보냄
NestJS 권장 설정:app.enableCors({ origin: ['https://app.example.com'], maxAge: 86400, // 24시간 (Firefox 기준 최대)});(출처: Cache your CORS — httptoolkit.com, MDN CORS)
| 개념 A | 개념 B | 차이점 |
|---|---|---|
| 401 Unauthorized | 403 Forbidden | 401은 “너 누구야” (인증 실패), 403은 “너인 건 아는데 안 돼” (권한 부족) |
| PUT | PATCH | PUT은 리소스 전체를 교체, PATCH는 일부만 수정 |
| HTTP | HTTPS | HTTPS = HTTP + TLS 암호화. 데이터 전송 시 암호화 여부의 차이 |
| 500 | 502 | 500은 서버 자체 오류, 502는 중간 프록시가 뒷단 서버로부터 비정상 응답을 받은 것 |
| HTTP/1.1 | HTTP/2 | HTTP/1.1은 직렬 요청, HTTP/2는 멀티플렉싱으로 동시 요청 처리 |
| 200 OK | 304 Not Modified | 200은 새 데이터 포함, 304는 캐시가 유효해서 바디 없이 “그대로 써도 됨” |
| 502 | 504 | 502는 뒷단 서버가 비정상 응답, 504는 뒷단 서버가 응답 시간 초과 |
증상: 프론트엔드에서 API 호출 시 브라우저 콘솔에 CORS 에러가 뜨고 요청이 막힘
원인: 서버가 다른 출처(Origin)에서 오는 요청을 허용하지 않도록 설정되어 있음. HTTP 응답 헤더에 Access-Control-Allow-Origin이 없거나 잘못된 값
해결:
// main.ts — NestJS에서 CORS 허용async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: "http://localhost:3000", // 개발 환경 credentials: true, }); await app.listen(8080);}주의: 프로덕션에서
origin: '*'은 보안 위험. 허용할 도메인을 명시적으로 지정할 것.
증상: 배포 직후 또는 서버 재시작 시 502 응답이 옴. 로드밸런서/리버스 프록시(nginx, ALB)는 살아있는데 앱 서버가 응답 안 함
원인: 앱 컨테이너가 아직 기동 중이거나, 헬스체크 포트가 잘못 설정되어 있거나, 앱이 크래시 후 재시작 중
해결:
/health)가 정상 응답하는지 확인증상: API 호출 시 401 또는 403 응답
원인과 해결:
401 Unauthorized → 인증 토큰이 없거나 만료됨
exp 필드 확인)403 Forbidden → 토큰은 유효하지만 해당 리소스 접근 권한 없음
증상: API 호출 시 응답이 오지 않고 일정 시간 후 ETIMEDOUT 또는 504 Gateway Timeout 발생
원인: 서버 처리 시간이 너무 길거나(DB 쿼리 슬로우), 서버가 다운되었거나, 방화벽/보안 그룹이 요청을 막고 있음
해결:
# curl로 응답 시간 측정curl -o /dev/null -s -w "Total: %{time_total}s\nHTTP Code: %{http_code}\n" \ https://api.example.com/users
# 예상 출력 (정상):# Total: 0.234s# HTTP Code: 200
# 예상 출력 (슬로우 쿼리):# Total: 30.001s# HTTP Code: 504AWS 환경에서는 ALB의 기본 idle timeout이 60초이고, ECS 태스크가 그 안에 응답하지 않으면 504가 발생한다. 슬로우 쿼리는 CloudWatch Logs에서 DB 실행 시간을 확인해야 한다.
증상: 트래픽이 적을 때나 배포 직후 간헐적으로 502가 발생. 재시도하면 정상. ALB 로그에 502 Target closed the connection while we were reading the response
원인: NestJS(Node.js)의 기본 keepAliveTimeout(5초)이 AWS ALB의 idle timeout(기본 60초)보다 짧다. ALB가 연결을 재사용하려는 시점에 NestJS가 이미 연결을 끊어버림
해결:
// main.ts — NestJS Keep-Alive 타임아웃을 ALB보다 길게 설정async function bootstrap() { const app = await NestFactory.create(AppModule); const server = app.getHttpServer(); // ALB idle timeout(60초)보다 서버 타임아웃을 길게 설정 server.keepAliveTimeout = 65000; // 65초 server.headersTimeout = 66000; // keepAliveTimeout보다 반드시 길게 await app.listen(3000);}// 예상 결과: 간헐적 502 사라짐증상: 분명히 Controller에 핸들러를 정의했는데 요청이 도달하지 않음. 로그가 찍히지 않거나 404가 아닌 엉뚱한 응답이 옴.
원인: NestJS 요청 처리 순서를 모르면 어느 단계에서 막히는지 파악하기 어렵다.
NestJS 요청 처리 순서 (Request Lifecycle):
HTTP 요청 수신 ↓[1] Middleware (전역 → 모듈별 순서) ↓ (next()를 호출하지 않으면 여기서 멈춤)[2] Guards (전역 → 컨트롤러 → 핸들러 순서) ↓ (canActivate()가 false를 반환하면 403)[3] Interceptors (전역 → 컨트롤러 → 핸들러) — 요청 측 ↓[4] Pipes (전역 → 핸들러 → 파라미터 순서) ↓ (유효성 검증 실패 시 400)[5] Controller 핸들러 실행 ↓[6] Interceptors — 응답 측 (역순) ↓[7] Exception Filters (예외 발생 시)해결 디버깅 체크리스트:
// 1. Guard가 막고 있는지 확인 — 임시로 Guard 제거 후 테스트// @UseGuards(JwtAuthGuard) ← 주석 처리 후 요청이 오면 Guard 문제@Get(':id')findOne(@Param('id') id: string) { ... }
// 2. Pipe 오류인지 확인 — ValidationPipe 로그 확인app.useGlobalPipes(new ValidationPipe({ exceptionFactory: (errors) => { console.log('Validation errors:', JSON.stringify(errors)); // 로그 추가 return new BadRequestException(errors); }}));
// 3. Middleware에서 next()를 호출하는지 확인@Injectable()export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`Request: ${req.method} ${req.url}`); next(); // ← 이게 없으면 요청이 여기서 멈춤! }}상태 코드만 보는 모니터링이 가장 놓치기 쉬운 실패 카테고리다. RFC 9111도 “재무 트랜잭션이 조용히 미실행되는 것을 막기 위해” must-revalidate 같은 directive를 정의한다 (출처: RFC 9111 — HTTP Caching). 아래 3가지가 BackOps에서 자주 만나는 silent failure다.
Silent failure 1 — Stale ETag 캐시: 304 와야 할 때 200을 보내며 옛 데이터 반환
# 증상: 클라이언트가 옛 데이터를 본다고 보고했는데 서버 로그·5xx 알림은 깨끗# 진단: 같은 ETag로 두 번 요청해 304가 와야 정상ETAG=$(curl -sI https://api.example.com/users/1 | awk '/ETag/ {print $2}' | tr -d '\r')curl -sI -H "If-None-Match: $ETAG" https://api.example.com/users/1 | head -1# 예상 출력 (정상): HTTP/1.1 304 Not Modified# silent failure 신호: HTTP/1.1 200 OK ← ETag 비교가 깨졌거나 프록시가 헤더 제거
# 복구: CloudFront/ALB가 If-None-Match 헤더를 origin으로 전달하는지 확인aws cloudfront get-distribution-config --id E1ABCDEF \ | jq '.DistributionConfig.DefaultCacheBehavior.ForwardedValues.Headers'# 예상 출력: "If-None-Match"가 Items 배열에 있어야 함# 다음 단계: 누락이면 update-distribution-config로 헤더 forward 추가 후 캐시 무효화Silent failure 2 — Cache-Control 누락으로 의도하지 않은 CDN 캐싱
# 증상: 사용자별 응답이 다른 사람에게 노출 (개인 데이터 유출 가능성)curl -I https://api.example.com/users/me -H "Authorization: Bearer $TOKEN"# 예상 출력 (정상): Cache-Control: private, no-store# silent failure 신호: Cache-Control 헤더 자체 부재 또는 public ← CDN이 사용자 응답을 공유 캐싱
# 복구 (NestJS 글로벌 인터셉터에서 Auth 응답에 no-store 강제):# main.ts: app.use((req, res, next) => { if (req.headers.authorization) res.setHeader('Cache-Control', 'private, no-store'); next(); });# 검증: CDN 캐시 즉시 무효화aws cloudfront create-invalidation --distribution-id E1ABCDEF --paths "/users/me*"# 예상 출력: { "Invalidation": { "Status": "InProgress" ... } }Silent failure 3 — preflight 실패가 실제 요청 실패로 보고되는 CORS 케이스
# 증상: 프론트엔드에서 "API 응답 안 옴"으로 보고. 백엔드 로그에는 OPTIONS만 보이고 GET 흔적 없음curl -X OPTIONS https://api.example.com/users \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: GET" -i# 예상 출력 (정상): HTTP/1.1 204 + Access-Control-Allow-Origin 헤더# silent failure 신호: 200이지만 Access-Control-Allow-Origin 헤더 부재# → 브라우저가 실제 GET을 보내지 않아 백엔드는 "요청 자체가 없었던" 것처럼 보임
# 복구: NestJS enableCors에 origin 추가 후 재배포 (rollback이 필요하면 이전 이미지 태그로 되돌림)kubectl set image deploy/api-server api=myrepo/api:previous-tag -n productionkubectl rollout status deploy/api-server -n production --timeout=120sREST, HTTP/3(QUIC), Keep-Alive, CORS, Content Negotiation, Cookie vs Session, Stateless, WebSocket, ETag, Cache-Control
curl -v https://httpbin.org/get 실행해서 요청/응답 헤더 확인curl -v https://httpbin.org/get# 예상 출력:# * SSL handshake done# > GET /get HTTP/1.1# > Host: httpbin.org# < HTTP/1.1 200 OK# < Content-Type: application/json# {# "headers": { "Host": "httpbin.org", "User-Agent": "curl/8.5.0" },# "url": "https://httpbin.org/get"# }curl -X POST https://httpbin.org/post \ -d '{"name":"test"}' \ -H 'Content-Type: application/json'# 예상 출력: 요청한 바디가 "json" 키 아래에 그대로 반환됨# { "json": { "name": "test" }, ... }# 응답 시간 측정 포함curl -o /dev/null -s -w "HTTP Code: %{http_code}\nTotal: %{time_total}s\n" \ https://api.example.com/health
# 예상 출력:# HTTP Code: 200# Total: 0.087s# ETag 캐싱 동작 확인# 첫 번째 요청curl -I https://api.example.com/users/1# 응답 헤더에 ETag가 있으면: ETag: "v1-abc123"
# ETag를 포함한 두 번째 요청curl -I -H 'If-None-Match: "v1-abc123"' https://api.example.com/users/1# 예상 출력 (데이터 변경 없으면): HTTP/1.1 304 Not Modified# 예상 출력 (데이터 변경됐으면): HTTP/1.1 200 OK, ETag: "v1-xyz789"HTTP/3는 HTTP/2의 TCP 기반 한계를 극복하기 위해 UDP 위에 구축된 QUIC 프로토콜을 사용한다.
HTTP/2의 남은 문제점: TCP는 패킷 하나가 손실되면 모든 스트림이 기다려야 한다(Head-of-Line Blocking). HTTP/3는 각 스트림이 독립적이라서 하나의 패킷 손실이 다른 스트림에 영향을 주지 않는다.
HTTP 버전 비교:HTTP/1.1 → HTTP/2 → HTTP/3직렬 요청 멀티플렉싱 QUIC(UDP) 기반 멀티플렉싱TCP TCP UDP + 독립 스트림 헤더 압축(HPACK) 헤더 압축(QPACK)HTTP/3의 또 다른 장점은 연결 마이그레이션이다. QUIC는 4계층(전송) 연결을 3계층(IP) 흐름과 분리하므로, 모바일 기기가 Wi-Fi에서 셀룰러로 네트워크를 전환해도 연결이 끊기지 않는다. 2024년 기준 상위 1천만 웹사이트의 34%가 HTTP/3를 지원하며, 주요 브라우저의 95% 이상이 호환된다. AWS CloudFront, Cloudflare는 이미 HTTP/3를 지원한다. NestJS 백엔드 자체는 여전히 HTTP/1.1 또는 HTTP/2로 동작하고, 엣지(CDN/로드밸런서)에서 HTTP/3를 처리하는 방식이 일반적이다.
📖 더 보기: HTTP/3 and QUIC — Cloudflare — HTTP/3가 TCP 대신 QUIC를 사용하는 이유와 0-RTT 연결 복원을 설명 (중급)
요청이 실패했다├── 4xx → 클라이언트 문제│ ├── 400 → 요청 형식/데이터 잘못│ ├── 401 → 로그인 안 함 (토큰 없거나 만료)│ ├── 403 → 로그인은 했지만 권한 없음│ ├── 404 → 리소스가 존재하지 않음│ └── 429 → 너무 많이 요청함 (Rate Limit)└── 5xx → 서버 문제 ├── 500 → 서버 내부 코드 에러 ├── 502 → 뒷단 서버가 이상한 응답 (앱 크래시/재시작 중) ├── 503 → 서버 과부하 또는 점검 └── 504 → 뒷단 서버가 너무 오래 걸림 (슬로우 쿼리 의심)