쿠키, 세션 및 지속성

소개

HTTP(HyperText Transfer Protocol)는 서버에서 클라이언트로 데이터를 전송하는 무상태 요청-응답 모델을 지원하도록 설계되었습니다. 최초 버전인 1.0에서는 요청과 연결의 비율이 정확히 1:1이었습니다(즉, 연결당 하나의 요청-응답 쌍만 지원했습니다).

버전 1.1에서는 이 비율이 N:1, 즉 연결 하나당 여러 요청을 처리할 수 있도록 확장되었습니다. 이는 서버에서 클라이언트로 전송해야 하는 수많은 객체와 요소를 비롯해 웹 페이지의 복잡성이 증가하는 문제를 해결하기 위한 것이었습니다.

2.0이 도입된 후에도 HTTP는 연결당 여러 요청을 처리하는 모델을 계속 지원했습니다. 가장 획기적인 변화는 헤더 교환 방식과 텍스트 기반 전송에서 바이너리 전송으로의 전환입니다.

어느 순간 HTTP는 서버에서 클라이언트로 텍스트와 이미지를 전송하는 단순한 메커니즘을 넘어 애플리케이션을 위한 플랫폼이 되었습니다. 브라우저의 보편성, 크로스 플랫폼 특성, 그리고 여러 운영 체제와 환경을 지원하는 데 드는 막대한 비용 없이도 애플리케이션을 손쉽게 배포할 수 있다는 점은 분명 매력적이었습니다. 하지만 안타깝게도 HTTP는 애플리케이션 전송 프로토콜로 설계된 것이 아니라 문서를 전송하도록 설계되었습니다. API와 같은 현대적인 HTTP 활용 방식조차 문서와 유사한 페이로드를 전제로 합니다. 대표적인 예가 키-값 쌍으로 이루어진 데이터 형식을 텍스트로 전송하는 JSON입니다. 문서와 애플리케이션 프로토콜은 일반적으로 텍스트 기반이라는 공통점이 있지만, 유사성은 거기까지입니다.

기존 애플리케이션은 상태를 유지할 방법이 필요하지만, 문서는 그렇지 않습니다. 애플리케이션은 논리적 흐름과 프로세스를 기반으로 구축되며, 이를 위해서는 애플리케이션이 현재 사용자의 위치를 파악해야 하므로 상태가 필요합니다. HTTP는 본질적으로 상태를 유지하지 않지만, 웹에서 사실상의 애플리케이션 전송 프로토콜로 자리 잡았습니다. 기술 역사상 가장 널리 받아들여지고 유용한 편법 중 하나를 통해, 애플리케이션을 사용하는 동안 상태를 추적할 수 있는 수단이 HTTP에 추가되었습니다. 바로 이 "편법"에서 세션과 쿠키가 등장합니다.

세션

상태 비저장 방식을 상태 저장 방식으로 전환하기

세션은 웹 서버와 애플리케이션 서버가 상태를 유지하는 방식입니다. 이러한 단순한 메모리 영역은 웹 서버나 애플리케이션 서버에 설정된 각 TCP 연결과 연계되며, HTTP 기반 애플리케이션에서 정보를 저장하는 인메모리 저장소 역할을 합니다.

사용자가 서버에 처음 연결하면 세션이 생성되어 해당 연결과 연결됩니다. 그런 다음 개발자는 이 세션을 애플리케이션 관련 데이터를 저장하는 공간으로 사용합니다. 이 데이터에는 고객 ID와 같은 중요한 정보부터 사이트의 첫 페이지를 어떤 방식으로 표시할지와 같은 비교적 덜 중요한 정보까지 포함될 수 있습니다.

세션의 유용성을 가장 잘 보여 주는 예는 장바구니입니다. 우리 대부분이 한 번쯤은 온라인 쇼핑을 해 본 적이 있기 때문입니다. 장바구니에 담긴 상품은 "세션"이 유지되는 동안 그대로 남아 있는데, 이는 장바구니의 각 상품이 서버의 세션에 어떤 형태로든 저장되기 때문입니다. 또 다른 좋은 예로는 마법사 방식의 제품 구성 또는 사용자 지정 애플리케이션이 있습니다. 이러한 "미니" 애플리케이션에서는 여러 옵션을 살펴보고 선택할 수 있으며, 마지막에는 추가한 온갖 부가 기능의 예상 비용을 보고 대개 깜짝 놀라게 됩니다. 각 "옵션 화면"을 차례로 진행할 때 이전에 선택한 옵션은 세션에 저장되므로 손쉽게 조회하거나 추가 또는 삭제할 수 있습니다.

최신 애플리케이션은 상태 비저장 방식으로 설계되지만, 실제 아키텍처는 이 원칙을 따르지 않을 수도 있습니다. 최신 확장 방식은 샤딩과 같은 아키텍처 패턴에 의존하는 경우가 많으며, 샤딩에서는 사용자 이름이나 계정 번호처럼 인덱싱할 수 있는 데이터를 기준으로 요청을 라우팅해야 합니다. 올바른 라우팅과 애플리케이션 동작을 보장하려면 인덱싱 가능한 데이터를 각 요청에 포함해야 하므로, 일종의 상태 저장 방식이 필요합니다. 이런 점에서 최신 ‘상태 비저장’ 애플리케이션과 API도 상태 저장 방식의 이전 애플리케이션과 마찬가지로 세심한 관리가 필요한 경우가 많습니다.

문제는 세션이 연결에 종속되어 있으며, 연결이 너무 오랫동안 유휴 상태로 유지되면 시간 초과된다는 점입니다. 또한 연결에서 말하는 "너무 오랫동안"의 기준은 세션에 적용되는 기준과 상당히 다릅니다. 예를 들어 일부 웹 서버의 기본 설정에서는 연결이 15초 동안 유휴 상태, 즉 더 이상 요청이 없는 상태로 유지되면 해당 연결을 종료합니다. 반면 이러한 웹 서버의 세션은 기본적으로 300초, 즉 5분 동안 메모리에 유지됩니다. 이 두 설정은 명백히 서로 맞지 않습니다. 연결이 시간 초과로 종료되고 나면, 그 연결에 종속된 세션이 무슨 소용이 있겠습니까?

이러한 차이를 해소하려면 연결 시간 제한 값을 세션 시간 제한에 맞게 늘리기만 하면 된다고 생각할 수도 있습니다. 하지만 시간 제한을 늘리면 사용될 수도 있고 사용되지 않을 수도 있는 연결을 유지하는 데 메모리를 소모할 가능성이 있습니다. 이로 인해 서버가 동시에 수용할 수 있는 전체 사용자 수가 줄어들고 궁극적으로 성능이 저하될 수 있습니다. 그렇다고 세션 시간 제한을 연결 시간 제한에 맞춰 줄이는 것도 바람직하지 않습니다. 대부분의 사용자는 상품을 둘러보거나 새로 구입할 제품을 원하는 대로 구성하는 데 5분 이상 걸리기 때문입니다.

중요 참고 사항: HTTP/2는 이러한 문제 중 일부를 해결하지만, 상태 유지와 관련된 또 다른 문제를 야기합니다. 사양에서 요구하는 것은 아니지만, 주요 브라우저는 TLS/SSL을 통한 HTTP/2 연결만 허용합니다. 두 프로토콜 모두 재협상으로 인한 성능 저하를 방지하려면 연결을 지속적으로 유지해야 하며, 이를 위해서는 세션 인식, 즉 상태 저장 방식이 필요합니다.

결국 비활성 상태로 인해 관련 연결이 종료된 후에도 세션이 서버 메모리에 남아 귀중한 리소스를 소모하게 되며, 애플리케이션이 제대로 작동하지 않는 사용자들의 불만을 초래할 수도 있습니다.

다행히도 이 문제는 쿠키를 사용하면 해결할 수 있습니다.

쿠키

빵 부스러기의 흔적은 집으로 이어집니다

쿠키는 브라우저가 클라이언트에 저장하는 데이터 조각입니다. 쿠키에는 사용자와 사용 중인 애플리케이션, 방문한 사이트에 관한 다양하고 흥미로운 정보가 저장될 수 있으며, 실제로도 저장됩니다. "쿠키"라는 용어는 UNIX 컴퓨팅에서 잘 알려진 개념으로, 쿠키의 발상과 명칭에 영감을 준 "매직 쿠키"에서 유래했습니다. 쿠키는 HTTP 헤더인 Cookie를 통해 브라우저와 서버 간에 생성되고 공유됩니다.

Cookie: JSESSIONID=9597856473431 Cache-Control: no-cache Host: 127.0.0.2:8080 Connection: Keep-Alive

브라우저는 HTTP 헤더의 쿠키를 컴퓨터의 파일에 저장해야 한다는 것을 자동으로 인식하며, 도메인별로 쿠키를 관리합니다. 브라우저는 특정 도메인의 쿠키를 항상 HTTP 헤더에 담아 서버로 전달하므로, 웹 애플리케이션 개발자는 애플리케이션의 서버 측에서 해당 값을 요청하기만 하면 가져올 수 있습니다.

세션/연결 지속 시간 문제는 쿠키를 통해 해결됩니다. 거의 모든 최신 웹 애플리케이션은 "세션 ID"를 생성하여 쿠키로 전달합니다. 이를 통해 세션이 생성된 연결이 종료된 후에도 애플리케이션이 서버에서 해당 세션을 찾을 수 있습니다. 이처럼 세션 ID를 주고받으면 HTTP와 같은 무상태 프로토콜에서도 상태를 유지할 수 있습니다. 하지만 웹 애플리케이션의 사용량이 단일 웹 서버나 애플리케이션 서버의 처리 용량을 초과하면 어떻게 될까요? 일반적으로 모든 사용자가 만족할 만한 가용성과 성능을 제공하도록 애플리케이션을 확장하기 위해 로드 밸런서 또는 오늘날의 아키텍처에서는 애플리케이션 전송 컨트롤러(ADC)를 도입합니다.

최신 애플리케이션에서도 쿠키가 여전히 사용될 수 있지만, 다른 HTTP 헤더가 매우 중요해집니다. 인증 및 권한 부여를 위한 API 키는 HTTP 헤더를 통해 전달되는 경우가 많으며, 라우팅과 백엔드 서비스의 적절한 확장에 필요한 데이터를 담는 기타 사용자 지정 헤더도 마찬가지입니다. 이러한 데이터를 전달하는 데 기존의 ‘쿠키’를 사용하는지 아니면 다른 HTTP 헤더를 사용하는지보다, 전체 아키텍처에서 이러한 데이터가 지니는 중요성을 인식하는 것이 더 중요합니다.

이 방식의 문제점은 로드 밸런싱 알고리즘이 일반적으로 서버 간 요청 분산에만 초점을 맞춘다는 것입니다. 로드 밸런싱 기법은 라운드 로빈, 최소 연결 또는 최단 응답 시간과 같은 업계 표준 알고리즘을 기반으로 합니다. 이러한 알고리즘은 모두 상태를 유지하지 않으므로 동일한 사용자가 애플리케이션에 보내는 각 요청이 서로 다른 서버로 분산될 수 있습니다. 이 경우 한 서버의 세션에 저장된 데이터가 "풀"의 다른 서버와 공유되는 경우가 거의 없기 때문에 HTTP의 상태를 구현하기 위해 수행한 모든 작업이 무용지물이 됩니다.

바로 이럴 때 지속성이라는 개념이 유용합니다.

지속성

우리를 하나로 묶는 유대

지속성(persistence), 즉 스티키니스(stickiness)는 단일 사용자의 요청이 항상 처음 연결된 서버로 전달되도록 ADC에서 구현하는 기법입니다. 일부 부하 분산 제품과 서비스에서는 이 기법을 ‘스티키 세션(sticky sessions)’이라고 부르는데, 그 특성을 매우 적절하게 표현한 명칭입니다.

지속성은 SSL/TLS가 활성화된 사이트의 부하 분산에 오랫동안 사용되어 왔습니다. 컴퓨팅 리소스를 많이 소모하는 협상 프로세스가 완료되고 키 교환까지 이루어진 후 이 프로세스를 다시 시작하면 성능이 크게 저하되기 때문입니다. 따라서 ADC는 사용자가 처음 연결한 서버로 항상 전달되도록 SSL 세션 지속성을 구현했습니다.

수년에 걸쳐 브라우저 구현 방식이 변화하면서, 이러한 세션의 비용이 많이 드는 재협상을 방지하기 위한 기법이 필요해졌습니다. 이 기법을 쿠키 기반 지속성이라고 합니다.

로드 밸런서는 SSL/TLS 세션 ID에 의존하는 대신, 클라이언트가 사이트에 처음 접속할 때 세션을 고유하게 식별하는 쿠키를 삽입한 다음 후속 요청에서 해당 쿠키를 참조하여 적절한 서버와의 연결을 유지합니다.

이후 쿠키 기반 지속성 개념은 애플리케이션 세션에도 적용되었습니다. 웹 서버와 애플리케이션 서버에서 생성된 세션 ID 정보를 사용하여 동일한 세션 동안 사용자의 요청이 항상 같은 서버로 전달되도록 합니다. 이 기능이 없다면 부하 분산이 필요한 애플리케이션은 세션 정보를 공유할 다른 방법을 찾거나, 사용자 기반을 지원하는 데 필요한 서버 수가 감당할 수 없을 정도로 빠르게 늘어날 만큼 세션 및 연결 제한 시간을 연장해야 합니다.

가장 일반적인 지속성 방식은 HTTP 헤더로 전달되는 세션 ID를 사용해 구현되지만, 오늘날의 ADC는 다른 데이터도 기준으로 삼아 세션 지속성을 유지할 수 있습니다. 쿠키에 저장할 수 있거나 IP, TCP 또는 HTTP 헤더에서 추출할 수 있는 모든 데이터를 사용해 세션 지속성을 유지할 수 있습니다. 실제로 애플리케이션 메시지 내에서 사용자를 고유하게 식별하는 모든 데이터를 지능형 ADC가 활용하여 브라우저와 서버 간의 연결 지속성을 유지할 수 있습니다.

결론

HTTP는 상태 비저장 프로토콜이지만, 우리는 이 보편적인 프로토콜에 상태를 억지로 끼워 넣는 데 성공했습니다. 지속성 기능과 애플리케이션 전송 컨트롤러를 활용하면 상태 유지에 필요한 쿠키와 세션 간의 다소 취약한 연동을 깨뜨리지 않으면서도 가용성과 성능이 뛰어난 웹 애플리케이션을 설계할 수 있습니다.

이러한 기능 덕분에 HTTP는 상태를 유지할 수 있지만, 그 구현과 실행 자체는 여전히 무상태로 이루어집니다. 쿠키, 세션, 지속성이 없었다면 우리는 애플리케이션을 구축할 수 있는 상태 저장 프로토콜을 분명 찾아냈을 것입니다. 하지만 그 대신 애플리케이션 전송 컨트롤러(Application Delivery Controller)의 기능이 브라우저(클라이언트)와 서버 사이를 중재하여 이러한 기능을 제공함으로써, HTTP의 활용 범위를 정적 웹 페이지와 기존 애플리케이션에서 현대적인 마이크로서비스 기반 아키텍처와 디지털 경제의 총아인 API까지 확장했습니다.

2018년 1월 19일 게시