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이 내장되어 있습니다.
HTTP는 어떻게 발전했나
HTTP는 웹에서 문서를 요청하고 응답하는 단순한 텍스트 프로토콜로 시작해, 같은 요청과 응답의 의미를 유지한 채 전송 방식만 바꿔 가며 발전해 왔습니다. HTTP/1.1은 1997년 RFC 2068로 처음 발표되었고 지금은 RFC 9110(공통 의미)과 RFC 9112(1.1 메시지 형식)로 정리되어 있습니다. HTTP/2(RFC 9113)와 HTTP/3(RFC 9114)는 같은 의미 위에서 더 효율적인 전송을 제공합니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
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)을 보냅니다.
실제 시스템에서는
Cache-Control, ETag, 조건부 요청(If-None-Match)으로 304 응답을 이끌어냅니다.
함께 보면 좋은 토픽
참고 자료
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의 성능 한계로 자주 언급되는 것은?
Q2
HTTP/1.1의 성능 한계로 자주 언급되는 것은?
Q3
HTTP/2의 멀티플렉싱(Multiplexing)이 해결한 문제는?
Q4
HTTP/2의 멀티플렉싱(Multiplexing)이 해결한 문제는?
Q5
HTTP는 기본적으로 어떤 특성을 가진 프로토콜인가요?
Q6
HTTP는 기본적으로 어떤 특성을 가진 프로토콜인가요?