HTTP Basics
분류: Layer 1 - 백엔드 기초 | 작성일: 2026-03-21
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”HTTP(HyperText Transfer Protocol)는 클라이언트와 서버가 데이터를 주고받기 위한 요청-응답 기반의 통신 규약이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”BackOps에서 다루는 거의 모든 서비스는 HTTP 위에서 동작한다. API 호출이 실패했을 때, 배포 후 서비스가 안 될 때, 외부 연동이 안 될 때 — 원인을 파악하려면 HTTP가 어떻게 동작하는지 알아야 한다. 이걸 모르면 에러 로그를 봐도 뭐가 문제인지 판단이 안 된다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”요청(Request)과 응답(Response)
클라이언트가 요청을 보내면 서버가 응답을 돌려준다. 항상 이 순서. 요청에는 메서드, URL, 헤더, 바디가 있고 응답에는 상태 코드, 헤더, 바디가 있다.
HTTP 요청이 실제로 어떻게 도달하는가 — 내부 동작 원리
“브라우저에서 URL을 치면 서버가 응답한다”는 사실 뒤에 이런 단계가 숨어 있다:
- DNS 조회:
api.example.com→203.0.113.5IP 주소로 변환 (전화번호부를 찾는 것과 같다) - TCP 3-way Handshake: 클라이언트 SYN → 서버 SYN-ACK → 클라이언트 ACK. 연결 수립 (실제로 악수하는 것과 같다)
- TLS Handshake (HTTPS인 경우): 인증서 검증 + 암호화 키 교환
- HTTP 요청 전송: 메서드 + URL + 헤더 + 바디를 텍스트로 전송
- 서버 처리: 라우팅 → 비즈니스 로직 → DB 조회 → 응답 생성
- HTTP 응답 수신: 상태 코드 + 헤더 + 바디
📖 더 보기: 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 메서드
GET: 데이터 조회. 바디 없음.POST: 데이터 생성. 바디에 데이터 포함.PUT: 데이터 전체 수정.PATCH: 데이터 일부 수정.DELETE: 데이터 삭제.
상태 코드 (외워야 할 것들)
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: 중간 서버가 뒤에 있는 서버의 응답을 기다리다 타임아웃
헤더(Header)
요청/응답에 대한 메타데이터. Content-Type(데이터 형식), Authorization(인증 정보), Cache-Control(캐싱 규칙) 등이 자주 쓰인다.
Stateless(무상태)
HTTP는 각 요청이 독립적이다. 서버는 이전 요청을 기억하지 않는다.
그래서 인증이 필요한 모든 요청마다 Authorization 헤더를 포함해야 한다.
Session/Cookie가 필요한 이유도 이 stateless 성질 때문이다.
HTTPS
HTTP + TLS 암호화. 데이터가 중간에 탈취당해도 읽을 수 없게 한다. 현재 거의 모든 서비스가 HTTPS를 사용한다.
HTTP/2 — 왜 HTTP/1.1보다 빠른가
비유하자면, HTTP/1.1은 “한 줄짜리 창구”이다. 요청 하나가 끝나야 다음 요청을 할 수 있다. HTTP/2는 “여러 창구를 동시에 열어두는 것”이다.
HTTP/1.1의 문제점: 하나의 TCP 연결에서 요청을 직렬로 처리한다. 브라우저는 이를 우회하려고 보통 6~8개의 TCP 연결을 병렬로 열지만, 이것은 서버 리소스를 낭비한다.
HTTP/2가 이를 해결하는 방법:
- 멀티플렉싱(Multiplexing): 하나의 TCP 연결에서 여러 요청·응답을 동시에 주고받는다. 요청 순서가 달라도 각 요청에 스트림 ID를 붙여서 구분한다.
- 헤더 압축(HPACK): HTTP/1.1은 요청마다 동일한 헤더(User-Agent, Cookie 등)를 반복 전송한다. HTTP/2는 HPACK 알고리즘으로 이전에 보낸 헤더를 인덱스로 참조해 중복을 제거한다.
- 바이너리 프레이밍: 텍스트 대신 0/1 이진 형식으로 데이터를 전송해 파싱이 빠르고 에러에 강하다.
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 응답이 중요한가
서버가 동일한 데이터를 계속 보내지 않도록 하는 메커니즘이다. 클라이언트와 서버가 합의한 기준으로 “이미 가진 데이터를 재사용해도 된다”고 판단하면 네트워크 전송을 건너뛸 수 있다.
- Cache-Control: max-age=300: 300초(5분)간 캐시된 응답을 사용해도 됨
- ETag: 리소스 버전을 나타내는 식별자. 서버가 응답에
ETag: "abc123"포함 → 클라이언트가 다음 요청에If-None-Match: "abc123"포함 → 서버가 변경이 없으면304 Not Modified반환 (바디 없음)
ETag 검증 흐름:1. GET /users/1 → 응답: 200 OK, ETag: "v1-abc123", { name: "홍길동" }2. GET /users/1 (If-None-Match: "v1-abc123") → 서버: 변경 없음 → 응답: 304 Not Modified (바디 없음, 네트워크 절약)3. 서버에서 데이터 변경4. GET /users/1 (If-None-Match: "v1-abc123") → 서버: 변경됨 → 응답: 200 OK, ETag: "v1-xyz789", { name: "홍길동(수정)" }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 활성화됨3.5. HTTP 원리의 전이 가능성 — 다른 기술에 어떻게 적용되는가
섹션 제목: “3.5. HTTP 원리의 전이 가능성 — 다른 기술에 어떻게 적용되는가”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)
3.6. HTTP/2 도입 Trade-off — 언제 ALB에 위임하고 언제 직접 적용하는가
섹션 제목: “3.6. HTTP/2 도입 Trade-off — 언제 ALB에 위임하고 언제 직접 적용하는가”HTTP/2를 도입할 때 “NestJS 서버에서 직접 HTTP/2를 활성화할 것인가, ALB에 위임할 것인가”를 선택해야 한다.
ALB HTTP/2 위임이 적합한 경우 (대부분의 백엔드 서비스)
- 서비스 규모에 무관하게, 클라이언트(브라우저/앱) ↔ ALB 구간만 HTTP/2 혜택이 필요한 경우
- NestJS는 HTTP/1.1 유지, ALB가 HTTP/2를 종료하고 HTTP/1.1로 내부 전달
- 이점: 앱 코드 변경 없이 HTTP/2 혜택, ALB가 연결 관리 전담
- 한계: ALB ↔ NestJS 구간은 여전히 HTTP/1.1 (내부 마이크로서비스 트래픽 많을 때 병목 가능)
NestJS에서 HTTP/2 직접 활성화가 고려되는 경우
- 서비스 간 gRPC 통신이 주가 되어 ALB를 통한 gRPC 라우팅이 필요한 경우
- AWS ALB는 2020년부터 end-to-end HTTP/2 및 gRPC 라우팅을 공식 지원 (출처: AWS Blog — ALB HTTP/2 and gRPC)
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초 길게)(출처: Reverse Proxy Keep-Alive 502 분석 — iximiuz)
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)
4. 실무에서 어디에 쓰이나
섹션 제목: “4. 실무에서 어디에 쓰이나”- API 서버 간 통신 (내부 마이크로서비스 호출)
- 프론트엔드 ↔ 백엔드 통신
- 외부 서비스 연동 (결제, 알림, 외부 API)
- 배포 후 헬스체크 (서버가 살아있는지 확인)
- 모니터링 (HTTP 상태 코드 기반 알림)
5. 현재 내 업무와 연결점
섹션 제목: “5. 현재 내 업무와 연결점”- API 호출 실패 시 로그에서 상태 코드를 보고 원인을 파악해야 함
- 배포 후 502/503 에러가 발생하면 서버 상태를 판단해야 함
- 외부 서비스 연동 이슈 디버깅 시 요청/응답 구조를 이해해야 함
- CI/CD 파이프라인에서 헬스체크 엔드포인트의 동작을 이해해야 함
6. 자주 헷갈리는 개념 비교
섹션 제목: “6. 자주 헷갈리는 개념 비교”| 개념 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는 뒷단 서버가 응답 시간 초과 |
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”🔧 CORS 에러 — “Access to fetch has been blocked by CORS policy”
섹션 제목: “🔧 CORS 에러 — “Access to fetch has been blocked by CORS policy””증상: 프론트엔드에서 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 Bad Gateway — 배포 후 갑자기 502
섹션 제목: “🔧 502 Bad Gateway — 배포 후 갑자기 502”증상: 배포 직후 또는 서버 재시작 시 502 응답이 옴. 로드밸런서/리버스 프록시(nginx, ALB)는 살아있는데 앱 서버가 응답 안 함
원인: 앱 컨테이너가 아직 기동 중이거나, 헬스체크 포트가 잘못 설정되어 있거나, 앱이 크래시 후 재시작 중
해결:
- ECS/EC2의 앱 로그 확인 — 앱 서버 자체 에러인지 파악
- 헬스체크 엔드포인트(
/health)가 정상 응답하는지 확인 - 배포 전략이 Rolling이라면 구버전 컨테이너가 내려가기 전에 신버전이 완전히 뜨는지 확인
🔧 401 vs 403 — 디버깅 방향이 다르다
섹션 제목: “🔧 401 vs 403 — 디버깅 방향이 다르다”증상: API 호출 시 401 또는 403 응답
원인과 해결:
401 Unauthorized→ 인증 토큰이 없거나 만료됨- Authorization 헤더가 포함되었는지 확인
- 토큰 만료 여부 확인 (jwt.io에서 디코딩해서
exp필드 확인) - 토큰 재발급 로직(Refresh Token) 동작 여부 확인
403 Forbidden→ 토큰은 유효하지만 해당 리소스 접근 권한 없음- 해당 사용자의 역할(Role)이 맞는지 확인
- RBAC 정책 또는 AWS IAM 정책 확인
🔧 요청 타임아웃 — 응답이 안 오고 연결이 끊김
섹션 제목: “🔧 요청 타임아웃 — 응답이 안 오고 연결이 끊김”증상: 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 실행 시간을 확인해야 한다.
🔧 NestJS 서버에서 간헐적 502 — Keep-Alive 타임아웃 불일치
섹션 제목: “🔧 NestJS 서버에서 간헐적 502 — Keep-Alive 타임아웃 불일치”증상: 트래픽이 적을 때나 배포 직후 간헐적으로 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 사라짐🔧 NestJS 요청이 처리되지 않고 Controller까지 도달하지 않음
섹션 제목: “🔧 NestJS 요청이 처리되지 않고 Controller까지 도달하지 않음”증상: 분명히 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(); // ← 이게 없으면 요청이 여기서 멈춤! }}7. 체크리스트
섹션 제목: “7. 체크리스트”- GET과 POST의 차이를 설명할 수 있다
- 400, 401, 403, 500, 502의 차이를 설명할 수 있다
- 요청의 구성요소(메서드, URL, 헤더, 바디)를 설명할 수 있다
- HTTP가 stateless인 이유와, 그래서 매 요청마다 인증 정보를 보내야 하는 이유를 설명할 수 있다
- HTTPS가 왜 필요한지 설명할 수 있다
- curl로 API를 호출하고 응답을 읽을 수 있다
- HTTP/2가 HTTP/1.1보다 빠른 이유를 설명할 수 있다
- Keep-Alive가 뭔지, NestJS에서 타임아웃 설정이 왜 중요한지 설명할 수 있다
8. 추가 학습 키워드
섹션 제목: “8. 추가 학습 키워드”REST, HTTP/3(QUIC), Keep-Alive, CORS, Content Negotiation, Cookie vs Session, Stateless, WebSocket, ETag, Cache-Control
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 표준 스펙 (Primary)
섹션 제목: “📚 표준 스펙 (Primary)”- 📖 RFC 7230 — HTTP/1.1: Message Syntax and Routing — HTTP/1.1 메시지 구조, 연결 관리(Keep-Alive 포함), 라우팅 정의 (스펙)
- 📖 RFC 7231 — HTTP/1.1: Semantics and Content — HTTP 메서드(GET/POST/PUT/DELETE), 상태 코드, 헤더 필드 의미 정의 (스펙)
- 📖 RFC 9110 — HTTP Semantics — RFC 7231 등을 통합·폐기하고 2022년 발행된 현행 HTTP 시맨틱 표준 (스펙)
- 📖 RFC 7540 — HTTP/2 — 멀티플렉싱, HPACK 헤더 압축, 바이너리 프레이밍 정의 (스펙)
- 📖 RFC 9114 — HTTP/3 — QUIC 기반 HTTP/3 프로토콜 정의, HOL 블로킹 해결 방식 (스펙)
📚 입문·중급 리소스
섹션 제목: “📚 입문·중급 리소스”- 📖 HTTP | MDN Web Docs — HTTP 메서드, 상태 코드, 헤더를 한국어로 설명하는 공식 레퍼런스 (입문)
- 📖 Life Cycle of an HTTP Request - Requestly — DNS → TCP → TLS → 요청 → 응답까지 전 단계를 다이어그램으로 설명 (입문)
- 📖 HTTP/2 vs HTTP/1.1 — Cloudflare — 멀티플렉싱과 헤더 압축 개념을 그림과 함께 쉽게 설명 (입문)
- 📖 NestJS CORS 공식 문서 — NestJS에서 CORS 설정하는 공식 가이드, 옵션 설명 포함 (중급)
- 📖 Optimizing NestJS Performance with HTTP Keep-Alive — Michael Guay — NestJS에서 Keep-Alive 설정과 성능 개선 방법을 실측 수치와 함께 설명 (중급)
- 📖 gRPC on HTTP/2 — gRPC 공식 블로그 — gRPC가 HTTP/2 멀티플렉싱과 Keep-Alive를 어떻게 활용하는지 설명 (중급)
- 📖 AWS ALB HTTP/2 and gRPC Support — AWS Blog — ALB에서 end-to-end HTTP/2 및 gRPC 라우팅 설정 방법 (중급)
9. 내가 직접 확인해볼 것
섹션 제목: “9. 내가 직접 확인해볼 것”- 터미널에서
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"# }- POST 요청 보내보기
curl -X POST https://httpbin.org/post \ -d '{"name":"test"}' \ -H 'Content-Type: application/json'# 예상 출력: 요청한 바디가 "json" 키 아래에 그대로 반환됨# { "json": { "name": "test" }, ... }- 현재 팀 서비스의 API를 하나 골라서 curl로 호출해보고, 응답 상태 코드와 헤더 확인
# 응답 시간 측정 포함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"- 브라우저 개발자 도구 → Network 탭에서 요청/응답 확인해보기
HTTP/3(QUIC) — 다음 세대 HTTP
섹션 제목: “HTTP/3(QUIC) — 다음 세대 HTTP”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 연결 복원을 설명 (중급)
10. 요약 — 이것만 기억해도 된다
섹션 제목: “10. 요약 — 이것만 기억해도 된다”상태 코드 빠른 판단 트리
섹션 제목: “상태 코드 빠른 판단 트리”요청이 실패했다├── 4xx → 클라이언트 문제│ ├── 400 → 요청 형식/데이터 잘못│ ├── 401 → 로그인 안 함 (토큰 없거나 만료)│ ├── 403 → 로그인은 했지만 권한 없음│ ├── 404 → 리소스가 존재하지 않음│ └── 429 → 너무 많이 요청함 (Rate Limit)└── 5xx → 서버 문제 ├── 500 → 서버 내부 코드 에러 ├── 502 → 뒷단 서버가 이상한 응답 (앱 크래시/재시작 중) ├── 503 → 서버 과부하 또는 점검 └── 504 → 뒷단 서버가 너무 오래 걸림 (슬로우 쿼리 의심)5줄 핵심
섹션 제목: “5줄 핵심”- HTTP는 요청-응답 구조의 통신 규약이다
- 메서드(GET/POST/PUT/DELETE)로 의도를 표현하고, 상태 코드로 결과를 전달한다
- 상태 코드의 첫 자리 의미: 2xx 성공, 4xx 클라이언트 잘못, 5xx 서버 잘못
- 헤더로 메타데이터를 주고받고, HTTPS로 암호화하며, HTTP/2는 멀티플렉싱으로 성능을 높인다
- API 디버깅의 출발점은 항상 HTTP 요청/응답을 읽는 것이다