컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 03

HTTP

네트워크HTTP웹

HTTP(HyperText Transfer Protocol)는 웹에서 클라이언트와 서버가 데이터를 주고받는 애플리케이션 계층 프로토콜입니다. 무상태(Stateless) 프로토콜로 각 요청은 독립적이며, 버전별로 성능과 기능이 크게 발전해왔습니다.

핵심 개념

요청 / 응답 구조

HTTP 메시지는 요청(Request)과 응답(Response)으로 구성됩니다. 요청은 메서드(GET·POST·PUT·DELETE), URL, 헤더, 바디로 이루어집니다. 응답은 상태 코드(200·404·500), 헤더, 바디로 구성됩니다. 헤더는 Content-Type·Authorization·Cache-Control 등 메타데이터를 포함합니다.

핵심 개념

HTTP/1.1

1997년 RFC 2068로 처음 발표되었고 지금은 RFC 9112로 정리된 버전입니다. 기본적으로 Keep-Alive를 지원해 하나의 TCP 연결로 여러 요청을 처리합니다. 하지만 요청을 순서대로 처리하는 HOL(Head-of-Line) 블로킹 문제가 있습니다. 파이프라이닝을 지원하지만 실제로는 거의 사용되지 않습니다.

핵심 개념

HTTP/2

2015년 RFC 7540으로 표준화됐습니다(현재는 RFC 9113). 멀티플렉싱으로 하나의 TCP 연결에서 여러 요청을 동시에 처리합니다. 헤더 압축(HPACK)으로 오버헤드를 줄이고, 서버 푸시로 클라이언트 요청 전에 리소스를 미리 전송할 수 있도록 설계되었습니다(클라이언트가 끌 수 있고, 성능 이득이 불분명해 Chrome은 106부터 기본으로 꺼 두었습니다). 바이너리 프레이밍 계층을 사용합니다.

핵심 개념

상태 코드

2xx(성공): 200 OK·201 Created·204 No Content. 3xx(리다이렉트): 301 Moved Permanently·302 Found·304 Not Modified. 4xx(클라이언트 오류): 400 Bad Request·401 Unauthorized·403 Forbidden·404 Not Found. 5xx(서버 오류): 500 Internal Server Error·502 Bad Gateway·503 Service Unavailable.

핵심 개념

무상태 & 쿠키/세션

HTTP는 각 요청이 독립적인 무상태 프로토콜입니다. 상태 유지가 필요하면 쿠키(Cookie)를 사용합니다. 서버는 Set-Cookie 헤더로 쿠키를 설정하고, 클라이언트는 이후 요청에 Cookie 헤더로 전송합니다. 세션은 서버 측에서 상태를 관리하며 세션 ID만 쿠키로 전달합니다.

핵심 개념

HTTP/3 & QUIC

2022년 RFC 9114로 표준화됐습니다. TCP 대신 UDP 기반의 QUIC 프로토콜을 사용합니다. 한 스트림의 손실이 다른 스트림을 막는 TCP 수준의 HOL 블로킹을 없애고, 전송과 TLS 1.3 핸드셰이크를 함께 처리해 연결 설정을 줄이며, 이전에 접속한 서버와는 0-RTT 데이터 전송이 가능합니다(재전송 공격에 주의해야 합니다). TLS 1.3이 내장되어 있습니다.

HOL(Head-of-Line) 블로킹 — HTTP/1.1에서 첫 번째 요청이 느리면 뒤의 요청들이 대기해야 합니다. 브라우저는 이를 우회하기 위해 도메인당 여러 개(흔히 6개)의 TCP 연결을 병렬로 열었습니다. HTTP/2의 멀티플렉싱은 TCP 레벨의 HOL은 해결하지 못하고, HTTP/3(QUIC)에서야 완전히 해결됩니다.
캐시 제어 — Cache-Control 헤더로 캐싱 동작을 제어합니다. max-age=3600은 1시간 캐싱, no-cache는 매번 서버 검증, no-store는 캐시 금지입니다. ETag와 Last-Modified로 조건부 요청(304 Not Modified)을 구현해 불필요한 데이터 전송을 줄입니다.
CORS (Cross-Origin Resource Sharing) — 브라우저는 보안 정책상 다른 출처의 리소스 접근을 제한합니다. 서버가 Access-Control-Allow-Origin 헤더를 응답에 포함하면 허용됩니다. GET·HEAD·POST가 아닌 메서드나 허용 목록에 없는 헤더(사용자 정의 헤더 등)가 있는 요청처럼 단순하지 않은 요청은 사전 OPTIONS 요청(Preflight)을 먼저 보냅니다.
REST vs GraphQL — REST는 URL 자원 중심으로 HTTP 메서드를 활용합니다. GraphQL은 단일 엔드포인트에서 클라이언트가 필요한 데이터 형태를 쿼리로 요청합니다. Over-fetching과 Under-fetching 문제를 해결하지만 캐싱이 복잡합니다.

HTTP는 어떻게 발전했나

HTTP는 웹에서 문서를 요청하고 응답하는 단순한 텍스트 프로토콜로 시작해, 같은 요청과 응답의 의미를 유지한 채 전송 방식만 바꿔 가며 발전해 왔습니다. HTTP/1.1은 1997년 RFC 2068로 처음 발표되었고 지금은 RFC 9110(공통 의미)과 RFC 9112(1.1 메시지 형식)로 정리되어 있습니다. HTTP/2(RFC 9113)와 HTTP/3(RFC 9114)는 같은 의미 위에서 더 효율적인 전송을 제공합니다.

숫자로 확인하기

새 연결에서 첫 응답까지의 왕복 수(DNS 조회와 패킷 손실은 뺀 단순화) — TCP 연결에 1 RTT, TLS 1.2 핸드셰이크에 2 RTT(메시지 흐름에서 센 값), 요청과 응답에 1 RTT로 총 4 RTT입니다. TLS 1.3이면 핸드셰이크가 1 RTT라서 3 RTT, HTTP/3은 전송과 TLS 설정을 한 번에 해서 2 RTT입니다. RTT가 100ms이면 400ms, 300ms, 200ms입니다.
상태 코드 분류 — 첫 숫자로 1xx 정보, 2xx 성공, 3xx 리다이렉션, 4xx 클라이언트 오류, 5xx 서버 오류의 다섯 가지로 나뉩니다.
HTTP/1.1의 병렬성 — 한 연결에서는 응답을 요청 순서대로 받으므로, 브라우저는 호스트당 여러 개의 연결을 병렬로 열어 로딩을 빠르게 하려 했습니다. Chromium 문서는 목적지 호스트당 6개 소켓이라고 설명합니다(RFC는 특정 숫자를 두지 않습니다).

자주 하는 오해

HTTP/2나 HTTP/3이면 항상 더 빠르다 — 요청이 많고 작을 때 이득이 크지만, 요청 수가 적거나 네트워크가 좋으면 차이가 작습니다. 손실이 많은 환경에서는 HTTP/3이 유리한 경우가 많습니다.
HTTP가 무상태라서 로그인 상태를 못 쓴다 — 프로토콜은 요청 사이의 상태를 기억하지 않지만, 쿠키나 토큰으로 요청마다 식별 정보를 실어 상태를 만듭니다.
401과 403은 같은 뜻이다 — 401은 인증이 필요하거나 인증에 실패했다는 뜻이고, 403은 인증되어도 권한이 없다는 뜻입니다.
POST와 PUT은 같다 — PUT은 같은 요청을 여러 번 보내도 결과가 같은 멱등(idempotent) 메서드이고, POST는 멱등으로 정의되어 있지 않습니다. GET, HEAD 같은 메서드는 안전(safe)하고 멱등합니다.

면접에서 이렇게 물어요

면접 질문

GET과 POST의 차이는 무엇인가요?

GET은 자원을 읽는 안전하고 멱등한 메서드이고 보통 캐시할 수 있습니다. POST는 서버에 데이터를 보내 처리를 요청하며 결과가 바뀔 수 있고 멱등하지 않습니다. 데이터를 URL에 싣느냐 본문에 싣느냐는 부차적인 차이입니다.

면접 질문

301과 302의 차이는 무엇인가요?

301은 영구 이동, 302는 임시 이동입니다. 검색 엔진과 캐시가 영구 이동은 주소를 바꿔 기억하고, 임시 이동은 원래 주소를 유지합니다.

면접 질문

HTTP/1.1의 HOL 블로킹은 무엇이고 어떻게 줄였나요?

한 연결에서 앞 응답이 늦으면 뒤 응답이 기다리는 문제입니다. HTTP/2는 멀티플렉싱으로 한 연결에서 여러 스트림을 섞어 보내고, HTTP/3은 TCP 대신 QUIC를 써서 한 스트림의 손실이 다른 스트림을 막지 않게 합니다.

면접 질문

CORS 에러는 왜 발생하나요?

브라우저가 다른 출처로 보낸 요청의 응답을 서버가 허용하지 않았을 때 응답을 스크립트에 노출하지 않는 것입니다. 서버가 Access-Control-Allow-Origin 같은 헤더로 허용해야 합니다. GET·HEAD·POST가 아닌 메서드, 허용 목록에 없는 헤더, form·multipart·text/plain이 아닌 Content-Type 같은 요청은 먼저 OPTIONS 사전 요청(preflight)을 보냅니다.

실제 시스템에서는

캐시 — RFC 9111이 HTTP 캐시의 규칙을 정의하고, Cache-Control, ETag, 조건부 요청(If-None-Match)으로 304 응답을 이끌어냅니다.
CORS — 브라우저가 따르는 CORS 규칙과 사전 요청의 조건은 WHATWG Fetch 표준이 정의합니다.
HTTPS와 CDN — 실제 서비스는 보통 TLS 위에서 HTTP를 쓰고 CDN을 거칩니다. HTTPS / TLS와 CDN 토픽에서 이어서 봅니다.

함께 보면 좋은 토픽

TCP/IP — HTTP가 올라타는 연결 설정과 전송 방식을 봅니다.
HTTPS / TLS — HTTP에 암호화를 더하는 방식을 봅니다.
DNS — 요청 전에 이름을 주소로 바꾸는 과정을 봅니다.
CDN — HTTP 캐시를 이용해 응답을 가까이 두는 방식을 봅니다.
WebSocket — HTTP 연결에서 양방향 통신으로 넘어가는 방식을 봅니다.
gRPC 통신 — 다른 서버의 함수를 내 코드의 함수처럼 부르는 RPC 방식을 봅니다.
HTTP/3와 QUIC — TCP 대신 UDP 위의 QUIC을 쓰는 HTTP/3의 동작을 봅니다.

참고 자료

Java — Spring Boot HTTP 요청/응답 및 상태 코드 처리

import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;

/* ── REST 컨트롤러 (서버 측) ──────────────────────────── */
@RestController
@RequestMapping("/api")
public class HttpDemoController {

    // GET — 200 OK / 404 Not Found
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userRepository.findById(id)
            .map(ResponseEntity::ok)              // 200 OK
            .orElse(ResponseEntity.notFound().build()); // 404
    }

    // POST — 201 Created + Location 헤더
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody UserDto dto) {
        User saved = userService.create(dto);
        URI location = URI.create("/api/users/" + saved.getId());
        return ResponseEntity.created(location).body(saved); // 201
    }

    // 캐시 제어 헤더 설정
    @GetMapping("/static/{file}")
    public ResponseEntity<Resource> getStatic(@PathVariable String file) {
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
                                      .cachePublic())   // Cache-Control: public, max-age=3600
            .eTag(computeETag(file))                   // ETag 헤더
            .body(loadResource(file));
    }

    // CORS 직접 설정 (전역은 WebMvcConfigurer 사용)
    @CrossOrigin(origins = "https://frontend.example.com")
    @GetMapping("/public")
    public ResponseEntity<String> publicApi() {
        return ResponseEntity.ok("OK"); // Access-Control-Allow-Origin 헤더 포함
    }
}

/* ── HTTP 클라이언트 (RestTemplate) ───────────────────── */
@Service
public class HttpClientService {

    private final RestTemplate rest = new RestTemplate();

    public void demo() {
        // GET 요청 — 응답 바디를 객체로 역직렬화
        ResponseEntity<User> res = rest.getForEntity(
            "https://api.example.com/users/1", User.class
        );
        System.out.println(res.getStatusCode()); // 200 OK
        System.out.println(res.getHeaders().getContentType());

        // POST 요청 — 헤더 직접 설정
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth("eyJhbGci..."); // Authorization: Bearer ...

        HttpEntity<UserDto> req = new HttpEntity<>(dto, headers);
        ResponseEntity<User> created = rest.postForEntity(
            "https://api.example.com/users", req, User.class
        );
        // 201 Created → Location 헤더에서 새 리소스 URI 확인
        URI newUri = created.getHeaders().getLocation();
    }
}

Spring의 ResponseEntity를 사용하면 HTTP 상태 코드와 헤더를 명시적으로 제어할 수 있습니다.
orElse(notFound().build())처럼 Optional과 조합하면 404 처리가 간결해집니다.
POST 생성 시 201 Created와 Location 헤더를 반환하는 것이 REST 표준입니다.
클라이언트는 Location을 보고 새 리소스 URI를 알 수 있습니다.
CacheControl 빌더로 Cache-Control 헤더를 설정하면 브라우저·CDN 캐싱을 제어합니다.
ETag와 함께 사용하면 조건부 요청(304)으로 불필요한 데이터 전송을 줄입니다.
실무에서는 RestTemplate보다 WebClient(비동기) 또는 OpenFeign(선언적)을 더 많이 사용합니다.

Python — HTTP 요청 분석 및 HTTP/2 비교

import httpx   # HTTP/1.1 + HTTP/2 모두 지원
import time

# ── HTTP/1.1 순차 요청 ──────────────────────────────────
def http1_sequential():
    urls = [
        "https://example.com/index.html",
        "https://example.com/style.css",
        "https://example.com/script.js",
    ]
    start = time.time()
    with httpx.Client(http2=False) as client:
        for url in urls:
            res = client.get(url)
            print(f"{res.status_code} {url}")
            # HOL 블로킹: 앞 요청 완료 후 다음 전송
    print(f"HTTP/1.1 총 시간: {time.time()-start:.2f}s")

# ── HTTP/2 멀티플렉싱 ────────────────────────────────────
import asyncio

async def http2_multiplex():
    urls = [
        "https://example.com/index.html",
        "https://example.com/style.css",
        "https://example.com/script.js",
    ]
    start = time.time()
    async with httpx.AsyncClient(http2=True) as client:
        # 3개 요청 동시 전송 (멀티플렉싱)
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        for res in responses:
            print(f"{res.status_code} HTTP/{res.http_version}")
    print(f"HTTP/2 총 시간: {time.time()-start:.2f}s")

# ── 응답 헤더 분석 ──────────────────────────────────────
def inspect_headers():
    with httpx.Client() as client:
        res = client.get("https://httpbin.org/get")

        print(f"상태 코드: {res.status_code}")              # 200
        print(f"Content-Type: {res.headers['content-type']}")
        print(f"X-RateLimit-Remaining: {res.headers.get('x-ratelimit-remaining', 'N/A')}")

        # 조건부 요청 (ETag 활용)
        etag = res.headers.get('etag')
        if etag:
            res2 = client.get(
                "https://httpbin.org/get",
                headers={"If-None-Match": etag}
            )
            print(f"조건부 요청: {res2.status_code}")  # 304 Not Modified

# asyncio.run(http2_multiplex())

httpx 라이브러리는 HTTP/1.1과 HTTP/2를 모두 지원합니다. http2=True 옵션만으로 멀티플렉싱을 활성화할 수 있습니다.
HTTP/1.1 순차 요청과 HTTP/2 멀티플렉싱을 같은 URL로 비교하면 RTT 차이를 직접 확인할 수 있습니다. 리소스가 많을수록 HTTP/2의 이점이 커집니다.
ETag를 활용한 조건부 요청은 변경이 없으면 304 Not Modified를 반환해 바디 전송을 생략합니다. 정적 리소스 캐싱에 매우 효과적입니다.
실제 HTTP/2 멀티플렉싱 효과는 네트워크 지연(RTT)이 클수록 두드러집니다. 로컬 환경에서는 차이가 미미할 수 있습니다.

Q1

HTTP/1.1의 성능 한계로 자주 언급되는 것은?

한 연결에서 여러 요청을 동시에 처리하지 못해 생기는 문제입니다.
HTTP/1.1은 한 커넥션에서 요청이 순서대로 처리되어야 해, 앞선 요청이 느리면 뒤 요청까지 막히는 Head-of-Line Blocking이 발생할 수 있습니다.

Q2

HTTP/1.1의 성능 한계로 자주 언급되는 것은?

한 연결에서 여러 요청을 동시에 처리하지 못해 생기는 문제입니다.
HTTP/1.1은 한 커넥션에서 요청이 순서대로 처리되어야 해, 앞선 요청이 느리면 뒤 요청까지 막히는 Head-of-Line Blocking이 발생할 수 있습니다.

Q3

HTTP/2의 멀티플렉싱(Multiplexing)이 해결한 문제는?

여러 스트림을 하나의 연결에 겹쳐 보냅니다.
HTTP/2는 하나의 TCP 연결 위에서 여러 요청/응답 스트림을 동시에 주고받는 멀티플렉싱을 지원해, HTTP/1.1의 순차 처리 한계를 완화합니다.

Q4

HTTP/2의 멀티플렉싱(Multiplexing)이 해결한 문제는?

여러 스트림을 하나의 연결에 겹쳐 보냅니다.
HTTP/2는 하나의 TCP 연결 위에서 여러 요청/응답 스트림을 동시에 주고받는 멀티플렉싱을 지원해, HTTP/1.1의 순차 처리 한계를 완화합니다.

Q5

HTTP는 기본적으로 어떤 특성을 가진 프로토콜인가요?

서버는 기본적으로 이전 요청을 기억하지 않습니다.
HTTP는 기본적으로 무상태(stateless) 프로토콜로, 각 요청이 독립적으로 처리됩니다. 로그인 상태 유지 등은 쿠키/세션 같은 별도 메커니즘으로 구현합니다.

Q6

HTTP는 기본적으로 어떤 특성을 가진 프로토콜인가요?

서버는 기본적으로 이전 요청을 기억하지 않습니다.
HTTP는 기본적으로 무상태(stateless) 프로토콜로, 각 요청이 독립적으로 처리됩니다. 로그인 상태 유지 등은 쿠키/세션 같은 별도 메커니즘으로 구현합니다.