'www.google.com'을 URL 입력창에 입력하면 어떻게 될까?
: 흔히 면접에서 단골 질문(?)으로 나오는 이 질문에 대해서 파헤쳐보자
Chapter
- URI, URL, URN ?
- HTTP & HTTPS + HTTP message
- DNS, IP주소, 도메인 주소
- 프로토콜 스택 with socket library
- TCP connection (3,4 way handshake)
- TCP vs UDP
- OSI 7 layer
- DOM + CSSOM => Render Tree
- JS parsing(+ defer & async attribute) & AST(Abstract Syntax Tree)
- layout and paint
- relow and repaint(re-rendering)
HTTP & HTTPS + HTTP message
HTTP의 문제점
: 앞서 1편에서 HTTP에 대해서 설명했는데, 이러한 HTTP에는 보안상의 취약점이 존재한다. TCP/IP 프로토콜(TCP/IP 프로토콜에 대한 설명은 OSI 7 layer 설명부분에 나온다)을 기반으로 통신을 하는 HTTP는 평문 방식(암호화를 하지 않은 방식)으로 데이터를 전달하는데, TCP/IP 구조 자체가 중간에서 도청이 가능한 네트워크이기 때문에 도청이 가능하게 된다. 또한, 통신 상대를 확인하지 않기 때문에 위장 또한 가능하다. 하나더! '완전성을 증명할 수 없기 때문에 변조가 가능하다'. 여기서 완전성이란 정보의 정확성을 의미한다. 서버 또는 클라이언트에서 수신한 내용이 송신측에서 보낸 내용과 일치한다라는 것을 보장할 수 없는 것이다. 리퀘스트나 리스폰스가 발신된 후에 상대가 수신하는 사이에 누군가에 의해 변조되더라도 이 사실을 알 수 없다. 이와 같이 공격자가 도중에 리퀘스트나 리스폰스를 빼앗아 변조하는 공격을 중간자 공격(Man-in-the-Middle)이라고 부른다.
보완 방법 = HTTPS
: HTTPS란 HTTP의 통신 상에서의 소켓 부분(socket이 뭔지는 추후 프로토콜 스택 설명 부분에 나온다)을 SSL or TLS이라는 프로토콜로 대체할 뿐인 것!. 즉, 본래 HTTP ⇒ TCP/IP 였다면, HTTPS에서는 HTTP ⇒ SSL or TLS ⇒ TCP 가 되는 것(중간에 매개체가 생긴 것).
- 통신 자체를 암호화
SSL(Secure Socket Layer)
or TLS(Transport Layer Security)
라는 다른 프로토콜을 조합함으로써 HTTP 의 통신 내용을 암호화할 수 있다. SSL 을 조합한 HTTP 를 HTTPS(HTTP Secure)
or HTTP over SSL
이라고 부른다.
- 콘텐츠를 암호화 말 그대로 HTTP 를 사용해서 운반하는 내용인, HTTP 메시지에 포함되는 콘텐츠만 암호화하는 것이다. 암호화해서 전송하면 받은 측에서는 그 암호를 해독하여 출력하는 처리가 필요하다.
- 또한 상대를 확인하지 않아서 발생하는 문제에 대해서도 보완이 가능하다. 상대를 확인하지 않는 기존 HTTP 통신 방법은 어떤 요청이 와도 response를 보내게 되고, 어떤 request가 와도 수신을 하게 된다. 따라서 이를 위해 SSL에서 제공하는 '증명서'를 이용하면된다. 제3 기관에서 제공하는 이 증명서를 통해 상대가 내가 통신하고자 했던 상대임이 증명되고, 앞선 문제가 사라진다. 이에 더하여 클라쪽에서는 이를 가지고 '인증'을 받을 수도 있다(http 특성상 인증이 어려운데 이를 통해 구현이 쉬워짐).
- 중간자 공격에 대해서는 MD5, SHA-1 등의 해시 값을 확인하는 방법과 파일의 디지털 서명을 확인하는 방법이 존재하지만 확실히 확인할 수 있는 것은 아니다. 확실히 방지하기에는 HTTPS를 사용해야 한다. SSL 에는 인증이나 암호화, 그리고 다이제스트 기능을 제공하고 있다.
모든 웹 페이지에서 HTTPS를 사용해도 될까?
: 평문 통신에 비해서 암호화 통신은 CPU나 메모리 등 리소스를 더 많이 요구한다. 통신할 때마다 암호화를 하면 추가적인 리소스를 소비하기 때문에 서버 한 대당 처리할 수 있는 리퀘스트의 수가 상대적으로 줄어들게 된다.
하지만 최근에는 하드웨어의 발달로 인해 HTTPS를 사용하더라도 속도 저하가 거의 일어나지 않으며, 새로운 표준인 HTTP 2.0을 함께 이용한다면 오히려 HTTPS가 HTTP보다 더 빠르게 동작한다. 따라서 웹은 과거의 민감한 정보를 다룰 때만 HTTPS에 의한 암호화 통신을 사용하는 방식에서 현재 모든 웹 페이지에서 HTTPS를 적용하는 방향으로 바뀌어가고 있다.
여기까지 정리
: 여기까지 URL 입력창에 'www.google.com'을 입력하고, 엔터를 친다음에 브라우저가 이 정보들을 파싱한 다음에 최종적으로 이 정보를 바탕으로 http message를 만드는 과정까지 필요한 용어 및 개념들을 살펴봤다. 이제 http message를 만든 브라우저는 이것을 실제로 클라이언트가 원하는 서버(구글)에 전송(혹은 송출)을 해야한다. 택배를 보내는 과정을 생각해보면, 이제 우리는 택배에 담을 물건 혹은 편지와 택배 포장까지 마친 상태이다(HTTP message or HTTPS message) 그러면 이제 수신지 주소를 적고, 우체국 or 배송 업체에 위탁하는 일이 남아있다.
DNS, IP주소, 도메인 주소
: 생각해보면 우리는 통합 자원 위치 혹은 통합 자원 식별자인 URL or URI를 통해 서버의 위치를 이미 알려줬다고 생각된다. 그러나, 앞서 말한 적이 있지만 실제로 OSI 7 layer의 과정을 거쳐서 네트워크 소프트웨어가 메시지를 송출하는 과정에서는 'www.google.com' 과 같은 '도메인 주소'가 아닌 'IP주소'가 필요하다.
브라우저 캐시 및 hosts 파일 참조
: 그전에 먼저 이전에 우리가 'www.google.com'을 검색했었다면 이미 브라우저 캐시 혹은 OS 마다 하나씩은 가지고 있는 hosts 파일에 해당 도메인에 매칭되는 ip주소 정보가 있을 것이다. 만약에 여기를 참조했을 때 이미 IP주소가 있다면, 굳이 DNS(Domain Name Server)에 질의를 하지 않아도 된다.
DNS에 질의
: 하지만, 어디에도 정보가 없다면 어쩔 수 없이 이러한 '도메인 주소 : IP주소' 매칭 정보를 가지고 있는 Domain Name Server에 물어봐야한다. 뒤에서도 나오지만 모든 것을 우직하게(?) 해주는 브라우저에게는 사실 '송출 기능' 혹은 역량이 없다. 따라서 프로토콜 스택에 이에 대한 의뢰를 해야한다. DNS도 서버이고, 서버와의 통신을 위해서는 http message를 보내는 것처럼 송출을 해야하는데, 이 때 위탁하는 곳이 프로토콜 스택(OS 내의 네트워크 제어용 소프트웨어)이다. 이 때, 브라우저는 직접 프로토콜 스택과 컨택하지 않고, 우리가 모듈을 import 해서 쓰듯이 socket library를(프로토콜 스택과 브라우저의 매개체) 써서 컨택한다. gethostbyname() 이라는 메서드를 써서 프로토콜 스택에 의뢰를하고, 프로토콜 스택이 DNS와 실제로 통신을 해서 ip 주소값을 받아오게 되낟. 이 때, 소켓 라이브러리의 메서드(gethostbyname)와 같이 dns를 받아오는 매개체를 'DNS 리졸버(resolver)'라고 부른다.
DNS 내에서 ip주소를 찾는 과정
: DNS는 Local DNS(SKT, KT 등)를 중심점으로 root DNS, .com DNS, domain DNS 이런식으로 계층적 구조 혹은 트리구조로 돼있고, 그 안에서도 서로에게 질의를 한다. 즉, local DNS가 ip주소 질의 요청을 받아서 root DNS를 먼저 조회해서 그에 맞는 주소값이 있는지 찾고, 없으면 다시 local DNS가 없다는 응답을 받고, .com DNS에 질의를 해서 찾고, 없으면 다시 다음 계층으로 묻는 식으로 진행된다. 이렇게 최종적으로 ip주소를 찾으면 local DNS가 이를 응답해준다.
여기까지 정리
: 여기까지 DNS를 통해 IP 주소값까지 얻었다. 이제 우리는 택배로 치면 수신지의 주소를 명확히 알았다. 이제 택배사에 주소를 적어놓은 택배를 위탁하면 된다.
프로토콜 스택 with socket library
: 앞서 말했듯이 브라우저 자체는 송출 기능이 없기 때문에 OS 내의 소프트웨어인 프로토콜 스택에 위탁을 해야한다. 그리고 그 위탁 과정은 socket library를 매개해서 이뤄진다. 그러면 이번에는 DNS가 아닌 실제 http message를 송출하는 과정을 프로토콜 스택 및 socket library와 관련하여 알아보자.
- socket() : 먼저, 브라우저는 데이터를 전송하기 전에 소켓을 생성한다. 이 소켓은 명력 prompt 혹은 command 창에 netstat을 입력하면 나오는
위에서 각 row가 하나의 소켓이 된다. 소켓은 구체적인 실체가 없는데, 간단히 말해서 '프로토콜 스택이 통신을 할 때 참조하는 제어 정보가 담긴 메모리 영역' 이라고 할 수 있다(메모리 영역은 프로토콜 스택이 지정하여 할당한다). 그래서 통신 상대의 IP주소, 프로토콜 종류(tcp or udp), 현재 통신 상태 등의 정보가 담겨있고, 이를 바탕으로 통신 상에서 행동을 정하게 된다(프로토콜 스택이). 다시 돌아와서 브라우저가 소켓 라이브러리의 socket() 메서드를 쓰면, 위의 row 한줄이 생성되고, 생성됐다는 상태로 초기화된다. 하지만 이외에 어떠한 정보도 아직 담겨있지 않은 상태이다. 그리고 socket() 메서드는 'descriptor(디스크립터)'를 브라우저에게 리턴해주고, 브라우저는 이를 브라우저 내의 메모리에 저장해둔다. 이 때, 디스크립터는 생성한 소켓을 가리키는 식별자와 같다.
- connect() : 그런 다음에 브라우저는 아까 받은 디스크립터, 상대의 IP주소(DNS로부터 받은), 포트 넘버 등을 매개변수로 하여 connect() 메서드를 호출한다(전부다 소켓 라이브러리에서 제공). 사실 이 connect() 메서드에서 행해지는 과정이 우리가 흔히 알고 있는 '3 way handshake' 과정이다.
TCP connection (3 way handshake)
: 여기서 잠깐 흐름을 끊고, 3 way handshake에 대해서 알아보자
3 way handshake란 ?
: 보통 커넥션을 생성한다고 하는데, '데이터 송수신을 할 수 있는 상태'가 커넥션이 이뤄진 상태라고 할 수 있다. 그래서 3 way handshake가 원활히 이뤄지면 데이터를 송수신 할 수 있는 상태가 된다. 이 때, 알아둬야할 점은 3 way handshake는 only TCP 에서만 쓴다는 것이다. 즉, 비교군인 UDP에서는 이러한 커넥션 생성 과정이 생략된다(이 후 알게될 4 way handshake 과정 또한 생략한다 udp에서는).
3 way handshake의 과정
- 어느 쪽에서 시작하던지 먼저 보내는 송신측에서 패킷에 TCP Header 만 넣어서(아직 데이터를 보내는 단계가 아니라 데이터는 비어있고 헤더만 보낸다) 보낸다. 이 때, TCP Header 안에는 플래그 비트 중에 SYN 비트에 1을 넣어서 보내게 되고, 추가로 시퀀스 넘버에 임의의 난수를 넣어서 보낸다(이 때, 시퀀스 넘버는 데이터를 순차적으로 보내고, 받기 위한 부가정보이고, 난수를 쓰는 것은 악의적인 공격을 방지하기 위해서이다). 그러면 수신측에서는 받은 시퀀스 넘버에 + 1을 해준 값을 ACK 넘버로 해서 똑같이 패킷에 TCP Header만 넣어서 보내고, 이번에는 플래그 비트 중에 ACK 비트를 쓰고 똑같이 ACK(1) 을 넣어준다. 이에 더하여, 방금 수신한 쪽에서도 처음 송신한 측과 똑같이 SYN(1) + 시퀀스 넘버를 보내주고, 처음 송신한 쪽도 이를 받아서 똑같이 ACK(1)과 ACK 넘버(아크 넘버)를 보내주고 마무리한다.
: 참고로 위에서 언급한 TCP Header 내에는 위와 같은 정보들이 있다. 먼저, 송신 및 수신측의 포트넘버는 TCP 자체가 전송계층(4계층)에 해당하는 프로토콜이고, 전송 계층에서는 포트 넘버를 패킷에 넣는 역할을 하는 계층이기에 TCP Header에 이와 같이 포트 넘버를 담게 된다. 나머지 중에 '시퀀스 넘버', 'ack 넘버', 컨트롤 비트(플래그 비트)는 알아봤고, 후에 윈도우에 대해서 알아볼 것이다.
: 결론적으로 위와 같은 과정을 거치기 때문에 3 way handshake이다(3번의 교류). 위의 과정을 전화 통화에 비유해보면, 먼저 한쪽에서 "너지금 전화 가능해?"라고 묻는다 => 그럼 다른 쪽에서 "응 나 가능해" 라는 응답과 "너 지금 전화 가능해?"라고 똑같이 묻게된다. => 그러면 처음에 물어본 쪽에서도 "응 나 가능해"라고 응답함으로써 둘 다 전화가 가능한 상태임이 확인됐고(데이터 송수신이 가능한 상태), 그 때부터 전화를 하면 된다.
커넥션의 생성
: 위에서 말했듯이 앞서 말한 3 way handshake 과정이 끝나면 이제서야 데이터 송수신을 할 수 있는 상태가 된다. 클라이언트 및 서버 각각에서 소켓에 상대의 포트 넘버 정보와 현재 연결 상태 등을 입력한 채로 실제 송수신을 대기하게 된다. 추가적으로 connect() 단계에서 데이터 송수신을 할 때 사용할 버퍼 메모리 영역도 할당한다.
TCP vs UDP
: 4 way handshake 및 실제 데이터 송수신 과정으로 넘어가기 전에 앞서 말한 TCP에 의해 진행되는 3 way handshake에서의 TCP란 무엇이고, 그와의 비교군인 UDP는 뭔지 알아보고 넘어가자.
- TCP : 사실 TCP는 'UDP + 엄격함, 안전성' 이라고 할 수 있다. 본래 UDP가 기본이고, 거기에 +a를 해서 조금은 무겁게 된 프로토콜이 TCP 인 것이다. TCP는 UDP와 같이 OSI 7 layer의 4계층(전송계층)에 해당하는 프로토콜로 통신하는 상대의 특정 프로세스를 가리키는 포트넘버를 패킷에 넣는 역할을 한다. 이 때, TCP와 UDP는 둘다 통신을 가능하게 해주는 프로토콜인데, TCP는 간략하게 '속도를 포기하면서 안정성을 가져가는 프로토콜'이다. 무슨말인가 하면, TCP는 앞서 말했듯이 3 way handshake(4 way handshake) 등의 커넥팅 과정을 포함하면서까지 완벽한 통신을 지향한다(안전한). 또한, 시퀀스 넘버, 아까 말한 윈도우 사이즈, ACK 응답이 온 것을 확인하는 과정을 포함하는 등의 데이터의 순차성 보증, 네트워크 혼잡 제어, 송수신 측간의 네트워크 속도 차이를 계산한 제어 등을 제공한다. 이러한 안정성은 TCP의 장점이지만, 동시에 이러한 과정을 거쳐야함에 따라 그 속도면에서 상당히 무거운 편이다. 이에 더하여 중요하지 않은 이미지 정보를 TCP로 보내면 사소한 픽셀 단위의 정보를 100% 보장형으로 보내기 위해 too much(?)한 비용을 들이게 된다. 이에 따라 TCP는 은행 거래 등 그 안정성과 확실성이 보장돼야하는 분야에서 사용한다. 또한 HTTP/2.0까지 TCP를 썼고, HTTP/3.0에서는 UDP를 커스터마이징한 구글의 QUIC이 쓰인다.
- UDP : TCP처럼 header에 시퀀스 넘버, 플래그 비트 등 안정성을 위한 정보들이 포함돼 있지 않다. 커넥팅 과정(핸드쉐이크) 또한 없고, 단방향 통신으로 수신측에서 이를 받았는지는 확인하지 않는다(개의치 않는다..). 간략하게 정리하면 '속도를 가져가면서 안정성 등은 어느정도 포기하는 프로토콜'이다. 그래서 동영상 스트리밍과 같이 안정성보다는 속도가 중요한 부분에 쓰인다. 게이트웨이, DNS 서버정보 및 IP 주소를 받는 DHCP나 DNS에 도 UDP를 쓴다. HTTP/3.0인 QUIC에서도 UDP를 커스터마이징해서 쓴다고함(TCP 안씀).
프로토콜 스택 with socket library
: 그럼 다시 프로토콜 스택의 socket 라이브러리에 따른 데이터 송수신 과정으로 돌아와보자. 아까 connect()를 통해 3 way handshake 과정까지 알아봤다. 그러면 이제 '데이터를 송수신 할 수 있는 상태'가 됐으므로 실제 데이터를 송수신 해보자.
- write() : 데이터를 보내는 소켓 라이브러리의 메서드이다. 소켓 정보를 바탕으로 실제로 데이터의 송신이 이뤄지고, 수신측에서는 수신이 이뤄진다(수신측에서는 read()를 한다). 이 때, 아까 말했던 시퀀스 넘버, ACK 넘버, 그리고 윈도우 사이즈를 사용해서 송수신을 한다. 먼저, 시퀀스 넘버를 통해 송신측에서는 몇번째 데이터임을 표기한다. 그러면 수신측에서는 시퀀스 넘버와 받은 패킷에서 헤더 부분을 뺀 순수 데이터 부분의 길이를 바탕으로 여태까지 받은 데이터의 길이를 얻어서 그 값에 +1을 하여 ACK 넘버로 다시 송신측에 넘긴다. 그러면 송신측에서는 이를 통해 데이터를 잘받았구나를 확인하고 남은 데이터를 넘기는 식으로 진행된다(데이터가 큰 경우에만 이렇게 분할해서 보낸다). 이 때, ACK(1) 플래그 비트를 받을 때까지 송신측에서는 보냈던 자료를 버퍼 메모리에 저장해두고, ACK가 돌아오지 않으면 일정 시간 뒤에 다시 보낸다. 계속 오지 않으면 에러를 리턴하고 송수신을 취소한다. 이 때, ACK 요청을 받고, 데이터를 보내고, 다시 ACK를 받고 데이터를 보내고 하는 '핑퐁방식'으로 송수신을 하면 100% 안정성을 보장받을 수 있지만, 속도가 그만큼 느려지게 된다. 이를 효율화하기 위해 '윈도우 사이즈' 헤더 정보를 사용한다. 이는 수신측의 버퍼 메모리에 잔여 공간을 표기한 헤더값으로 송신측에서는 이를 바탕으로 얼마의 데이터를 연속으로 ACK 비트를 신경쓰지 않고 보낼지 결정한다. 물론 최종적으로 보낸 것에 대해 ACK 비트를 돌려받을 때까지 대기하겠지만, 핑퐁방식으로 하는 것이 아닌 일단 전부다 보낼 수 있게 된다(윈도우 사이즈를 참조하여). 만약 윈도우 사이즈를 참조하지 않고, 무작정 데이터를 보내면 수신측에서 브라우저로 데이터를 파싱해서 보내기전에 버퍼 메모리가 오버플로하는 일이 발생할 수 있다. 이런식으로 데이터를 read & write 하며 송수신이 이뤄진다.
- read() : 데이터를 읽는 과정은 write() 과정을 하면서 설명했다.
- close() : 마지막으로 송수신 과정을 위해 만들었던 커넥션을 끊고 소켓을 말소하며 연결이 끊긴다.
4 way handshake
: to be continued
references
- 성공과 실패를 결정하는 1%의 네트워크 원리(Tsutomu Tone)
- 모던 자바스크립트 Deep Dive(이웅모)