지금까지 다룬 요청과 응답, 헤더와 상태 코드는 메시지의 논리적 구조에 관한 것이었다. 그 메시지가 실제로 네트워크 위를 어떻게 지나가는지는 그 아래 계층의 몫이다. 이번 편은 그 아래 계층으로 내려가, 메시지를 안전하게 나르는 암호화와 메시지를 빠르게 나르는 규약의 진화를 다룬다. 개념 자체는 바뀌지 않지만, 그 개념이 실려 가는 통로는 계속 발전해 왔다.


보안 연결과 규약의 새 버전은 개발자가 매번 직접 손대는 영역은 아니다. 앞단의 서버나 배포망이 대부분 알아서 처리하기 때문이다. 그러나 성능 문제나 인증서 오류를 마주치면 결국 이 계층을 이해해야 원인을 짚을 수 있다. 이번 편은 평문 전송의 위험, 암호화가 그것을 어떻게 막는지, 그리고 규약의 두 세대에 걸친 성능 개선이 무엇을 바꾸었는지를 순서대로 살펴본다.

평문의 위험

초기의 HTTP는 메시지를 아무런 보호 없이 그대로 전송했다. 요청과 응답이 사람이 읽을 수 있는 텍스트로 오가는데, 그 텍스트가 암호화되지 않은 채 네트워크를 지난다는 뜻이다. 이는 메시지가 지나는 경로 어딘가에서 누군가 그 내용을 그대로 들여다볼 수 있음을 의미한다. 평문 전송의 근본 문제가 여기에 있다.


메시지가 지나는 경로에는 여러 중간 지점이 있다. 사용자의 기기에서 서버까지 신호는 여러 장비를 거쳐 흐르고, 그 사이 어느 지점에서든 내용이 노출될 수 있다. 로그인 정보나 개인 정보가 평문으로 오가면, 그 경로를 감시하는 누군가에게 고스란히 넘어간다. 내용을 훔쳐보는 것을 도청이라 부른다.


더 위험한 것은 내용을 바꿔치기하는 변조다. 중간의 누군가가 오가는 메시지를 몰래 열어볼 뿐 아니라 그 내용을 바꿔 전달하면, 사용자는 서버가 보낸 것과 다른 응답을 받는다. 받은 내용이 진짜 서버가 보낸 것인지 확인할 방법이 평문에는 없다. 도청이 비밀을 깨뜨린다면 변조는 신뢰 자체를 깨뜨린다.


세 번째 위험은 상대가 진짜인지 모른다는 것이다. 접속한 서버가 정말 의도한 그 서버인지, 아니면 그 서버인 척하는 가짜인지를 평문 통신은 보장하지 못한다. 가짜 서버에 정보를 넘기고도 진짜에 넘긴 줄 알 수 있다. 도청과 변조와 위장이라는 세 위험이 평문 전송에 함께 도사린다.

암호화된 통로

이 위험을 막는 것이 보안 연결이다. HTTP 메시지 자체는 그대로 두고, 그 메시지가 지나가는 통로를 암호화한다. 메시지가 통로에 들어가기 전에 암호화되고 통로를 빠져나온 뒤 복호화되므로, 통로 위를 흐르는 동안은 내용을 알아볼 수 없는 형태가 된다. 도청자가 신호를 가로채도 의미 없는 데이터만 얻는다.


이 암호화는 응용 계층과 전송 계층 사이에 놓인 별도의 보안 계층이 담당한다. HTTP는 자신이 암호화되는지 몰라도 되고, 평소처럼 메시지를 만들어 아래로 내려보내기만 하면 된다. 보안 계층이 그 메시지를 암호화해 전송 계층으로 넘기고, 반대편에서 복호화해 다시 HTTP로 올린다. 계층이 분리되어 있어 HTTP는 보안의 세부에 관여하지 않는다.


암호화는 도청과 변조를 함께 막는다. 내용이 암호화되어 있으니 도청자는 읽을 수 없고, 메시지에 무결성을 검증하는 장치가 함께 실리므로 중간에 내용이 바뀌면 받는 쪽이 그것을 알아챈다. 비밀을 지키는 일과 내용이 온전한지 확인하는 일이 하나의 보안 계층 안에서 함께 처리된다. 이 둘이 갖추어져야 통신이 안전하다고 말할 수 있다.


남은 것은 상대가 진짜인지를 확인하는 문제다. 통로를 암호화해도 그 반대편이 가짜라면 안전하다고 할 수 없다. 그래서 보안 연결에는 상대의 정체를 증명하는 절차가 함께 들어간다. 이 증명을 맡는 것이 인증서다.

신뢰를 증명하는 인증서

인증서는 이 서버가 자신이 주장하는 그 서버가 맞다는 것을 증명하는 문서다. 서버는 접속한 클라이언트에게 자신의 인증서를 제시하고, 클라이언트는 그것을 검증해 상대가 진짜인지 판단한다. 이 검증이 통과해야 클라이언트는 안심하고 통신을 이어 간다. 인증서는 위장을 막는 장치다.


인증서를 신뢰할 수 있는 이유는 그것이 믿을 만한 제삼자의 보증을 담고 있기 때문이다. 서버가 스스로 자신이 진짜라고 주장하는 것만으로는 부족하다. 대신 널리 신뢰받는 인증 기관이 그 서버의 신원을 확인하고 보증을 서명해 준다. 클라이언트는 그 기관을 신뢰하므로, 그 기관이 보증한 서버도 신뢰한다.


클라이언트는 신뢰하는 인증 기관의 목록을 미리 갖고 있다. 서버가 제시한 인증서가 그 목록에 있는 기관의 보증을 받았는지, 인증서가 만료되지 않았는지, 접속한 주소와 인증서에 적힌 대상이 일치하는지를 차례로 확인한다. 이 중 하나라도 어긋나면 경고를 띄우고 연결을 막는다. 브라우저가 이따금 보여 주는 보안 경고가 이 검증의 실패다.


인증서 검증이 실패하는 원인은 대개 몇 가지로 좁혀진다. 인증서의 유효 기간이 지났거나, 주소와 인증서의 대상이 맞지 않거나, 보증한 기관이 신뢰 목록에 없는 경우다. 보안 연결이 갑자기 막힐 때는 이 세 가지를 먼저 확인하면 원인이 빠르게 드러난다. 인증서의 만료를 놓쳐 서비스가 멈추는 일은 실무에서 드물지 않은 사고다.

연결을 여는 절차

보안 연결은 실제 메시지를 주고받기 전에 준비 과정을 거친다. 클라이언트와 서버가 서로 인사를 나누고, 인증서를 검증하고, 앞으로 쓸 암호화 방식과 열쇠를 합의하는 절차다. 이 준비가 끝나야 비로소 암호화된 통로가 열리고 HTTP 메시지가 오가기 시작한다. 이 절차를 핸드셰이크라 부른다.


이 준비 과정은 공짜가 아니다. 실제 데이터를 주고받기 전에 여러 번의 왕복이 오가므로, 그만큼 시간이 든다. 서버가 가까이 있으면 이 지연이 눈에 띄지 않지만, 멀리 있으면 한 번의 왕복마다 지연이 쌓여 첫 응답이 느려진다. 보안이 성능에 얼마간 비용을 지우는 지점이 여기다.


그래서 한 번 맺은 보안 연결은 되도록 재사용한다. 매 요청마다 준비 과정을 새로 하면 비용이 크므로, 이미 열어 둔 통로로 여러 요청을 잇달아 보낸다. 앞서 다룬 연결의 재사용이 보안 연결에서는 더욱 중요해진다. 규약의 새 버전들이 이 준비 비용을 줄이는 데 공을 들이는 것도 그 때문이다.


보안 연결은 이제 예외가 아니라 기본이 되었다. 과거에는 로그인처럼 민감한 구간에만 보안 연결을 쓰기도 했지만, 지금은 모든 통신을 암호화하는 것이 표준이다. 검색 노출이나 최신 규약의 사용에서도 보안 연결이 전제되는 경우가 많다. 평문 통신은 이제 특별한 이유가 없는 한 선택지에서 빠진다.

다중화로 걷어낸 병목

이제 통로의 안전에서 통로의 속도로 넘어간다. 오랫동안 웹을 떠받친 규약의 버전은 하나의 연결로 요청을 하나씩 순서대로 처리했다. 앞선 요청의 응답이 와야 다음 요청을 보낼 수 있으니, 요청이 많으면 줄을 서서 기다리는 병목이 생겼다. 이를 회피하려고 연결을 여러 개 여는 편법이 쓰였지만 근본 해결은 아니었다.


이 병목을 걷어낸 것이 다중화다. 하나의 연결 안에서 여러 요청과 응답을 동시에 실어 나르는 방식이다. 요청들이 줄을 서지 않고 하나의 통로 위를 섞여 흐르다가, 받는 쪽에서 다시 제자리로 조립된다. 연결을 여러 개 열지 않고도 여러 요청을 병렬로 처리할 수 있게 된 것이다.


이를 가능하게 하려고 메시지의 표현이 바뀌었다. 사람이 읽던 텍스트 형태 대신, 기계가 다루기 좋은 이진 형태로 메시지를 잘게 나누어 전송한다. 잘게 나뉜 조각에 어느 요청의 것인지 표시가 붙어 있어, 섞여 와도 제대로 조립된다. 개발자가 다루는 논리적 구조는 그대로지만, 그 아래에서 전송되는 형태가 달라진 것이다.


헤더를 압축한 것도 이 버전의 개선이다. 요청마다 비슷한 헤더가 반복해서 실려 가는 낭비가 있었는데, 반복되는 헤더를 압축해 그 낭비를 줄였다. 요청이 많을수록 이 절감의 효과가 커진다. 다중화와 헤더 압축이 맞물려, 같은 통로로 더 많은 요청을 더 빠르게 처리하게 되었다.


이 개선의 이점을 온전히 누리려면 연결을 잘게 쪼개지 않아야 한다. 여러 요청을 하나의 연결에 몰아 넣어야 다중화가 힘을 발휘하는데, 자원을 여러 곳에 흩어 두고 각기 다른 연결로 가져오면 그 이점이 흩어진다. 과거에 연결을 여러 개 열어 병목을 피하던 편법이 이제는 오히려 손해가 되는 것이다. 규약이 바뀌면 그에 맞춰 자원을 두는 방식도 함께 손보아야 개선이 실제 성능으로 이어진다.

전송 계층을 바꾼 세 번째 판

다중화가 응용 계층의 병목을 걷어냈지만, 그 아래 전송 계층에는 또 다른 병목이 남아 있었다. 여러 요청이 하나의 전송 연결을 공유하는데, 그 연결의 어느 한 조각이 중간에 유실되면 그 뒤의 모든 조각이 함께 기다려야 했다. 응용 계층에서는 요청들을 분리했지만 전송 계층에서 다시 묶여 버린 것이다.


세 번째 버전은 이 문제를 풀려고 전송 계층 자체를 교체했다. 기존의 신뢰성 있는 전송 방식 대신, 더 가볍고 유연한 새 전송 방식 위에서 동작한다. 이 새 방식에서는 각 요청의 흐름이 서로 독립적이어서, 한 흐름의 조각이 유실되어도 다른 흐름은 영향을 받지 않는다. 전송 계층에 남아 있던 마지막 병목을 걷어낸 것이다.


연결을 여는 비용도 줄였다. 기존에는 전송 연결을 맺는 과정과 보안 연결을 맺는 과정이 따로 이루어져 왕복이 겹쳤는데, 새 방식은 이 둘을 하나로 합쳐 준비에 드는 왕복을 줄였다. 앞서 보안 연결의 준비 비용이 크다고 했는데, 그 비용을 규약 차원에서 깎아 낸 셈이다. 첫 응답까지 걸리는 시간이 그만큼 짧아진다.


이 새 방식은 연결이 특정 경로에 묶이지 않는 성질도 갖는다. 기존 방식에서는 네트워크 환경이 바뀌면 연결이 끊겨 다시 맺어야 했지만, 새 방식은 환경이 바뀌어도 같은 연결을 이어 갈 수 있다. 이동하면서 네트워크가 전환되는 상황에서 이 성질이 끊김을 줄인다. 전송 계층을 통째로 바꾼 대가로 얻은 이점들이다.

무엇이 바뀌고 무엇이 남는가

규약의 버전이 세 세대에 걸쳐 바뀌는 동안 아래에서 데이터를 나르는 방식은 크게 달라졌다. 텍스트에서 이진으로, 순차 처리에서 다중화로, 기존 전송에서 새 전송으로 바닥이 계속 교체되었다. 이 모든 변화의 목적은 같은 메시지를 더 빠르고 안전하게 나르는 것이었다.


그런데도 개발자가 다루는 개념은 거의 그대로 남았다. 요청과 응답, 메서드와 상태 코드, 헤더와 본문이라는 논리적 구조는 버전이 올라가도 유지된다. 첫 편에서 이 점을 짚었는데, 이번 편에서 그 이유가 분명해진다. 바뀐 것은 전부 통로에 관한 것이고, 통로를 지나는 메시지의 형식은 그대로였기 때문이다.


이 분리가 규약이 진화할 수 있었던 비결이다. 응용 계층과 전송 계층의 책임이 나뉘어 있으니, 아래 계층을 통째로 교체해도 위 계층의 개념은 흔들리지 않았다. 개발자가 한 번 익힌 지식이 버전이 바뀌어도 낡지 않는 것은 이 계층 분리 덕분이다. 밑바닥이 바뀌어도 그 위의 약속은 유지된다.


실무에서는 이 버전들을 직접 지정할 일이 드물다. 서버와 배포망이 클라이언트와 협상해 쓸 수 있는 최선의 버전을 골라 주기 때문이다. 다만 성능을 파고들 때는 실제로 어떤 버전과 어떤 보안 방식으로 연결되어 있는지 확인하는 습관이 필요하다. 기대한 버전보다 낮게 붙어 있어 성능을 잃는 경우가 종종 있다.


이번 편은 평문 전송의 위험, 암호화된 통로와 인증서, 연결을 여는 절차, 그리고 규약이 두 세대에 걸쳐 성능을 어떻게 끌어올렸는지를 살펴보았다. 여기까지 규약의 구조와 원리, 설계와 전송을 두루 다루었다. 마지막 편에서는 이렇게 설계한 시스템을 다른 사람이 쓸 수 있도록 정리하고 검증하는 문서화와 테스트를 다루며 시리즈를 맺는다.