웹 서버는 Stateless 프로토콜인 HTTP 를 사용하기 때문에 웹사이트에서 인증을 관리하기 위한 방안이 필요하다.

로그인을 한 유저들에 대해 권한이 필요한 매 요청마다 재로그인을 시킬수는 없는 일이다.

그렇기 때문에 웹사이트는 일반적으로 유저의 접속 정보를 관리하기 위한 몇가지 방안을 사용한다.

 

1. Session 기반 인증

세션 기반인증을 위해 Session 과 Cookie 가 사용된다. 다음 Flow 로 인증 절차가 진행된다.

 

 - 유저가 로그인을 하고 세션이 서버 메모리 상에 저장된다. 이 때 세션을 식별하기 위한 Session Id 를 기준으로 정보를 저장한다.

 - 브라우저에 쿠키로 Session Id 가 저장된다.

 - 쿠키에 정보가 담겨있기 때문에 브라우저는 해당 사이트에 대한 모든 Request 에 Session Id 를 쿠키에 담아 전송한다.

 - 서버는 클라이언트가 보낸 Session Id 와 서버 메모리로 관리하고 있는 Session Id 를 비교하여 Verification 을 수행한다.

 

Session 기반 인증은 다음과 같은 장단점을 갖는다.

 

+ 세션 기반 인증 방식은 구현이 상당히 명확하다는 장점이 있다. 또한 실제 서버에서 로그인 상태 확인이 굉장히 유용하다.

+ 상대적으로 안전하다. 서버측에서 관리하기 때문에 클라이언트 변조에 영향받거나 데이터의 Stale (손상) 우려가 없다.

- 유저들의 세션에 대한 정보를 서버 메모리에 들고 있게 된다는 부담이 있다.

- 서버 메모리에 세션 정보가 저장되기 때문에 Scale Out / Scale In 이 부담이 될 수 있으며, 결국에는 유저 상태에 무관하게 동작할 수 있도록 Data-Driven 아키텍처가 요구된다.

- 멀티 디바이스 환경에서 로그인 시 신경써줘야할 부분들이 생긴다. 

 

2. Token 기반 인증

Token 기반 인증의 방법으로 많은 웹 서버들은 JWT(JSON Web Token) 을 사용한다.

Token 기반 인증 방식과 Session 기반 인증 방식의 가장 큰 차이점은 유저의 정보가 서버에 저장되지 않는다는 점이다.

Flow 는 다음과 같다.

 

 - 유저가 로그인을 하고 서버에 세션을 이용해서 정보를 기록하는대신, Token 을 발급한다.

 - 클라이언트는 발급된 Token 을 저장한다 (일반적으로 local storage 에 저장한다.)

 - 클라이언트는 요청 시 저장된 Token 을 Header 에 포함시켜 보낸다. 

 - 서버는 매 요청시 클라이언트로부터 전달받은 Header 의 Token 정보를 Verification 한 뒤, 해당 유저에 권한을 인가한다.

 

Flow 에서 차이를 확인할 수 있듯, Session 기반 서버가 서버측에 정보를 기록하는 반면, Token 기반 인증은 Token 에 대한 Verification 만 수행할 뿐 저장은 클라이언트에서 수행한다.

Token 기반 인증은 다음과 같은 장단점을 갖는다.

 

+ 클라이언트에 저장되기 때문에 서버의 메모리에 부담이 되지않으며 Scale 에 있어 대비책을 고려할 필요가 없다

+ 멀티 디바이스 환경에 대한 부담이 없다.

- 상대적으로 Stale (손상) 의 위험이 크다.

- 결국 구현을 하다보면 서버측에 token blacklist 를 관리하게 될 가능성이 있고 그렇게 되면 서버측 메모리의 소모가 발생하게 된다

- Token 은 일반적으로 Session ID 보다 길다.

- XSS 공격에 취약할 수 있어 가능한 민감한 정보는 포함시키지 않을 필요가 있다.

 

최근에는 Scaling 이슈와 멀티 디바이스 이슈로 Token 방식이 좀 더 핫한 느낌이지만, Session 방식도 여전히 많이 쓰인다.

두 방식 모두 장단점이 있기 때문에 적합한 구조를 선택하는 것이 좋겠다.

 

분산처리 환경을 구축하는 데 있어서 로드밸런서의 역할은 핵심적이다.

클라우드 환경에서 분산처리를 위한 아키텍처를 설계한다면, AWS 의 Load Balancer 를 이용해볼 수 있다.

 

Amazon Web Service 가 Provisioning 하는 Load Balancer 는 ELB(Elastic Load Banacing) 서비스라 하며, 기본적으로 Logging, Cloud Watch 를 통한 지표, 장애 복구, Health Check 와 같은 기능들을 제공한다.

ELB 서비스의 종류로는 2019년 8월 기준으로 3가지 종류가 있다.

 

 (1) Classic Load Balancer

가장 기본적인 형태이자 초기에 프로비저닝되던 서비스로, 포스팅 등에 나오는 설명에 단순히 ELB 라고 나와있으면 Classic Load Balancer 를 말하는 경우가 많다.

L4 계층부터 L7 계층 까지 포괄적인 로드밸런싱이 가능하며, 따라서 TCP, SSL, HTTP, HTTPS 등 다양한 형태의 프로토콜을 수용할 수 있고 Sticky Session 등의 기능도 제공해준다.

특징적으로 Classic Load Balancer 는 Health Check 를 할 때, HTTP 의 경우 /index.html 경로를 참조한다.

즉, 웹서버 인스턴스의 경로에 /index.html 가 포함되어있지 않다면(404) Health Check 를 실패한다는 뜻이다...

따라서 이럴 때에는 Health Check 를 위한 프로토콜만 TCP 로 바꾸고 포트만 80 으로 체크하게 하는 방법이 있다.

중요한 특징으로... CLB 는 로드밸런서가 url 하나를 가질 수 있다. 즉, CLB 로 매핑되어있는 인스턴스들은 무조건 하나의 URL 을 통해 접근하게 된다.

 

 (2) Application Load Balancer

Classic Load Balancer 이후 출시된 서비스로 HTTP 및 HTTPS 트래픽 로드밸런싱에 최적화되어있다.

L7 계층에서 작동하며 Micro Service Architecture 와 같은 Modern 환경을 겨냥한 많은 강점들이 있는데, WebSocket 이나 HTTP/1.1 이상의 프로토콜에 대한 지원, 향상된 라우팅 정책과 사용자의 인증 까지도 지원을 해준다.

웹서비스를 목적으로 한다면 기존 Classic Load Balancer 가 갖고 있던 장점 이상의 강점들을 많이 포함하고 있다.

단 L7 계층에서 작동하기 때문에 TCP 등에 대한 밸런싱은 지원되지 않는다.

직접 사용해서 구축 할때에 Classic Load Balancer 에 비해 좀 더 구축이 편하고 웹서버 밸런싱에 있어서는 확실히 다양한 옵션이 있다. Health Check 경로 또한 지정할 수 있으며, 기본이 / 로 정해져있다.

CLB와 다르게 ALB 는 host-based Routing 과 path-based Routing 을 지원한다. Content Based Routing 이라고도 하며 ALB 에 연결된 인스턴스들은 여러개의 URL 과 path 를 가질 수 있다.

 

 (3) Network Load Balancer

Load Balancer 중 성능적으로 최적의 로드밸런싱을 지원한다. TCP, UDP 등의 서버를 구축할 때 해당 프로토콜 들에 대해 굉장히 낮은 지연 시간으로 최적의 성능을 보여준다고 한다. 

사용해본적이 없는데... 사용해본다면 좀 더 포스팅 내용을 보강해야겠다.

 

 

로드밸런서에 EIP 를 직접 붙일 수는 없고, 필요하다면 DNS 의 도메인 네임 대신 CNAME 을 사용하거나 Route53 서비스와 같이 사용해야 한다. 그 이유는... ELB 역시 서버이기 때문에, 부하 상황에서 오동작의 위험이 있고 그에 따라 자체적으로 Scale In - Scale Out 을 수행하기 때문이다. (즉, IP 를 지정할 서버가 한대가 아니게 될 수 있다.)

또한, ELB 는 Multi-AZ 환경에서 "내부적으로는" 각 Availability Zone 별로 구성된다. 이 때 트래픽의 분배는 AZ 당 ELB 사정에 맞게 분배되므로... Multi AZ 환경에서는 각 Availability Zone 별 인스턴스의 숫자를 동일하게 맞춰주는 것이 좋다. (동일하게 맞추지 않으면 특정 Zone 의 인스턴스들에 트래픽이 몰릴 수 있다.)

 

로드밸런서를 통해 Cloud 환경에서 아키텍처를 설계할 때에는 Private Subnet 들에 EC2 인스턴스들을 배치해둔 상태에서, ELB로 트래픽을 연결할 수 있도록 인스턴스들을 연결시켜주고, Public 영역에 위치시켜 놓는 식으로 구성을 한다.

 

혹은, ELB 자체는 VPC 내부에 형성시켜 두고, ELB로 들어온 트래픽을 Private Subnet 으로 전개시켜주며, 인터넷에 연결을 위한 Public NAT 를 두고 출력을 해당 NAT 를 거치게 하는 방식도 있다.

 


HTTP 와 HTTPS 의 차이점에 대해 면접 볼 때 질문 받은 적이 있었다.
당시 많이 긴장하고 해당 면접에 대해 Deep 한 질문들이 나오던 와중에 받은 질문이어서 보안을 위한 HTTP 라는 정도로 얼버무린 기억이 있는데,
만약 다음에 질문받게되면 깔끔하고 완벽하게 답변할 수 있도록 정리해보았다.


HTTP 란 일반적으로 웹 서버 통신을 위한 프로토콜이다.
HTTPS 란 정확히 어떤 것일까?
간략하게 정리하자면, HTTPS 란 "암호화된 통신을 제공하는 HTTP" 를 일컫는다.

HTTP 를 이용해 클라이언트와 서버가 통신을 할때, 암호화 통신을 위한 키를 설정하고 통신을 하게 된다.

이 때 사용되는 암호화 방식은 공개키 암호화방식을 사용하며, 데이터를 암호화하는데 2개의 키를, 복호화하는데 한개의 키를 사용한다.

HTTP 프로토콜을 사용하면 공격자가 패킷을 가로챌 경우, 평문이기 때문에 해당 데이터를 갈취하고, 변조해서 공격이 가능하다. (Man in the middle)
이에 반해 HTTPS 프로토콜을 사용하면, 패킷이 중간에 탈취되더라도 공격자가 메시지를 알아내고 암호화까지 하여 변조하는데, 일반적으로 천문학적인 시간을 소모하게 된다.

이처럼 암호화된 통신을 함으로써 안전한 구조를 가져갈 수 있지만, 공개키 암호화와 복호화 과정은 많은 비용을 수반한다.
따라서 HTTPS 통신은 HTTP 에 비해 느릴 수밖에 없으며, 보통 선택적으로 사용하게 된다.

예를들어 금융정보 및 기밀 또는 민감한 개인정보들의 경우에는 HTTPS 로, 그와 상관없는 UI 처리 및 일반 컨텐츠 관련 정보는 HTTP 로 처리하는게 정석적이다.



COMET 이란 2006년 알렉스 러셋(Alex Russel)이 정의한 용어로, 브라우저가 HTTP 요청에 대해 데이터를 푸시하는 방법을 고안한 웹 모델이다.


일반적인 웹모델은 널리 알려진 바 대로 서버와 클라이언트 브라우저간 상태를 유지한 통신이 불가능하다. 


COMET 은 이러한 HTTP 의 본질적 한계를 어느정도 극복하고자 만든 Web Application Model 이라고 생각하면 된다.


가령 브라우저가 지속적으로 서버에 데이터를 받아야할 필요가 있을 때 서버의 데이터가 업데이트 되지않았다면 이는 무의미하다.

즉, 클라이언트의 입장에서 갱신이 필요한 절대적인 시점은 무조건 서버의 데이터가 변경되었을 때가 된다.


COMET 은 간단하게는 Client 로 유의미한 메시지를 전달할 때까지 HTTP 응답을 지연시키는 기술이다.


좀 더 정확히는 서버가 클라이언트의 요청에 응답할 때 응답을 "늘어뜨리는 방법" 을 이용해서 긴 시간동안 브라우저가 접속을 끊지않고 서버의 응답을 대기하게 만든다. 


즉, Long polling 과 원리가 같다.

(Long Poll – 서버측에서 단순히 클라이언트 측에 대한 연결을 길게 유지함으로써 지정한 기간 내에 정보가 있으면 응답을 전달한다. 메시지 양이 많으면 Polling 과 차이가 없으며 지정한 시간이 초과되어 끊기면 다시 요청하는 말그대로 긴 – Polling 이다.)


클라이언트는 Long polling 을 해주고, 그 시간 동안 서버는 서버에서 발생한 이벤트 정보 등도 같이 끼워넣어줌으로써 이른바 "HTTP 푸시" 를 흉내내게끔 한다.


COMET 모델에서 서버는 비동기 방식을 취하고, 클라이언트가 다양한 테크닉으로 푸시를 받을 수 있는 구조를 취하는 경우가 일반적이다.


이에 Javascript 레벨에서 사용되는 테크닉으로, Hidden Iframe 영역(Forever frame)을 할당하고 서버측에서 발생하는 이벤트별로 <script> 태그 내에 데이터를 채워넣는 방식 또는 Ajax 의 long polling 방식, 그리고 Active X 를 이용하는 방식을 사용하기도 한다.


COMET 모델에서 서버와 클라이언트가 통신하기 위해 JSONP 라는 방식을 사용하는데, 이는 JSON 규격의 데이터로 통신을 하되 Client 로부터 전달받은 Callback 함수를 호출하게끔 하는 방식이다.


이렇게 되면 브라우저는 Callback 함수로 감싸여진 Json 데이터를 이용해서 <script> 또는 XHR 을 구성하며 해당 리소스를 읽고 수행하게 된다.


이처럼 COMET 모델을 이용하면 서버에서 발생한 이벤트를 Client 의 추가 요청없이 송신할 수 있다. 



현재는 Websocket 및 HTML5에서 표준화된 Server-sent Event 와 연관되어 발전해가고 있다.






HTTP/1.1 은 변해가는 웹서비스 환경에서 발생하는 상당한 규모의 문제들을 처리하는데 훌륭한 방법을 제시해주었고, 


그 결과 성능적으로나 서비스적으로나 변해가는 웹생태계에 걸맞는 진화를 보여주었다.


하지만 그럼에도 불구하고 웹의 사용자는 계속해서 웹서비스에게 많은 리소스를 요구하고, 많은 리소스로 클라이언트를 충족해줘야만 살아남을 수 있게 바뀌어 가고 있다.


실제로 웹 초창기에 비해 평균 다운로드하는 리소스의 용량은 60배 이상 늘었으며, 이 추세는 계속될 것으로 보인다.


이에 HTTP/2 는 HTTP/1.1 에서 다소 불안정하던 부분을 해소하고 웹어플리케이션을 더 빠르고 효율적으로 만들어주는데 초점을 두었다.


HTTP/2 의 근본을 이해하기 위해서는 Google 이 2000년대에 진행했던 프로젝트인 SPDY 를 이해해야 한다.


SPDY(스피디) 란 Google이 개발한 비표준 네트워크 프로토콜로 패킷 압축, Multiplexing 을 기반으로 인터넷에서의 Latency 를 줄이기 위해 고안된 프로토콜로, 초창기 크롬 브라우저에 탑재되어 높은 로딩 속도를 자랑하게 했던 구글의 자체 프로토콜이다.


HTTP/2 는 바로 이 SPDY 에 기반을 둔 HTTP 프로토콜 Layer 하위의 TCP 통신 레이어에 새로운 Binary 계층을 도입하여 HTTP 의 기반이 되는 TCP 연결의 호율성을 추구하였다.


HTTP/2 의 특징들은 다음과 같다.


(1) Binary Framework


 HTTP/2 는 TCP 계층과의 사이에 새로운 Binary Framework 를 통해 네트워크 스택을 구성한다.

기존에 텍스트 기반으로 Header 와 Data 가 연결되고 있던 1.1 이하 버전과 다르게 HTTP/2 는 전송에 필요한 메시지들을 Binary 단위로 구성하며 필요 정보를 더 작은 프레임으로 쪼개서 관리한다. 


여기서 데이터를 Binary Encoding 방식으로 관리하는데, 인코딩된 데이터를 다루기 위해서는 반드시 Decoding 과정이 필요하게 된다.


즉, 이말은 HTTP/1.1 버전의 클라이언트는 HTTP/2 버전의 서버와 통신이 불가능하다는 뜻이다.

그래도 걱정할 필요는 없다. 해당 메커니즘은 이미 도입이 되었고 충분히 지원하는 상황이기 때문에 이슈가 생기지 않는다면 추가로 신경쓸 필요는 없다.



(2) Packet Capsulation


 HTTP/2 의 패킷들은 더 작은 단위로 Capsulation 된다. 여기서 Frame 과 Message, Stream 이라는 개념이 도입된다.


 - Frame : HTTP/2 의 통신 최소단위로 모든 패킷에는 하나의 Frame Header 가 포함된다.

 - Message : Frame 의 시퀀스 데이터를 말한다.

 - Stream : 연결의 흐름을 의미한다. 


HTTP/2 의 모든 연결은 TCP 기반의 Stream 이며 양방향으로 Frame Header 를 지닌 Message 들을 통신한다.

데이터는 위에서 언급한 바 대로 Binary 인코딩된 데이터들이며 Multiplexing 과 성능 최적화 알고리즘들이 적용된다.



(3) Multiplexing 개선


<출처 : https://medium.com/@factoryhr/http-2-the-difference-between-http-1-1-benefits-and-how-to-use-it-38094fa0e95b>


 

 HTTP/1.1 에도 멀티플렉싱을 지원하기 위한 노력은 있었지만, HTTP/1.1 은 이 부분에서 다소 한계점을 갖고 있었다. 

(참고 : http://jins-dev.tistory.com/entry/HTTP11-%EC%9D%98-HTTP-Pipelining-%EA%B3%BC-Persistent-Connection-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC)


요청과 응답의 동시처리는 이루어지나 결국 응답처리를 지연시키는 블로킹 방식이었기 때문에, 한 개의 Connection 이 하나의 Request / Response 를 처리하는 한계를 극복하기는 어려웠으며 그로 인해 결국 HOL(Head Of Line) 문제 발생과 연결에 있어서 비효율성을 갖는다는 아쉬운 부분이 있었다.


HTTP/2 는 이부분에서 개선된 멀티플렉싱을 지원하며, Connection 하나에서 다수의 입출력이 가능하게끔 지원한다.


이것이 가능한 이유는 HTTP/2는 패킷을 Frame 단위로 세분화하여 순서에 상관없이 받는쪽에서 조립하도록 설계하였기 때문이다. 


그렇기 때문에 HTTP/2 에서는 각 요청과 응답을 병렬로 전달할 수 있으며 하나의 Connection 에서도 여러 응답 / 요청을 처리할 수 있게 되었고 HTTP/1.1 에서 사용하던 임시방편을 사용할 필요가 없어졌다.



(4) Header Compression


 HTTP/2 의 Header 필드는 Huffman code 로 인코딩되며 이에 따라 텍스트적인 압축이 수반된다. 여기서 패킷을 더 최적화해 만들어진 HPACK 알고리즘은 Header 의 크기를 80% 이상 압축해서 전송한다고 알려져있다.



(5) Server Push


 HTTP/2 에서 서버는 단일 클라이언트의 요청에 대해 추가적인 응답을 내려줄 수 있다.

HTTP에서 Push 문제는 쉽지않은 이슈이지만, HTTP/2 에서는 이를 PUSH_PROMISE 라는 Frame 을 이용해 제공한다.

이렇게 제공되는 Push 리소스는 캐싱되거나 재사용 또한 가능해서 유용하게 사용이 가능할 것으로 보인다.



그 외에도 보안적인 측면이나 세세한 부분에서 HTTP/2 는 개선을 위한 노력들이 많이 보이며 확실히 효율적인 프로토콜로 자리매김한 것으로 보인다.


일각에서는 여전히 쿠키 보안에 취약하며 성능에 있어서의 향상 수준이 호환성을 저해할 만큼 대단한 것인가에 대한 의문도 있는 모양이지만, 개인적으로는 상당히 훌륭하고 배울 부분이 많은 프로토콜로 생각된다.




 

 

HTTP/1.0 은 가장 기초적인 형태의 웹 프로토콜을 제시하였고, 이는 일전 포스팅에도 정리되어 있듯이 TCP 프로토콜 위에 HTTP Spec 을 이용한 HTTP 프로토콜을 충실히 따른다.


이 방식은 웹이라는 새로운 환경에서 TCP에 비해 나은 성질의 프로토콜이었으나, 웹의 규모가 거대해지면서 몇가지 이슈들이 발생하였다.

 

특히 Network Latency 이슈가 주된 문제였는데, HTTP/1.1 은 이 문제를 해결하기 위해 특징적인 2가지 개념을 도입한다.

 

(1) HTTP Pipelining (HTTP 파이프라이닝)

 

 

 

HTTP Pipelining 이란 HTTP1.1 로 스펙이 업그레이드 되면서 클라이언트와 서버간 요청과 응답의 효율성을 개선하기 위해 만들어진 개념이다.

 

HTTP Request 들은 연속적으로 발생하며, 순차적으로 동작한다.


HTTP/1.0 에서 HTTP Request 는 소켓에 write 한뒤, 서버의 Response 를 받아 다음 Request 를 보내는 방식으로 웹이 동작한다. 

여러 요청에 대해 여러 응답을 받고, 각 처리가 대기되는 것은 Network Latency 에 있어서 큰 비용을 요구한다.

게다게 HTTP/1.0 에서 HTTP 요청들은 연결의 맺고 끊음을 반복하기 때문에 서버 리소스 적으로도 비용을 요구한다.


HTTP/1.1 에서는 다수의 HTTP Request 들이 각각의 서버 소켓에 Write 된 후, Browser 는 각 Request 들에 대한 Response 들을 순차적으로 기다리는 문제를 해결하기 위해 여러 요청들에 대한 응답 처리를 뒤로 미루는 방법을 사용한다.

 

즉, HTTP/1.1 에서 클라이언트는 각 요청에 대한 응답을 기다리지 않고, 여러개의 HTTP Request 를 하나의 TCP/IP Packet 으로 연속적으로 Packing 해서 요청을 보낸다.


파이프라이닝이 적용되면, 하나의 Connection 으로 다수의 Request 와 Response 를 처리할 수 있게끔 Network Latency 를 줄일 수 있다.


하지만 위의 기법 설명에서 언급하듯이, 결국 완전한 멀티플렉싱이 아닌 응답처리를 미루는 방식이므로 각 응답의 처리는 순차적으로 처리되며, 결국 후순위의 응답은 지연될 수 밖에 없다.


<이미지 출처 : https://www.popit.kr/%EB%82%98%EB%A7%8C-%EB%AA%A8%EB%A5%B4%EA%B3%A0-%EC%9E%88%EB%8D%98-http2/>



이는 HTTP의 Head Of Line Blocking 이라 부르며 Pipelining 의 큰 문제점이다.


따라서 HTTP/1.1 의 웹서버는 서버측에 Multiplexing 을 통한 요청의 처리를 요구하며, 각 Connection 당 요청과 응답의 순서가 보장되는 통신을 한다.

 

HTTP 파이프라이닝은 HTTP/2 가 등장하면서 멀티플렉싱 알고리즘으로 대체되었고, 모던 브라우저들에서도 기본적으로는 활성화하지 않고 있다.

 

 

(2) Persistent Connection

 

 

HTTP 1.0 초기의 HTTP 연결은 요청시 TCP 의 3-way handshake 방식으로 연결이 이루어졌었다.

즉, 웹클라이언트와 서버간의 연결 성립 이후 SYN, SYN-ACK, ACK 핸드셰이킹이 발생하고 이를 바탕으로 통신을 구성한 뒤 연결을 끊는 순서가 필요했다..

웹의 초창기에는 컨텐츠의 수가 많지 않았기 때문에 이런 TCP 연결은 부담되지 않았지만, 웹을 통한 멀티미디어 컨텐츠의 발달로 인해, TCP Connection 의 재사용이 요구되게 되었다.

 

웹에서의 커넥션 재사용을 Keep-alive 또는 Connection reuse 라 하며, HTTP/1.0 에서는 클라이언트가 서버에게 요청하는 Request Header에 다음과 같은 값을 통해 연결을 유지하였다.

 

Connection: keep-alive


HTTP/1.1 에서는 이 헤더를 사용하지 않더라도 모든 요청/응답이 Connection을 재사용하도록 설계되어 있으며, 필요없는 경우에만 TCP 연결을 종료하는 방식으로 변경되었다.

 

Connection: close

 

해제 시에 위와 같은 명시적 Connection 헤더를 이용하며, 그렇지 않은 경우 무조건 연결은 재사용되게 되어 있다.

 

이러한 이슈는 HTTP/2 로 웹이 한단계 더 발전하면서 해결되었다.

자세한 건 다음 링크를 참조

(https://jins-dev.tistory.com/entry/HTTP2-%ED%8A%B9%EC%A7%95%EB%93%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC)

 


현재 웹 표준으로는 HTTP/2 가 개발중이며 이미 공개가 되어있다. HTTP/2 에서는 구글이 프로토타입으로 제시한 SPDY 등의 프로토콜을 기반으로 한 웹 기술의 발전이 예상되는 만큼 위의 기술들 또한 보다 더 나은 방향으로 진화될 것이라 생각한다.


 HTTP 는 TCP 기술 스택 위에 놓인 Spec 기반의 프로토콜이며, Http Spec documentation 에서 정의한 스팩 대로 프로토콜이 인코딩, 디코딩 된다. 

그 중에서도 면접 단골 질문이자 자주 헷갈리는 부분은 GET 과 POST 간의 차이점이다. 또 많은 엔지니어들이 오해하고 있는 부분이기도 하다.


 HTTP Specification 에 따르면 GET과 POST 에는 몇가지 명확한 차이점이 있다.

먼저 GET 메서드는 idempotent하다. 이 말은, 동일한 작업을 어떠한 부작용 없이 여러 번 계속할 수 있다는 것이며 이 때 동일 요청은 동일한 응답을 가져야 한다는 의미는 아니다.


 HTTP 1.1 스펙에 따르면 GET, HEAD, PUT 메서드가 idempotent 하다.

반면에 POST로 전송되는 BODY는 되돌릴 수 있는 성질의 것이 아니다. 


 HTTP GET 메서드는 key/value 형태의 Query Parameter 가 URL에 담겨 전송되며, 이와 반대로 POST는 request body 에 담겨서 전송된다.

가령 같은 파라미터를 전달하는 요청에 대해 프로토콜은 다음과 같이 메시지를 전송한다.


GET > /url?param1=value1&param2=value2


POST> POST /url HTTP/1.1

      HOST: server

      … (기타 headers)

Content-Type: ~


//주로 JSON 형태로 주고받으므로 JSON 을 예시로 표시

    {

"Param1": value1,

"Param2": value2

    }



 보안에 있어서 GET보다 POST가 뛰어나다고 할 수는 없다. 두 메서드는 보안에 크게 차이가 없으며 보안은 사실상 SSL이 Security Layer에서 전담한다고 봐도 무방하다. 

굳이 따지자면, url 과 parameter 에 전달값이 그대로 노출되는 GET 방식보다, Request Body 에 숨겨져서 노출되지않는 POST 방식이 조금 더 보안에 안전하다고 할 수 있다.


또한 GET 은 담을 수 있는 전송데이터의 길이 상의 제약이 있다. url + parameter 를 전부 포함해서 255자(HTTP/1), 2048자(HTTP/1.1 ~) 로 제한된다. 반면 POST 는 전송 데이터의 길이상 제약이 없다.


 GET 방식은 Browser가 일반적으로 url에 요청할 때 사용하는 방식이다.

가령 Browser Cache 는 기본적으로 GET 요청과 응답에 대해 캐시 내에 저장하는 로직을 지닌다. 반면 POST 와 같은 요청은 브라우저 레벨에서 캐싱하지 않는다. 그 외에 Crawler Bot 등도 웹문서의 수집을 위해 HTTP GET 을 바탕으로 동작한다.

이 때문에, 예기치못한 무분별한 서버로직의 변경을 막을 수 있다는 측면에서 서버 사이드를 수정하는 동작은 GET이 아닌 POST 를 권장한다. 


 RequestBody에 대해 Http Specification 에서는 별 언급이 없지만 GET에도 RequestBody가 수반될 수 있다. 많은 엔지니어들조차 오해하는 부분인데, 원칙적으로 Request Body를 갖지 못할 이유가 없다. 이유는 HTTP 자체도 결국 TCP 위에서 설계된 텍스트 기반 프로토콜이기 때문이다. (HTTP 프로토콜 관련 포스팅을 참조해보자. )


링크 : http://jins-dev.tistory.com/entry/HTTP-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EA%B3%BC-%EC%9B%B9%EC%84%9C%EB%B2%84%EC%9D%98-%EA%B5%AC%ED%98%84?category=760047


 하지만 많은 브라우저 및 웹 프레임워크들이 이를 기본적으로 지원하지는 않는 경우가 많고 그말인즉슨, 이는 정식 스펙이 아니기 때문에 권고되는 사항은 아니다. 하위호환성 및 통신에 있어서 많은 불편함이 수반될 것이다.




 실무에서 REST API 설계를 처음 다뤄보았을 때, 부족한 점들이 많이 있었다. 

많이 혼나고 나서 너무나도 좋은 글을 찾아 정리해두었다.


원본 출처는 다음과 같으며, 아래 내용은 한글로 알아보기 쉽게 정리해 놓은 부분이다.

(https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/)



 RESTful Service API 의 설계 업무를 하던 중 처음에 칭찬받아서 대충 설계하다가 다시 공부하게 된 내용.

일반적으로 커뮤니케이션과 개발에 원활한 설계를 하기 위한 10가지의 방법이 존재하며 다음과 같이 서술된다.


(1) Use nouns but no verbs (동사가 아닌 명사를 통한 API 설계를 할것.)


- 가장 초보적인 실수가 CRUD 에 대한 기능을 Path에 명시하는 것이다.

 예를들어 /getAllCars, /createNewCar, /deleteAllCars 와 같은 방식인데, REST 디자인을 위해서는 CRUD 를 Method 를 이용하며 표현하기 때문에 그 외의 것은 전부 entity를 명시하는 명사로 표기한다.


 따라서 예시는 다음과 같이 HttpRequest Method를 통해서 기능이 구분되어야 한다.




 이 원칙은 표준 웹의 REST API 대부분에서 따르는 원칙이며, GET / POST / PUT / DELETE 외의 Method 들, 가령 HEAD 나 TRACE 등, 의 경우 별도의 원칙을 갖고있지 않다.



(2) GET method and query parameters should not alter the state


 역시 위에 명시한 바와 마찬가지로 모든 Functioning 은 Method로 하고 query parameter 역시 state change 등의 기능으로 사용하지 않는다.

(ex) GET /users/811?activate or GET /users/811/activate ---         (X)



(3) Use plural nouns


 path는 entity 항목을 나타내므로 복수형을 쓰고 특정하게 지정하고 싶다면 다음 Path에서 {항목} 으로 지정해준다.

(ex) GET /cars/{carName}            -- cars 와 같이 path 는 복수형. 뒤에 {carName} 으로 단수 객체 모델을 가져온다.



(4) Use sub-resources for relations


 리소스는 관계에 따른 계층형 구조를 유지한다.


(ex) GET /cars/811/drivers/ > 차811 의 드라이버 리스트를 반환한다.

(ex) GET /cars/811/drivers/4 > 차 811의 4번 드라이버 정보를 반환.



(5) Use HTTP headers for serialization formats


 서버와 클라이언트 간 커뮤니케이션에 있어서 Header Serialization format을 이용한다.



(6) Use HATEOAS


 HATEOAS 란 Hypermedia As The Engine Of Application State 의 약자로, 하이퍼텍스트 링크를 데이터 내에 명시할 경우 API를 통해 갈 수 있는 navigator 형태로 표기해준다.


<HATEOAS 의 예시. links 하위의 href 를 주목하자>


 위의 예시에서 drivers.links.href 항목은 주소를 저장함으로써, 외부주소에 저장된 리소스 자체를 가리키고 있다.

REST API 는 이런식으로 실제 리소스를 담는 것이 아닌 경로를 담아서 HyperMedia 컨텐츠를 기록한다.


 이렇게하면 네트워크 비용도 절감되고, 성능면에서도 많은 이점을 갖는다. 물론, 해당 주소로 다시 접근하여 실제 리소스를 받아와야 하는 점이 있지만, 오히려 리소스를 CDN 등을 이용한 static 저장소에 두고 가져온다면 네트워크 비용 측면에서도 훨씬 저렴하고 뛰어난 성능으로 핸들링할 수 있다.



(7) Provide filtering, sorting, field selection and paging for collections


 Filtering 은 특수한 query parameter 를 조건으로 주었을 때 해당 기능을 제공할 수 있게끔 하는 것이다.

(ex) GET /cars?color=red


 Sorting 은 오름차순, 내림차순의 정렬을 multiple field 에 대해 허용하는 것이다. 

(ex) GET /cars?sort=-name, +price > -(ascending) +(descending)


 Field selection 은 select 조건문과 같이 필드만 추출해서 보여줄 수 있도록 하는것으로, 네트워크 트래픽 관리에 효과적이다. 

(ex) GET /cars?fields=name, price


 Paging 은 Offset과 Limit 을 지정할 수 있는 조건으로 DB 조회에 필수적인 기능이다. (ex) GET /cars?offset=10&limit=5



(8) Version your API


 REST API는 Version 을 명기한다. (ex) /blog/api/v1



(9) Handle Errors with HTTP status codes


 에러는 별도의 로직으로 처리한다기 보다 HTTP 상태 코드와 메시지를 통해 처리하는 것이 좋다. 다음은 서버에서 반환하는 표준 HTTP 응답 코드를 설명한다.



에러에 따른 Payload 도 다음과 같이 명시한다.




(10) Allow overriding HTTP method


 몇몇 프록시 서버의 경우 POST와 GET 만을 지원하는 경우가 있다.

이러한 한계점을 지원하기 위해 API 단에서 override HTTP method 를 지원해줄 필요가 있다. X-HTTP-Method-Override 와 같은 Custom HTTP 헤더를 만들어 method 방식을 지원하자.






HTTP는 현재 세계에서 가장 널리 쓰이는 프로토콜 중 하나이다. 우리가 보통 사용하는 인터넷을 위한 기본 프로토콜이기도 하며, 최근래에는 다루는 기술도 비약적으로 발전하여, 예전에 웹의 영역이 아니라고 불렸던 게임 영역, 실시간 대용량 처리, 대용량 메시지 처리 등에서도 HTTP 기반의 웹 스택을 사용하는 경우를 흔히 몰 수 있다.


HTTP TCP 계층의 위에 HTTP 프로토콜 스택을 쌓아올린 Network Layer로 현재 가장 널리 사용되고 있는 Stateless / Connectless 형식의 프로토콜이다.


 여기서 Stateless 란, TCP와 다르게 상호간의 연결된 소켓이 연결을 유지하지 않는다는 의미이다.

 즉, 서로 요청과 응답만 처리하고 "상태" 는 기록하지 않는다. 서로 간에 지속적인 "연결" 이 유지되지 않기때문에 지속적이면서 연속적인 통신에는 적합하지 않다. 일반적으로 채팅 서비스를 구현할때 HTTP가 고려되지 않는 이유이기도 하다.


 대신에 단순한 정보 전달에 있어서는 가장 효율적인 프로토콜이라고 봐도 무방하다. 요청한대로 응답만 보내주면 되기 때문에, 서버 입장에서도 연결 관리에 대한 부담이 덜어지고, 클라이언트 측면에서도 원하는 정보만 얻을 수 있으므로 효율적이다.


 HTTP 역시 네트워크 프로그래밍이기 때문에 당연히 소켓을 이용하여 통신을 하게 되며, HTTP를 서비스하는 웹서버는 특유의 성질을 구현하기 위하여 일반적인 TCP 서버 등과는 다른 HTTP 프로토콜에 특화된 형태를 취하게 된다.


소켓을 이용하여 RAW한 방식으로 HTTP 웹서버를 구축할 때는 연결이 시작된 이후에(Accept) 바로 해당 소켓의 연결이 요청(Request)을 받고, 응답(Response)한 후에 연결이 끊어지게 만드는 것을 잊지 않아야 한다.


 HTTP URI Method 등을 기반으로 작동하게 된다.

 HTTP 프로토콜은 별다른 것이 있는 것이 아니라 말그대로 Socket 의 통신 버퍼에 특유의 프로토콜 스택을 쌓아올리는 것을 말한다. 다음은 요청과 응답에 따른 프로토콜 형태이다.

 

<HTTP Request Header>


 위의 요청 포맷에서 첫번째 라인은 Request Line이라고 해서 요청에 대한 포맷 정보를 명시하는 필수 요소이다. 해당 라인은 3가지의 필드로 이루어져 있으며 각 필드는 다음을 명시한다.


(1)  요청 메서드 : GET, POST, OPTIONS(UPDATE, DELETE), PUSH 등의 요청 방식이 온다.


(2)  요청 URI : 요청하는 자원의 위치를 명시한다.


(3)  HTTP 프로토콜 버전 : 프로토콜의 버전으로 1.0 1.1이 있다.



그 아래로 요청 헤더의 내용이 CRLF Delimeter로 하여 열거된다.

General Header : Cache-Control, Connection, Date, Pragma, Trailer, Transfer-Enco, Upgrade, Via, Warning

Request Header : Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, From, Host, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards, Proxy-Authorization, Range, Referer, TE, User-Agent

Entity Header : Allow, Content-Encoding, Content-Language, Content-Length, Content-Location, Content-MD5, Content-Range, Content-Type, Expires, Last-Modified, extension-header

요청 헤더의 내용이 전부 명시가 된 이후에는 Message Body두 개의 CRLF 아래에 명시된다. 두개의 CRLF 뒤에는 Request Body 가 포함되게 되며, 이부분은 HTTP 스펙에 따라서 해석의 여부가 나뉜다. (이부분은 REST API 소개 및 HTTP Method 분석 포스팅에서 자세히 다루겠다.)


 다음은 응답 헤더의 모습이다.


<HTTP Response Header>

 

첫번째 라인은 요청헤더의 Request Line 처럼 Response Header에서는 Status Line 이라 불리며 필수 정보를 포함한다.


(1)  응답 프로토콜과 버전 : HTTP/1.0, HTTP/1.1, HTTP/2.0 이 현재 버전으로 존재한다.


(2)  응답 코드 : 1xx, 2xx, 3xx, 4xx, 5xx 등의 번호가 응답 코드로 사용된다.


(3)  응답 메시지 : OK, Not Found, Internal Server Error 등의 메시지를 출력한다.


 역시 해당 라인 아래에 응답 헤더의 내용들이 포함되는데, Accept-Range, Age, Etag, Location, Proxy-Authenticate, Retry-After, Server, Vary, WWW-Authenticate 등의 정보가 포함된다. 이후에 두 개의 CRLF 라인 다음에 Message Body 가 첨부된다.

 


 복잡해보이지만 내부 구성원리는 간단하다. 

일반적인 TCP 서버를 구성하고, 프로토콜을 만드는데, 클라이언트로부터 요청을 받아서 해석하는 부분에 Request Parser를, 서버쪽에서 처리를 마치고 클라이언트에 응답을 내려줄 부분에 Response Builder 를 메시지의 머리에 붙여주면 된다.


 그렇게만 하면 웹서버가 HTTP Speculation 상에 약속한대로 메시지를 해석한다. 그렇다면 웹서버 작동의 기본 로직을 정리해보자.


(1) TCP 소켓을 열어 클라이언트의 접속을 받는다. (Accept)


(2) 커넥션을 관리할 수 있는 객체를 만들어, 쓰레드에 할당한다.


(3) 쓰레드가 해당 커넥션에 대해 HTTP Request 를 분석한다. Method 에 따라 서버 내에서 url Handler 를 라우팅해주고, 해당 라우터 메서드에서 요청에 대한 로직을 구현한다.


(4) 로직에 따라 구현한 HTTP Response 를 클라이언트에 반환하고, 접속을 끊는다. (Stateless)



좀 더 내부 동작 원리가 궁금하다면 자세한 예제는 다음 소스를 확인하면 도움이 될 것이다. 오래전에 작성한 소스라 허접하지만 웹서버 구현에 있어 기본에 충실한 좋은 예제라고 생각한다.

(https://github.com/ParkJinSang/Jinseng-Server)





CORS(Cross Origin Resource Sharing)


최근에는 대부분의 웹 서버가 REST API 서버를 통해 동작하며, 백엔드 개발자라면 익숙하겠지만 프론트엔드만 담당한다면 당황할 수도 있다. 실제로 처음에 본인도 웹페이지부터 서버 개발에 입문하면서 당황했던 부분이다.


웹사이트는 종종 인터넷 상의 다른 서버에 호스팅된 리소스를 요청하는 경우가 있다. 이때, 서버를 거치지 않고 Browser 단에서 직접 외부 호스트로부터 리소스를 받는 일은 서버의 보안 입장에서 유쾌한 일은 아니며, 호스팅하는 서버 입장에서도 무분별한 요청을 받게 되는 일이다. CORS는 이를 위해 보안정책을 통해 이를 제한하고자 하는 개념이다.


 CORS는 웹페이지가 리소스가 존재하는 다른 도메인으로 요청이 발생하는 것을 제한하는 규칙이다. 브라우저와 서버에서 다른 도메인과 안전하게 연결을 하고자 요청을 제약하는 규칙이다.


 이러한 정책이 생겨나게 된 배경에는 웹서버의 API 적 역할이 강화되었기 때문이다. REST API 를 소개하면서 포스팅하겠으나, 많은 종류의 Web API 는 GET을 제외한 요청들이 데이터를 변경할 수 있게끔 되어 있다. 


그렇기 때문에 GET이 아닌 다음과 같은 요청들의 경우 전처리과정(Pre-flight Request) 라 하여, HTTP 요청을 보내기 전에 먼저 OPTIONS 요청을 보내서 호스팅 서버로부터 확인 작업을 거친다.


 만약 호스팅 서버로부터 인증받은 도메인의 허가받은 요청이라면, 그다음 본 요청으로 리소스를 가져온다.


 - Pre-flight 요청이 선행되는 경우 : PUT , DELETE , CONNECT, OPTIONS, TRACE, PATCH

(POST 요청의 경우 조건적으로 특정 MIME Type에 대한 호출에 대해서 Pre-flight가 선행된다.)


 이는 브라우저에서 쉽게 확인할 수 가 있다. 브라우저는 위와 같이 GET / POST 이외의 요청에 대해 OPTIONS Request를 보내는데, 다음처럼 Origin 이라는 헤더를 Request 에 담아 보내게 되고, 서버 측에서 Response로 Access-Control-Allow-Origin 라는 헤더에 허용된 도메인을 담아 보낸다.




그렇다면 호스팅 서버의 응답 값을 같이 살펴보자





위의 예에서 해당 서버는 example.com 이라는 도메인에서 요청한 PUT, DELETE 메서드만 허용한다. 물론 example.com 에서 요청했기 때문에 위의 요청은 PUT 또는 DELETE 라면 에러를 발생시키지 않고, 해당 PUT / DELETE API 를 요청한다.


 CORS 표준은 서버가 지정한 일련의 도메인 들에 대해 정보를 읽을 수 있도록 허가하는 HTTP 헤더를 추가하여 문제를 해결한다.



 * 일반적으로 서버측에서는 요청들에 대해 Credentials 를 담아 처리하게 하며, 별도로 리소스에 대한 접근을 허용하는 Whitelist 방식으로 처리한다.



+ Recent posts