URL에서 서버까지
1. 주소만으로는 왜 찾아갈 수 없을까
주소창에 아래 주소를 입력했다고 해 봅시다.
https://shop.example.com/products/1?color=black#review
브라우저가 요청을 보내려면 이 요청을 받을 컴퓨터가 어디에 있는지 알아야 합니다. 그런데 주소를 아무리 들여다봐도 컴퓨터의 위치는 적혀 있지 않습니다. shop.example.com은 사람이 기억하기 쉽게 붙인 이름일 뿐이고, 컴퓨터끼리는 93.184.215.14 같은 숫자 주소로 서로를 찾기 때문입니다.
그래서 브라우저는 요청을 보내기 전에 몇 단계를 거쳐 목적지를 알아냅니다.
- URL 해석: 주소를 조각으로 나눠 서버 이름(
shop.example.com)을 꺼냅니다. - DNS 조회: 서버 이름을 숫자 주소인 IP 주소로 바꿉니다.
- IP로 연결: 그 IP 주소로 찾아가 연결합니다.
- CDN: 이때 연결되는 곳이 원본 서버가 아니라, 사용자와 가까운 CDN 서버일 수도 있습니다.
| 단계 | 들어가는 것 | 나오는 것 |
|---|---|---|
| URL 해석 | 주소 문자열 | 서버 이름, 경로, 쿼리 등 |
| DNS 조회 | 서버 이름 | IP 주소 |
| IP로 연결 | IP 주소와 포트 | 서버와의 연결 |
앞 단계의 결과가 다음 단계의 재료가 됩니다. 이 글에서는 이 순서를 그대로 따라가 보겠습니다.
2. URL
URL은 "어떤 방법으로, 어느 서버의, 어떤 리소스를" 요청할지 적어 둔 주소입니다. 브라우저는 가장 먼저 이 주소를 조각으로 나눕니다.
| 조각 | 예시 | 뜻 |
|---|---|---|
| 스킴 | https | 어떤 방법으로 통신할까? |
| 호스트 | shop.example.com | 어느 서버로 갈까? |
| 포트 | 443 | 서버의 어느 창구로 갈까? 기본값이면 생략합니다. |
| 경로 | /products/1 | 어떤 리소스를 원할까? |
| 쿼리 | ?color=black | 추가 조건은 무엇일까? |
| 프래그먼트 | #review | 페이지의 어느 위치를 보여 줄까? |
코드에서는 URL 객체로 각 조각을 꺼낼 수 있습니다.
const url = new URL("https://shop.example.com/products/1?color=black#review");
url.hostname; // 'shop.example.com'
url.pathname; // '/products/1'
url.searchParams.get("color"); // 'black'
url.hash; // '#review'
조각마다 쓰이는 곳이 다릅니다
나눈 조각은 각자 다른 곳으로 갑니다. 호스트는 다음 단계인 DNS 조회에 쓰이고, 경로와 쿼리는 HTTP 요청에 담겨 서버로 갑니다. 그런데 프래그먼트는 서버로 가지 않습니다. 실제로 서버가 받는 요청을 보면 #review가 어디에도 없습니다.
GET /products/1?color=black HTTP/1.1
Host: shop.example.com
프래그먼트는 브라우저가 페이지 안의 특정 위치로 스크롤할 때만 쓰는 정보이기 때문입니다. 서버 입장에서는 #review가 붙든 안 붙든 똑같은 요청입니다.
스킴, 호스트, 포트 세 가지를 묶어서 출처(origin) 라고 부릅니다. 셋 중 하나라도 다르면 다른 출처로 봅니다. 브라우저는 이 출처를 기준으로 다른 사이트의 데이터에 접근하는 것을 제한합니다(CORS).
3. DNS
URL에서 꺼낸 shop.example.com은 이름입니다. 이 이름을 IP 주소로 바꿔 주는 시스템이 DNS(Domain Name System) 입니다. 전화번호부에서 이름으로 전화번호를 찾는 것과 비슷합니다.
한 서버가 모든 이름을 알지는 못합니다
전 세계의 도메인은 셀 수 없이 많아서, 서버 한 대가 이걸 전부 들고 있을 수는 없습니다. 그래서 DNS는 이름을 오른쪽부터 한 단계씩 나누고, 단계마다 담당 서버를 따로 둡니다.
| 서버 | 알고 있는 것 |
|---|---|
| 루트 서버 | com을 담당하는 서버가 어디인지 |
TLD 서버 (com) | example.com을 담당하는 서버가 어디인지 |
권한 있는 네임서버 (example.com) | shop.example.com의 실제 IP 주소 |
루트 서버와 TLD 서버는 답을 직접 주지 않고 "그건 저 서버에 물어보세요"라고 다음 서버만 알려 줍니다. 실제 IP 주소는 마지막의 권한 있는 네임서버만 알고 있습니다.
찾아다니는 일은 재귀 리졸버가 합니다
그렇다고 브라우저가 이 서버들을 직접 돌아다니지는 않습니다. 통신사 DNS나 8.8.8.8 같은 재귀 리졸버가 대신 찾아 줍니다.
- 브라우저와 OS는 먼저 최근에 조회한 결과(캐시)가 있는지 확인합니다.
- 없으면 재귀 리졸버에 "
shop.example.com의 IP가 뭔가요?"라고 묻습니다. - 재귀 리졸버는 루트 → TLD → 권한 있는 네임서버 순서로 물어 가며 답을 찾습니다.
- 찾은 IP 주소를 브라우저에 돌려줍니다.
브라우저 입장에서는 한 번 묻고 답만 받으면 끝입니다. 최종 답을 달라고 맡기는 이 방식을 재귀 질의, 재귀 리졸버가 서버마다 "다음엔 어디에 물어야 하는지"를 받아 가며 찾는 방식을 반복 질의라고 부릅니다.
결과는 TTL 동안 캐시됩니다
매번 루트 서버부터 물어보면 너무 느립니다. 그래서 한 번 찾은 결과는 브라우저, OS, 재귀 리졸버에 저장해 두고 다시 씁니다. 얼마나 오래 저장할지는 DNS 응답에 들어 있는 TTL(Time To Live) 이 정합니다.
$ dig shop.example.com
;; ANSWER SECTION:
shop.example.com. 300 IN A 93.184.215.14
300이 TTL입니다. 이 답을 300초 동안은 다시 묻지 않고 그대로 쓰라는 뜻입니다. 같은 재귀 리졸버를 쓰는 사람이 많기 때문에, 자주 쓰이는 도메인은 대부분 캐시에서 바로 답이 나옵니다.
캐시 덕분에 빠르지만, 대신 생기는 문제도 있습니다. 서버를 옮기면서 DNS의 IP를 바꿔도, 이전 답이 여러 캐시에 TTL 동안 남아 있습니다. TTL이 하루였다면 길게는 하루 동안 일부 사용자가 옛 서버로 접속합니다. 그래서 서버를 옮기기 전에는 TTL을 미리 짧게 줄여 둡니다.
4. IP
DNS 덕분에 93.184.215.14라는 주소를 얻었습니다. IP 주소는 네트워크에서 컴퓨터를 찾아가기 위한 주소입니다. 데이터는 이 주소를 보고 여러 라우터를 거쳐 목적지 컴퓨터까지 전달됩니다.
IP 주소만으로는 부족합니다
IP 주소는 어느 컴퓨터로 갈지만 알려 줍니다. 그런데 컴퓨터 한 대에서는 여러 프로그램이 동시에 돌아가고 있습니다. 그중 어느 프로그램이 요청을 받을지도 정해야 하는데, 이걸 정하는 번호가 포트입니다.
93.184.215.14 : 443
어느 컴퓨터? 어느 프로그램?
URL에 포트를 적지 않으면 스킴의 기본값을 씁니다. HTTPS는 443, HTTP는 80입니다. 브라우저는 이 IP와 포트로 서버에 연결한 다음 HTTP 요청을 보냅니다.
한 IP에 여러 사이트가 있을 수 있습니다
서버 한 대에서 여러 사이트를 함께 운영하는 경우는 흔합니다. 그러면 같은 IP로 들어온 요청이 어느 사이트로 가는 요청인지 서버가 구분할 수 있어야 합니다. 그래서 브라우저는 요청에 도메인 이름을 함께 담아 보냅니다. 2번에서 본 요청의 Host: shop.example.com이 바로 그 역할입니다. HTTPS 연결 과정에서는 SNI라는 값으로도 도메인 이름을 전달합니다.
IP 주소를 주소창에 직접 입력하면 엉뚱한 사이트가 보이거나 인증서 경고가 뜨는 이유도 여기에 있습니다. 서버가 어느 사이트를 원하는지 알 수 없기 때문입니다. 도메인과 IP는 하나씩 짝지어진 관계가 아닙니다.
IPv4와 IPv6
| IPv4 | IPv6 | |
|---|---|---|
| 예시 | 93.184.215.14 | 2606:2800:21f:cb07::1 |
| 주소 개수 | 약 43억 개 | 사실상 무한 |
IPv4는 인터넷에 연결된 기기 수에 비해 주소가 부족합니다. 그래서 더 긴 IPv6가 만들어졌고, 지금은 두 방식이 함께 쓰이고 있습니다.
공인 IP와 사설 IP
IPv4 주소가 부족한 문제를 버티는 또 다른 방법이 사설 IP입니다. 집 노트북의 IP를 확인해 보면 192.168.0.10 같은 주소가 나옵니다. 이건 집 안에서만 쓰는 사설 IP이고, 인터넷에서 통하는 공인 IP는 공유기가 가지고 있습니다.
노트북이 인터넷으로 요청을 보내면, 공유기가 출발지 주소를 자기 공인 IP로 바꿔서 내보냅니다. 이걸 NAT라고 합니다. 그래서 서버가 보는 IP는 노트북이 아니라 공유기의 주소입니다. 같은 공유기를 쓰는 여러 사람이 서버에게는 같은 IP로 보이므로, IP만으로 사용자를 구분하면 정확하지 않습니다.
5. CDN
쇼핑몰 서버가 미국에 있다고 해 봅시다. 서울의 사용자가 상품 이미지를 받으려면 데이터가 태평양을 왕복해야 합니다. 거리가 멀수록 느려지고, 전 세계의 요청이 서버 한 대에 몰리면 서버도 버티기 힘듭니다.
CDN(Content Delivery Network) 은 세계 곳곳에 서버를 두고, 그 서버들에 콘텐츠를 복사해 두었다가 사용자와 가까운 곳에서 응답하는 서비스입니다. 이렇게 사용자 가까이에 놓인 서버를 엣지 서버, 원래 콘텐츠를 가진 서버를 원본 서버라고 부릅니다.
가까운 엣지 서버는 DNS 단계에서 정해집니다
그렇다면 브라우저는 어떻게 가까운 엣지 서버를 찾아갈까요? 답은 3번에서 본 DNS에 있습니다.
브라우저는 서버에 연결하기 전에 항상 DNS에 "이 도메인의 IP가 뭔가요?"라고 묻습니다. CDN은 이 질문에 묻는 사람의 위치에 따라 다른 IP를 알려 줍니다.
| 어디서 물었나 | DNS가 알려 주는 IP |
|---|---|
| 서울 | 서울 엣지 서버의 IP |
| 런던 | 런던 엣지 서버의 IP |
같은 static.example.com을 물어도 서울 사용자는 서울 엣지로, 런던 사용자는 런던 엣지로 연결됩니다. 브라우저는 평소처럼 DNS가 알려 준 IP로 연결했을 뿐인데, 자연스럽게 가까운 서버로 가게 됩니다.
이렇게 하려면 DNS 질문이 CDN에게 가야 합니다. 그래서 사이트 운영자는 DNS에 CNAME을 설정해 둡니다. CNAME은 "이 도메인의 IP는 저 도메인에 물어보세요"라고 다른 도메인으로 넘겨주는 설정입니다. 운영자의 도메인을 CDN의 도메인으로 넘겨 두면, IP를 묻는 질문이 CDN에게 갑니다.
static.example.com → (CNAME) → CDN의 도메인 → 가까운 엣지의 IP
나머지는 CDN이 알아서 합니다. 운영자는 엣지 서버가 어디에 몇 대 있는지 몰라도 됩니다.
엣지 서버에 파일이 없을 때
엣지 서버라고 처음부터 모든 파일을 갖고 있지는 않습니다.
- 캐시 히트: 엣지에 파일이 있으면 바로 응답합니다. 원본 서버까지 가지 않습니다.
- 캐시 미스: 엣지에 파일이 없으면 원본 서버에서 받아 와 저장한 뒤 응답합니다. 다음 사용자부터는 캐시 히트가 됩니다.
엣지가 무엇을 얼마나 오래 저장할지는 원본 서버가 응답 헤더로 알려 줍니다.
Cache-Control: public, max-age=31536000
"누구나 함께 써도 되고(public), 1년 동안 저장해도 된다"는 뜻입니다.
CDN 캐시는 여러 사용자가 함께 씁니다
여기서 조심해야 할 점이 있습니다. 브라우저 캐시는 나 혼자 쓰지만, CDN 캐시는 그 엣지를 거치는 모든 사용자가 함께 씁니다. 로그인한 사용자 A의 정보가 담긴 응답을 엣지가 저장하면, 같은 URL을 요청한 사용자 B에게도 A의 정보가 그대로 전달됩니다.
그래서 사용자마다 다른 응답에는 공유 캐시에 저장하지 말라는 뜻으로 Cache-Control: private이나 아예 저장하지 말라는 no-store를 붙입니다.
6. 한눈에 보는 표
| 단계 | 하는 일 | 핵심 개념 | 주의할 점 |
|---|---|---|---|
| URL | 주소를 조각으로 나눔 | 스킴, 호스트, 포트, 경로, 쿼리, 프래그먼트 | 프래그먼트는 서버로 가지 않음, 쿼리 값은 인코딩 |
| DNS | 이름을 IP 주소로 바꿈 | 재귀 리졸버, 루트 → TLD → 네임서버, TTL | 바꾼 설정이 TTL 동안 반영되지 않음 |
| IP | 목적지 컴퓨터를 찾아감 | IP는 컴퓨터, 포트는 프로그램 | 한 IP에 여러 사이트, Host로 구분 |
| CDN | 가까운 엣지에서 응답 | 엣지·원본 서버, 캐시 히트·미스 | 공유 캐시이므로 개인 데이터는 private·no-store |
"주소창에 URL을 입력한 뒤 화면이 표시될 때까지 무슨 일이 일어나는가?"라는 질문에서, 이 글은 요청을 보내기 전에 어디로 보낼지 정하는 구간을 다룹니다. 그다음의 요청과 응답은 HTTP 통신 구조로 이어집니다.
7. 퀴즈로 확인하기
퀴즈 1 (URL). 사용자가 https://shop.example.com/products/1#review로 접속했는데, 서버 로그에는 /products/1만 남았습니다. 왜 그럴까요?
정답 보기
프래그먼트(#review)는 서버로 보내지 않기 때문입니다.
프래그먼트는 브라우저가 페이지 안의 특정 위치로 스크롤할 때만 쓰는 정보입니다. 브라우저는 HTTP 요청을 만들 때 경로와 쿼리만 담고 프래그먼트는 빼므로, 서버는 #review가 있었다는 사실 자체를 알 수 없습니다.
퀴즈 2 (DNS). 서버를 옮기면서 api.example.com의 IP를 새 서버 주소로 바꿨습니다. 바꾸기 전 TTL은 86400(하루)이었습니다. 몇 시간이 지났는데도 일부 사용자가 옛 서버로 접속합니다. 왜 그럴까요?
정답 보기
이전 DNS 답이 재귀 리졸버, OS, 브라우저 캐시에 최대 하루 동안 남아 있기 때문입니다.
캐시는 TTL이 끝나기 전까지 DNS 서버에 다시 묻지 않고 저장해 둔 답을 그대로 씁니다. 그래서 TTL이 끝나지 않은 사용자는 계속 옛 IP로 접속합니다. 서버를 옮기기 전에 TTL을 미리 짧게 줄여 두고, 옮기는 동안은 옛 서버도 함께 켜 두면 전환을 매끄럽게 할 수 있습니다.
퀴즈 3 (IP). dig shop.example.com으로 알아낸 IP 주소를 주소창에 직접 입력했더니, 쇼핑몰이 아닌 다른 페이지가 나왔습니다. 서버가 고장 난 걸까요?
정답 보기
꼭 그렇지는 않습니다.
한 IP에서 여러 사이트를 운영하는 서버는 요청에 담긴 도메인 이름(Host 헤더, HTTPS의 SNI)을 보고 어느 사이트로 응답할지 정합니다. IP로 직접 접속하면 shop.example.com이라는 이름이 전달되지 않으므로, 서버는 기본으로 설정된 사이트를 보여 주거나 이름이 맞지 않는 인증서를 보냅니다. 그 IP가 CDN의 엣지 서버라면 이런 일이 더 흔합니다.
퀴즈 4 (CDN). /api/me 응답을 빠르게 하려고 아래 헤더를 붙였습니다. CDN을 거친 뒤 일부 사용자 화면에 다른 사람의 이름이 보입니다. 무엇이 문제일까요?
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
{"name":"김에이셉"}
정답 보기
public 때문에 CDN이 응답을 공유 캐시에 저장했기 때문입니다.
CDN 엣지는 처음 요청한 사용자의 /api/me 응답을 저장해 두고, 60초 동안 같은 URL을 요청한 모든 사용자에게 그 응답을 그대로 돌려줬습니다. CDN 캐시는 여러 사용자가 함께 쓰기 때문입니다. 사용자마다 달라지는 응답에는 Cache-Control: private이나 no-store를 붙여서 공유 캐시에 저장되지 않도록 해야 합니다.