콘텐츠로 이동

HTTP Basics

분류: Layer 1 - 백엔드 기초 | 작성일: 2026-03-21

HTTP(HyperText Transfer Protocol)는 클라이언트와 서버가 데이터를 주고받기 위한 요청-응답 기반의 통신 규약이다.

BackOps에서 다루는 거의 모든 서비스는 HTTP 위에서 동작한다. API 호출이 실패했을 때, 배포 후 서비스가 안 될 때, 외부 연동이 안 될 때 — 원인을 파악하려면 HTTP가 어떻게 동작하는지 알아야 한다. 이걸 모르면 에러 로그를 봐도 뭐가 문제인지 판단이 안 된다.

요청(Request)과 응답(Response)

클라이언트가 요청을 보내면 서버가 응답을 돌려준다. 항상 이 순서. 요청에는 메서드, URL, 헤더, 바디가 있고 응답에는 상태 코드, 헤더, 바디가 있다.

HTTP 요청이 실제로 어떻게 도달하는가 — 내부 동작 원리

“브라우저에서 URL을 치면 서버가 응답한다”는 사실 뒤에 이런 단계가 숨어 있다:

  1. DNS 조회: api.example.com203.0.113.5 IP 주소로 변환 (전화번호부를 찾는 것과 같다)
  2. TCP 3-way Handshake: 클라이언트 SYN → 서버 SYN-ACK → 클라이언트 ACK. 연결 수립 (실제로 악수하는 것과 같다)
  3. TLS Handshake (HTTPS인 경우): 인증서 검증 + 암호화 키 교환
  4. HTTP 요청 전송: 메서드 + URL + 헤더 + 바디를 텍스트로 전송
  5. 서버 처리: 라우팅 → 비즈니스 로직 → DB 조회 → 응답 생성
  6. HTTP 응답 수신: 상태 코드 + 헤더 + 바디

📖 더 보기: Life Cycle of an HTTP Request — 위 1~6단계 흐름을 다이어그램과 함께 설명하는 입문 가이드 (입문)

curl로 실제 HTTP 요청 확인하기

Terminal window
# -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 Content

HTTP 메서드

  • 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가 이를 해결하는 방법:

  1. 멀티플렉싱(Multiplexing): 하나의 TCP 연결에서 여러 요청·응답을 동시에 주고받는다. 요청 순서가 달라도 각 요청에 스트림 ID를 붙여서 구분한다.
  2. 헤더 압축(HPACK): HTTP/1.1은 요청마다 동일한 헤더(User-Agent, Cookie 등)를 반복 전송한다. HTTP/2는 HPACK 알고리즘으로 이전에 보낸 헤더를 인덱스로 참조해 중복을 제거한다.
  3. 바이너리 프레이밍: 텍스트 대신 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 혜택을 받을 수 있다.

Terminal window
# 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)


  • API 서버 간 통신 (내부 마이크로서비스 호출)
  • 프론트엔드 ↔ 백엔드 통신
  • 외부 서비스 연동 (결제, 알림, 외부 API)
  • 배포 후 헬스체크 (서버가 살아있는지 확인)
  • 모니터링 (HTTP 상태 코드 기반 알림)
  • API 호출 실패 시 로그에서 상태 코드를 보고 원인을 파악해야 함
  • 배포 후 502/503 에러가 발생하면 서버 상태를 판단해야 함
  • 외부 서비스 연동 이슈 디버깅 시 요청/응답 구조를 이해해야 함
  • CI/CD 파이프라인에서 헬스체크 엔드포인트의 동작을 이해해야 함
개념 A개념 B차이점
401 Unauthorized403 Forbidden401은 “너 누구야” (인증 실패), 403은 “너인 건 아는데 안 돼” (권한 부족)
PUTPATCHPUT은 리소스 전체를 교체, PATCH는 일부만 수정
HTTPHTTPSHTTPS = HTTP + TLS 암호화. 데이터 전송 시 암호화 여부의 차이
500502500은 서버 자체 오류, 502는 중간 프록시가 뒷단 서버로부터 비정상 응답을 받은 것
HTTP/1.1HTTP/2HTTP/1.1은 직렬 요청, HTTP/2는 멀티플렉싱으로 동시 요청 처리
200 OK304 Not Modified200은 새 데이터 포함, 304는 캐시가 유효해서 바디 없이 “그대로 써도 됨”
502504502는 뒷단 서버가 비정상 응답, 504는 뒷단 서버가 응답 시간 초과

🔧 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)는 살아있는데 앱 서버가 응답 안 함

원인: 앱 컨테이너가 아직 기동 중이거나, 헬스체크 포트가 잘못 설정되어 있거나, 앱이 크래시 후 재시작 중

해결:

  1. ECS/EC2의 앱 로그 확인 — 앱 서버 자체 에러인지 파악
  2. 헬스체크 엔드포인트(/health)가 정상 응답하는지 확인
  3. 배포 전략이 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 쿼리 슬로우), 서버가 다운되었거나, 방화벽/보안 그룹이 요청을 막고 있음

해결:

Terminal window
# 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: 504

AWS 환경에서는 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(); // ← 이게 없으면 요청이 여기서 멈춤!
}
}

  • GET과 POST의 차이를 설명할 수 있다
  • 400, 401, 403, 500, 502의 차이를 설명할 수 있다
  • 요청의 구성요소(메서드, URL, 헤더, 바디)를 설명할 수 있다
  • HTTP가 stateless인 이유와, 그래서 매 요청마다 인증 정보를 보내야 하는 이유를 설명할 수 있다
  • HTTPS가 왜 필요한지 설명할 수 있다
  • curl로 API를 호출하고 응답을 읽을 수 있다
  • HTTP/2가 HTTP/1.1보다 빠른 이유를 설명할 수 있다
  • Keep-Alive가 뭔지, NestJS에서 타임아웃 설정이 왜 중요한지 설명할 수 있다

REST, HTTP/3(QUIC), Keep-Alive, CORS, Content Negotiation, Cookie vs Session, Stateless, WebSocket, ETag, Cache-Control

  • 터미널에서 curl -v https://httpbin.org/get 실행해서 요청/응답 헤더 확인
Terminal window
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 요청 보내보기
Terminal window
curl -X POST https://httpbin.org/post \
-d '{"name":"test"}' \
-H 'Content-Type: application/json'
# 예상 출력: 요청한 바디가 "json" 키 아래에 그대로 반환됨
# { "json": { "name": "test" }, ... }
  • 현재 팀 서비스의 API를 하나 골라서 curl로 호출해보고, 응답 상태 코드와 헤더 확인
Terminal window
# 응답 시간 측정 포함
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
Terminal window
# 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는 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 → 뒷단 서버가 너무 오래 걸림 (슬로우 쿼리 의심)
  1. HTTP는 요청-응답 구조의 통신 규약이다
  2. 메서드(GET/POST/PUT/DELETE)로 의도를 표현하고, 상태 코드로 결과를 전달한다
  3. 상태 코드의 첫 자리 의미: 2xx 성공, 4xx 클라이언트 잘못, 5xx 서버 잘못
  4. 헤더로 메타데이터를 주고받고, HTTPS로 암호화하며, HTTP/2는 멀티플렉싱으로 성능을 높인다
  5. API 디버깅의 출발점은 항상 HTTP 요청/응답을 읽는 것이다