응용 계층과 프로토콜
응용 계층은 사용자의 애플리케이션이 네트워크 서비스에 접근할 수 있도록 도와주는 인터페이스(Interface) 역할을 합니다.
- 추상화 : 네트워크 하드웨어나 라우팅의 복잡한 경로는 신경 쓰지 않습니다. 오직 도착한 데이터를 어떻게 해석(Parsing)하고 사용자 화면에 렌더링할 것인지에만 집중합니다.
- 클라이언트-서버 모델 : 대부분의 응용 계층 프로토콜은 서비스를 요청하는 ‘클라이언트(Client)’와, 항시 대기하며 요청을 처리하는 ‘서버(Server)’구조를 띱니다.
HTTP(HyperText Transfer Protocol)
초창기 팀 버너스리(Tim Berners-Lee)가 연구소 내에서 HTML 문서(HyperText)를 교환하기 위해 만든 이 단순한 텍스트 기반 프로토콜은, 이제 이미지, 동영상, JSON 데이터를 넘어 전 세계의 API 통신을 규격화하는 가장 거대한 응용 계층(L7)의 표준이 되었습니다.
HTTP의 설계와 특징
HTTP는 클라이언트-서버(Client-Server) 모델을 따릅니다. 클라이언트(브라우저)가 요청(Request)을 보내면, 서버는 그에 대한 응답(Response)을 보냅니다. 이 단순한 구조를 지탱하는 두 가지 특징이 있습니다.
무상태성(Stateless)
서버는 클라이언트의 과거 요청을 기억하지 않습니다. 방금 1초 전에 로그인 요청을 보냈던 클라이언트가 다시 데이터를 요청해도, 서버 입장에서는 완전히 새로운 요청으로 취급합니다.
- 압도적 확장성 : 서버가 상태를 기억(메모리 할당)할 필요가 없으므로 서버 자원 낭비가 없습니다. 트래픽이 폭주할 때 앞단에 로드밸런서를 두고 서버를 1대에서 100대로 늘리는 수평 확장(Scale-out) 이 매우 쉽습니다. (어느 서버로 요청이 가든 똑같이 처리)
- 오버헤드 : 클라이언트가 로그인 상태를 유지하려면, 매 요청마다 자신이 누구인지 증명하는 데이터(Cookie, Session ID, 또는 JWT 토큰 등)를 헤더에 무겁게 실어 보내야 합니다.
비연결성(Connectionless)
클라이언트가 요청을 보내고 서버가 응답을 마치면, 맺었던 TCP 연결을 끊어버립니다.
수만 명이 접속하는 웹사이트라도, 실제로 데이터를 주고받는 찰나의 순간에만 자원을 사용하므로 서버가 감당해야 할 동시 접속자 수의 부담이 확연히 줄어듭니다.
하지만 웹 페이지 하나를 띄울 때 HTML, CSS, JS, 이미지 등 수십 개의 리소스를 받아야 하는데, 매번 TCP 3-Way Handshake를 다시 맺고 끊어야 하는 치명적인 지연(Latency)이 발생합니다.
- 현대의 HTTP는 이를 극복하기 위해
Connection: Keep-alive헤더를 기본적으로 사용하여, 일정 시간 동안 TCP 연결을 끊지 않고 재사용(Persistent Connection)합니다.
HTTP 메시지 구조(Message Format)
클라이언트의 HTTP 요청(Request)
1
2
3
4
5
6
GET /api/users?id=123 HTTP/1.1 // 1. 시작 줄 (메서드, 타겟 URL, 버전)
Host: api.example.com // 2. 헤더 (Host는 HTTP/1.1의 필수 헤더)
Authorization: Bearer <token> // 2. 헤더 (인증 정보 등 메타데이터)
Accept: application/json // 2. 헤더 (나는 JSON으로 응답받길 원해)
// 3. 빈 줄 (CRLF - 헤더와 바디의 경계)
// 4. 본문(Body) - GET 요청이므로 보통 비어있음
서버의 HTTP 응답(Response)
1
2
3
4
5
HTTP/1.1 200 OK // 1. 상태 줄 (버전, 상태 코드, 상태 메시지)
Content-Type: application/json // 2. 헤더 (내가 주는 데이터는 JSON이야)
Content-Length: 42 // 2. 헤더 (데이터 길이는 42바이트야)
// 3. 빈 줄 (CRLF)
{"name": "CS", "role": "Senior"} // 4. 본문(Body) - 실제 데이터
메서드(Method)와 멱등성(Idempotency)
HTTP 메서드는 리소스에 대한 행위를 정의하며, 각 메서드는 ‘멱등성’이라는 중요한 속성을 가집니다.
- GET(조회) : 리소스를 가져옵니다. 100번 조회해도 서버의 데이터는 변하지 않습니다. (브라우저 캐싱의 핵심)
- POST(생성/처리) : 리소스를 새로 생성하거나 복잡한 처리를 요구합니다. 결제 버튼을 2번 누르면 중복 결제가 발생할 수 있습니다. (재시도 시 매우 주의)
- PUT(전체 수정) : 리소스를 통째로 교체합니다. 같은 데이터를 100번 덮어써도 결국 마지막 덮어쓴 상태로 똑같아집니다.
- PATCH(부분 수정) : 리소스의 일부만 수정합니다.
- DELETE(삭제) : 리소스를 삭제합니다. 이미 삭제된 것을 100번 더 삭제하라고 지시해도 ‘없다’는 상태는 변하지 않습니다.
멱등성(Idempotency)
연산을 1번 수행하든, 100번 수행하든 그 ‘결과(서버의 상태)가 항상 동일’ 해야 함을 의미합니다. 네트워크 장애 시 클라이언트가 안전하게 요청을 재시도(Retry)할 수 있는지 판단하는 근거가 됩니다.
POST의 경우 결과가 변하므로 멱등하지 않으며, PATCH의 경우 설계하기에 따라 멱등할 수도 있고 아닐 수도 있습니다.
상태 코드(Status Code)
서버가 클라이언트에게 요청의 처리 결과를 알려주는 3자리 숫자입니다. 이것을 표준에 맞게 잘 설계하는 것이 좋은 RESTful API 아키텍처의 기본입니다.
- 1xx(정보) : 요청이 수신되어 진행 중
- 2xx(성공) : 200(OK), 201(Created - POST 성공 시)
- 3xx(리다이렉션) : 301(Moved Permanently). 주소가 이전된 경우, 새 주소로 다시 요청 알림
- 4xx(클라이언트 오류) : 클라이언트가 잘못된 요청을 보냈을 때
- 400(Bad Request : 파라미터 오류), 401(Unauthorized : 인증 안 됨), 403(Forbidden : 권한 없음), 404(Not Found : URL 없음)
- 5xx(서버 오류) : 클라이언트는 정상인데, 서버 내부(DB 연결 실패, Null Pointer Exception 등)에서 로직이 터졌을 때
- 500(Internal Server Error), 502(Bad Gateway : 앞단 Nginx 등은 멀쩡한데 뒷단 WAS가 죽었을 때), 503(Service Unavailable)
버전 별 특징
HTTP/1.0 (1996)
요청마다 새로운 연결을 생성합니다. (비효율적)
HTTP/1.1 (1997)
Keep-Alive로 연결을 재사용 하지만, 기본적으로 하나의 연결에서 요청을 순차적으로 처리합니다.
- HOLB(Head Of Line Blocking) : 앞선 요청(무거운 이미지 다운로드)이 처리될 때까지 뒤의 요청(작은 CSS 파일)들이 큐에서 대기해야하는 병목이 발생합니다.
HTTP/2 (2015)
텍스트 기반을 버리고 바이너리 프레이밍(Binary Framing) 을 도입했습니다.
- 멀티 플렉싱(Multiplexing) : 단일 TCP 연결 안에서 여러 개의 요청과 응답을 순서에 상관없이 섞어서(스트림) 동시에 보냅니다. HTTP 레벨의 HOLB를 해결했습니다.
- 여전히 밑바탕은 TCP를 사용하므로, 네트워크 패킷이 하나라도 유실되면 TCP가 이를 재전송할 때까지 모든 스트림이 멈춰버리는 ‘TCP 레벨의 HOLB’ 문제가 남아있습니다.
HTTP/3 (2022)
구글이 주도하여 만들었으며, 과감하게 TCP를 버리고 UDP 기반의 QUIC 프로토콜을 채택했습니다.
UDP의 빠른 속도를 가져오면서도 암호화(TLS)와 신뢰성 제어를 QUIC 프로토콜 자체에 내장하여, 패킷 하나가 손실되어도 다른 스트림에 전혀 영향을 주지 않습니다. (TCP HOLB 해결) 또한 연결 수립(Handshake) 시간을 0-RTT 수준으로 줄였습니다.
텍스트 프로토콜의 한계
앞서 HTTP는 ‘사람이 읽을 수 있는 텍스트 기반(JSON 등)’이라고 했습니다. 이것은 개발자가 디버깅하기에는 최고의 환경을 제공합니다. 네트워크 패킷을 뜯어보면 무슨 데이터가 오가는지 눈으로 바로 확인할 수 있기 때문입니다.
하지만 JSON은 괄호({}, "")와 띄어쓰기 등 불필요한 데이터가 너무 많고, 파싱(Parsing)하는 데 컴퓨터의 CPU 자원을 상당히 많이 소모합니다. 서버와 서버가 1초만에 수만 번씩 통신하는 현대의 ‘마이크로서비스 아키텍처(MSA)’에서는 이 오버헤드가 치명적입니다.
gRPC와 Protobuf
이를 해결하기 위해 구글은 gRPC라는 새로운 응용 계층 프레임워크를 만들었습니다. 데이터를 텍스트가 아닌 컴퓨터가 가장 좋아하는 바이너리(이진) 형태(Protocol Buffers) 로 압축해서 통신합니다.
사람이 눈으로 패킷을 읽을 수 없게 되는 ‘가독성’을 희생한 대신, 네트워크 대역폭 사용량을 대폭 줄이고 직렬화/역직렬화 성능을 극대화 한 아키텍처입니다.
DNS(Domain Name System)
초창기 인터넷(ARPANET) 시절에는 모든 컴퓨터의 IP 주소 목록을 hosts.txt 라는 단 하나의 텍스트 파일에 적어두고, 사람들이 이 파일을 복사해서 썻습니다. (지금도 윈도우나 리눅스 시스템 폴더에 이 파일이 남아있으며, DNS보다 우선순위가 높습니다.)
하지만 인터넷에 연결되는 컴퓨터가 늘어나면서 치명적인 문제가 발생합니다.
- 단일 장애점(SPOF) : 이 파일을 관리하는 중앙 서버가 죽으면 인터넷 전체가 마비됩니다.
- 확장성(Scalability) 한계 : 매일 수십만 개의 이름이 추가되는데, 이를 하나의 파일로 동기화하는 것은 물리적으로 불가능합니다.
이 문제를 해결하기 위해 데이터를 한 곳에 모아두는 것을 포기하고, 전 세계 수많은 서버에 데이터를 쪼개어 분산시키는 ‘계층형 트리(Tree) 구조의 분산 데이터베이스’를 고안했습니다. 이것이 DNS의 본질입니다.
DNS의 계층 구조(Hierarchical Structure)
도메인 이름은 사실 맨 뒤에 보이지 않는 점(.)이 하나 더 있습니다. (www.example.com.) 이 점을 기준으로 오른쪽에서 왼쪽으로 역순으로 탐색하는 트리 구조입니다.
- Root DNS 서버(
.) : DNS 트리의 최상위 꼭대기입니다. 전 세계에 단 13세트의 원본 IP(A~M)만 존재하며, 하위의 TLD 서버 주소들을 알고 있습니다. - TLD(Top-Level Domain) DNS 서버(
.com,.net등) : 최상위 도메인을 관리합니다..comTLD 서버는 전 세계의 모든.com으로 끝나는 도메인들의 실제 관리 서버(Authoritative) 주소를 알고 있습니다. - Authoritative(권한 있는) DNS 서버(
example.com) : 특정 도메인의 실제 IP 주소(A 레코드)를 최종적으로 쥐고 있는 서버입니다. AWS Route53이나 가비아 같은 호스팅 업체가 이 역할을 수행합니다.
DNS 질의 과정(Resolution Process)
사용자가 브라우저에 www.google.com을 입력했을 때, IP를 찾아오는 과정은 크게 두 가지 방식이 혼합되어 동작합니다.
- 재귀적 질의(Recursive Query) : 클라이언트(내 PC)가 통신사(SKT, KT)나 구글(
8.8.8.8)이 운영하는 ‘로컬 DNS 서버(Resolver)’에게 IP를 찾는 책임을 떠넘기는 방식입니다. - 반복적 질의(Iterative Query) : 로컬 DNS 서버가 Root부터 시작해 TLD, Authoritative 서버까지 차례로 IP를 묻고 다니는 방식입니다.
실제 동작 흐름
- 내 PC가 로컬 DNS에
www.google.com의 IP를 묻습니다. (Recursive) - 로컬 DNS가 모른다면, Root 서버에게 묻습니다. → Root 서버는
.com을 관리하는 TLD 서버 주소를 알려줍니다. (Iterative) - 로컬 DNS가 TLD 서버에게 묻습니다. → TLD 서버는
google.com을 관리하는 Authoritative 서버 주소를 알려줍니다. (Iterative) - 로컬 DNS가 Authoritative 서버에게 묻습니다. → Auth 서버가 실제 IP 주소를 알려줍니다.
- 로컬 DNS는 이 값을 자신의 메모리에 저장(캐싱)하고, 내 PC에 최종 전달합니다.
UDP 사용과 캐싱(Caching)
DNS는 전 세계에서 가장 많이 사용되는 프로토콜입니다. 성능을 극대화하기 위해 두 가지 아키텍처적 선택을 했습니다.
TCP 대신 UDP(포트 53) 사용
IP를 묻고 답하는 데이터는 크기가 매우 작습니다. (보통 512 바이트 이하) 신뢰성을 보장하겠다고 3-Way Handshake를 맺는 TCP를 쓰면 배보다 배꼽이 커지는 지연(Latency)이 발생합니다.
따라서 연결 설정 과정 없이 패킷 하나만 던지고 받는 무상태(Stateless) UDP를 기본으로 사용합니다. (단, 데이터가 크거나 서버 간 영역(Zone) 전송 시에는 예외적으로 TCP를 씁니다.)
TTL(Time To Live)과 캐싱의 딜레마
매번 Root 서버부터 찾아가면 인터넷이 마비될 것입니다. 그래서 브라우저, OS, 공유기, 로컬 DNS 등 모든 구간에서 한 번 찾은 IP를 메모리에 캐싱(저장)해 둡니다.
- 결과적 일관성 : 캐싱 덕분에 속도는 엄청나게 빠르지만, 서비스 운영자가 서버를 이전하여 IP를 바꿨을 때 문제가 생깁니다. 각 캐시에 설정된 수명(TTL)이 만료될 때까지, 일부 사용자들은 옛날 IP로 접속을 시도하는 현상(DNS 전파 지연)을 겪게 됩니다.
DNS 레코드의 종류와 GSLB (글로벌 로드밸런싱)
단순히 IP만 매핑하는 것을 넘어, 다양한 DNS 레코드 타입을 활용하여 인프라를 유연하게 제어합니다.
- A 레코드 : 도메인 이름을 IPv4 주소로 직접 매핑합니다. (
example.com→1.2.3.4) - AAAA 레코드 : 도메인 이름을 IPv6 주소로 매핑합니다.
- CNAME(Canonical Name) : 도메인 이름을 다른 ‘도메인 이름’으로 매핑하는 별칭(Alias)입니다. (
www.example.com→example.com) 서버 IP가 자주 바뀌는 클라우드 환경에서 하드코딩을 피하기 위해 필수적으로 사용합니다. - TXT 레코드 : 도메인에 임의의 텍스트를 저장합니다. 주로 이 도메인이 내 소유임을 구글이나 AWS에 증명(소유권 인증)하거나 이메일 스팸 방지(SPF) 용도로 씁니다.
GSLB(Global Server Load Balancing)
현대 클라우드의 DNS는 단순한 전화번호부가 아닙니다. 사용자가 netflix.com을 입력했을 때, DNS 서버가 사용자의 위치나 현재 서버들의 트래픽 부하 상태를 계산하여 ‘한국 사용자는 서울 리전 IP로, 미국 사용자는 버지니아 리전 IP로’ 응답을 다르게 줍니다. 이것이 글로벌 서비스의 응답 속도를 끌어올리는 GSLB 아키텍처의 핵심입니다.
이메일 프로토콜
이메일 서비스는 보내는 쪽과 받는 쪽의 프로토콜이 엄격하게 분리되어 있습니다.
- SMTP(Simple Mail Transfer Protocol) : 클라이언트가 메일 서버로 이메일을 보낼 때(Push), 또는 메일 서버 간에 메일을 전달할 때 사용합니다.
- POP3(Post Office Protocol version 3) : 사용자가 메일 서버에서 자신의 이메일을 가져올 때(Pull) 사용합니다. 가져온 메일은 서버에서 삭제되는 것이 기본 설정입니다. (오프라인 열람 중심)
- IMAP(Internet Message Access Protocol) : POP3의 단점을 보완하여, 서버에 메일을 그대로 두고 제목만 먼저 동기화하여 다수의 기기(스마트폰, PC)에서 메일함을 일관되게 관리할 수 있게 해줍니다.