우리가 매일 쓰는 웹사이트는 어떻게 보면 매우 단순한 방식으로 동작합니다. 그저 원하는 자료(Resource)를 웹서버에 요청하면 그 자료를 돌려주는 형태이지요. 예를 들어 아래 주소에 있는 이미지를 달라고 요청하면 요청한 이미지를 돌려줍니다.

http://show.me.the.image/my-horrible-face.png

그런데 우리가 하는 요청에 조건이 붙는 경우가 있습니다. 예를 들면 ‘쇼핑광’이라는 사용자가 로그인했으면 요청한 에르메스백 사진 을 보여주고, 아니라면 로그인 페이지로 넘겨서 로그인을 요구하는 경우입니다. 일상생활에 매우 자주 접하는 광경이죠. 또 쇼핑몰에서 원하는 상품을 장바구니에 열심히 담고 결제하려고 하니 아차! 로그인을 안 했네… 로그인하면 장바구니에 있던 거 다 날아가는 거 아니야? 하고 생각할 수 있겠지만 쇼핑몰이 매출이 일어나기 직전 상황에 절대 그렇게 안 하겠죠! 로그인해도 담아놨던 물건들은 그대로 남아있고 결제까지 할 수 있습니다.

자 그러면, ‘쇼핑광’이라는 사용자가 로그인한 상태인지 아닌지, 장바구니에 열심히 담아 놓은 물건들이 ‘쇼핑광’이 담아놓은 물건인지, 인터넷 쇼핑몰은 어떻게 알 수 있을까요? 이 포스트에는 이 모든 것을 가능하게 하는 세션(Session)에 대하여 알아봅니다.

인터넷을 지탱하는 서로의 약속 HTTP

이 세상에 약속이라는 것이 없으면 어떤 일이 벌어질까요? 내가 하고 싶은 데로 모든 것을 할 수 있으니 정말 프리한 세상이 올까요? 아마도 아닐 겁니다. 비효율의 극치를 달리는 혼란한 세상이 올 겁니다. 저는 사람들이 만들어낸 약속 중에 시간 이 가장 가치 있다고 생각하는데요 시간이라는 개념이 없으면 시간으로 표현되는 모든 과학적 사실들이 입증하기도 어려울 것이고 너와 나의 역사를 기억하는 그때 그 순간의 시각을 일기에 적을 수도 없을 겁니다.

아무튼 인터넷에서도 서로 이렇게 하기로 하자 라는 약속이 엄청 많은데요 그중에 하나가 저 위에 있는 주소 앞에 붙어있는 HTTP 라는 아이입니다. HyperText Transfer Protocol, 하이퍼텍스트라는 놈을 전송하기 위한 규약 정도로 대충 해석해 볼 수 있겠는데 요즘은 잘 가지는 않지만 은행에 가면 보내실 때, 찾으실 때 적혀있는 종이 본적 있으시죠? 돈을 보내기 위해서 또는 찾기 위서는 은행에서 정한 양식에 맞춰서 그리고 필요한 정보를 넣어서 제출해야 은행원이 처리해줄 수 있습니다. 이처럼 HTTP도 그런 양식 정도로 이해하면 되겠습니다. 사진을 요청한 주소를 다시 해석해보면 HTTP라는 양식으로 요청할 건데 show.me.the.image/my-horrible-face.png 에 있는 자원을 좀 줘 라고 해석할 수 있겠네요.

HTTP는 당연하게도 규칙을 정의한 규약서가 있습니다. 요청할 때에는 요청을 받는 사람(Server)이 누구인지, 어떤 정보를 원하는지에 대한 사항을 약속한 형태로 적어서 인터넷상으로 흘려보내면 이 요청은 받는 사람에게로 배달됩니다.

POST /restapi HTTP/1.1
Host: ninano.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

name=ohnami

반대로 요청을 받아 응답을 주는 서버도 요청에 대한 실제 응답 데이터와 요청한 결과 코드가 뭔지, 그리고 응답 결과는 어떤 타입의 자료인지 등을 넣어서 돌려주도록 약속되어 있습니다. 대략 아래처럼 생겼습니다.

HTTP/1.1 200 OK
Date: Mon, 23 May 2020 21:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Encoding: UTF-8
Content-Length: 138
Last-Modified: Wed, 08 Jan 2020 21:11:55 GMT

<html>
<body
  You know me?
</body>
</html>

이런 양식대로 서버에게 요청하고 그 결과를 받아서 우리가 보기 좋게 만들어 주는 아이가 바로 인터넷 익스플로러, 크롬 같은 웹 브라우저입니다.

HTTP에게 기억력을, 세션

HTTP는 상태를 저장하지 않는다 라는 특징을 가지고 있습니다. 즉, 무언가를 요청받은 사람은(Server) 요청한 사람(Client)의 정보를 저장하지 않고 모든 요청을 독립적으로 취급합니다. 이런 식으로 처리하면 서버는 클라이언트를 위해 별다른 저장공간을 할애하지 않고도 HTTP 서버를 구축할 수 있어서 구성 자체가 간단해지고, 구성이 간단해지니까 많은 양의 요청을 처리할 수 있는 성능을 확보할 수 있습니다.

앞서 링크한 규약서를 열어서 Abstract의 첫 줄을 보면

The Hypertext Transfer Protocol (HTTP) is a stateless application-level protocol

HTTP는 비 상태 애플리케이션 레벨 프로토콜이다 라고 명확히 정의하고 있습니다. 즉, HTTP로 하는 모든 요청은 독립적인 요청입니다. 다시 말하면 내가 1분 전에 보낸 요청과 지금 보내는 요청 사이에는 어떠한 관계도 없다 라는 것입니다.

그런데 우리가 주로 쓰는 웹사이트들을 보면 내가 누구인지 알고 있는 것처럼 동작합니다. 모든 HTTP 요청이 서로 독립적이라면 해당 요청 누가 보낸 건지, 달라고 한다고 그냥 줘도 되는 건지 서버는 어떻게 알 수 있을까요?

크게 두 가지 방법으로 나눌 수 있습니다. 요청할 때 사용자 정보를 포함하여 응답에 필요한 모든 정보를 같이 주는 방법, 아니면 내가 누군지만 알려주고 서버에 저장된 요청자 정보 활용하는 방법입니다. 세션은 두 번째 방법에 해당합니다.

아니, 아까 HTTP에서 상태를 저장하지 않는다고 하지 않았나요? 라고 반문하시는 분이 계실 겁니다. 네 맞습니다. HTTP는 상태를 저장하지 않습니다. 즉 사용자별 저장공간을 확보하거나 연결을 유지하거나 하는 일은 하지 않습니다. (HTTP/1.1의 Keep Alive는 별개로 얘기합시다) 하지만 어떻게든 클라이언트 정보를 저장해야 서비스가 동작할 수 있다면 어떻게든 해결을 해야 하는데 그 방법중에 하나가 세션인 겁니다.

세션은 HTTP가 지원하지 않는 요청자 정보를 잠시 저장할 수 있는 특별한 저장 공간이기도 하면서, 클라이언트가 서버에 실제는 연결이 되어 있지 않지만 마치 연결이 되어 있는 것처럼 만들어 주는 논리적인 연결 을 의미합니다.

넌 새로운 아이인데? 너만의 공간을 만들어 줄게

새로운 세션은 언제 어떻게 만들어질까요? 서버가 어떻게 설정되어 있느냐에 따라서 다르겠지만 일반적으로는 처음 요청을 받았을 때 누구인지에 대한 정보가 없으면 새로운 클라이언트라 생각하고 새 ID를 만든 뒤에 ID를 클라이언트에 응답할 때 알려줍니다.

다시 얘기하지만 이건 정해져 있는 건 아니고 서버 쪽 서비스 구현에 따라 다릅니다. 최초 접속에 모두 세션 저장소를 만들면 불필요한 부하를 줄 수 있기 때문입니다. 또 세션 자체가 필요 없는 경우도 있습니다. 응답을 브라우저로 받을 필요가 요청(REST API Call 등)이 대표적입니다.

클라이언트는 다음 요청 때 발급받은 ID를 같이 주면서 내가 누구인지 밝히고 요청을 하게 되면 서버는 누구의 요청인지 알 수 있게 됩니다.

서버는 클라이언트 정보를 다양한 방법으로 저장합니다. 메모리에 저장하거나, 서버와 연결된 DB에 저장하거나, 아니면 다른 원격 저장소를 이용하거나 어쨌든 어딘가에 저장하는데 어디에 저장하느냐에 따라 장단점이 있습니다. 이 부분은 조금 있다가 잠시 얘기해보겠습니다.

세션과 쿠키

많은 분이 쿠키와 세션을 헷갈리십니다. 세션을 쓴다면서 쿠키도 쓰던데? 쿠키나 세션이나 데이터 저장하는 공간 아니야? 등.

세션은 앞서 언급했듯이 서버에 정보를 잠시 저장할 수 있는 특별한 저장 공간이라고 했습니다. 사실 특별하다고 했지만 뭐 딱히 거창하지는 않습니다. JAVA로 표현하면 Map<String, Object> session 이런 아이거든요. String 타입 키로 구분하는 Object. DB로 표현하면 Header, Detail 두 개 테이블로 이루어진 저장공간으로 표현할 수 있습니다.

쿠키는 클라이언트(브라우저)에 정보를 잠시 저장할 수 있는 특별한 저장공간입니다. 클라이언트 코드에서 쿠키를 만들 수 있고, 서버에서 응답을 줄 때 이런 이런 쿠키 만들어서 저장해놔 라고 지시를 받고 만들 수 있습니다. 쿠키의 큰 특징중 하나는 브라우저가 서버로 요청을 할 때 이 쿠키를 요청서에 포함시킨다는 것입니다. 브라우저가 알아서 요청 헤더에 Cookie 라는 이름으로 딱 넣어서 요청합니다.

왜 쿠키 이야기를 하냐면 세션을 사용하기 위해 쿠키를 사용할 수 있기 때문입니다. 앞서 새로운 세션 ID를 발급받는 흐름을 표현한 그림을 보여드렸습니다. 저 흐름을 보면 서버에서 클라이언트에 ID를 줄 때 어떻게 ID를 주는지, 반대로 클라이언트에서 서버로 요청하면서 자기 ID를 어떻게 알려주는지 궁금하시지 않았겠지만… 궁금하지 않나요?

서버가 클라이언트 ID를 어떤 방법으로 추적할 것인지 정의한 것을 세션 트래킹 모드라고 합니다. 트래킹 모드에는 쿠키 사용 모드, URL Rewriting 모드, SSL 모드 가 있는데 대부분 서버에서 쿠키 사용 모드를 기본값으로 하고 있고 이 모드 사용을 권장하고 있습니다. 그래서 특별한 설정이 없으면 클라이언트를 구분하기 위해 쿠키를 활용하는 것입니다.

앞선 그림을 쿠키 트래킹 모드로 표현을 해보면 이렇습니다.

이런 이유로 Tomcat을 사용하는 서버에 접속하면 JSESSIONID 이라는 키로 쿠키가 만들어져 있는 것을 종종 볼 수 있습니다. 키 이름은 서버 설정으로 바꿀 수 있습니다. ASP.NET 으로 만들어진 서버도 마찬가지입니다. 디폴트 키 이름은 다르겠지만요.

다시 정리하면, 세션 정보를 저장하고 있는 서버가 어떤 클라이언트인지 구분하기 위해 세션 ID 를 발급해서 클라이언트로 내려주면 클라이언트는 ID는 쿠키 에 저장합니다. 추후 요청이 있을 때마다 쿠키에 담긴 ID를 같이 보내주어서 서버가 어떤 클라이언트의 요청인지 알 수 있게 합니다. 클라이언트의 세션 ID가 저장된 쿠키를 세션 쿠키라고 부르기도 합니다. (정식 명칭은 아니고 세션ID를 저장하고 있는 쿠키라는 의미입니다) 정리 되시나요?

세션이라는 단어는 광범위하게 사용됩니다. 쿠키 종류 중에서 세션 쿠키 라고 부르는 것이 있습니다. 세션 쿠키는 브라우저가 종료되면 자동으로 없어지는 쿠키입니다. 앞서 말한 세션을 저장하고 있는 쿠키와 다른 의미이니 혼돈하지 말아 주세요.

그다지 오래 살지 못하는 쿠키와 세션

세션과 쿠키를 설명한 글에 잠시 라는 단어에 주목해보겠습니다. 쿠키와 세션은 생명이 대체로 길지 않습니다. 쿠키를 만들 때 이 쿠키를 언제까지 존속 시켜야 하는지 지정해야 하는데 지정하지 않으면 세션 쿠키(쿠키 종류 중 하나의 세션 쿠키, 세션ID를 저장하고 있는 쿠키를 의미 하는 것이 아님), 지정하면 영속 쿠키(Persistent cookie)가 됩니다. 세션 쿠키는 브라우저를 닫으면 자동으로 삭제되고, 영속 쿠키는 지정한 만료일이 되면 삭제됩니다. 브라우저 닫고 다시 열었을 때 로그인 상태로 남겨두고 싶다면 영속 쿠키에 세션 ID를 저장해야 할 것이고, 아니라면 세션 쿠키에 세션ID를 담도록 해야 할 것입니다.

서버 측에 있는 세션에도 접속한 모든 클라이인트 ID를 영구적으로 보관하기는 저장공간이 부족할 수 있습니다. 그래서 세션에 만료일시를 지정하고 그 시간이 되도록 재요청이 없으면 세션 정보를 삭제하도록 설정을 합니다. 톰캣에는 session-timeout 이라는 속성이 있는데 타임아웃 시간을 분 단위로 설정할 수 있습니다.

세션 공유 문제

우리가 주로 접속하는 웹사이트 중에 서버 한 대로 운영되고 있는 곳은 거의 없다시피 합니다. 서버 한 대가 죽으면 비즈니스가 멈춰버리기 때문이죠. 이는 엄청난 영업 손실이 발생할 수 있습니다. 그래서 언제나 클라이언트가 서버에 접속할 수 있도록 서버를 여러 대 동시에 운영을 하여 가용성을 끌어 올립니다. 여러 서버를 동시에 운영하기 위해서는 클라이언트의 요청을 분배시켜주는 장비 또는 시스템이 있어야 합니다. 그림으로 표현하면 아래처럼 됩니다.

서버1에서 클라이언트로부터 최초로 요청받게 되면 새로운 세션 ID를 만들고 서버1의 메모리에 저장 시켜 놓습니다. 그런데 다음번에 요청이 왔을 때 다른 서버로 요청이 전달되면 세션정보를 찾을 수 없기 때문에 다시 새로운 새션 ID를 발행해 버립니다.

이 상태로는 클라이언트가 원하는 서비스를 받을 수 없게 됩니다. 왜냐하면 최근 받았던 사진을 다시 달라고 요청했을 때 이 요청이 서버1로 전달되느냐 서버2로 전달되느냐에 따라서 결과가 달라질 것이고 이는 클라이언트에서 예상한 응답 결과가 아닙니다.

그래서 여러 대의 서버를 동시에 운영하기 위해서는 세션을 동기화시킬 수 있는 방안 을 반드시 마련해야 합니다. 이 문제를 해결하기 위해서 크게 3가지 방법으로 생각해볼 수 있습니다.

  1. 클라이언트별로 담당 처리 서버를 지정하는 방법 - Sticky Session
  2. 서버끼리 자기 서버에 보관하고 있는 세션의 변경사항을 실시간으로 주고받는 방법 - Session Clustering
  3. 세션 정보를 서버가 아닌 다른 외부 저장소에 저장하는 방법 - Session Server

1. 난 한 놈만 패

최초 요청을 받은 서버가 해당 클라이언트에 대한 요청을 모두 책임지는 형태입니다. 최초에 어느 서버가 세션을 만들었는지는 Cookie에 서버ID를 포함하거나 IP를 이용하는 방법 등이 있습니다. 가장 구현이 간단하고 응답속도도 가장 빠르지만 한 서버에 부하가 집중 될 수 있고, 해당 서버가 다운되면 모든 세션 정보가 손실 된다는 단점이 있습니다.

2. 절친만 있었으면 좋겠어

세션 정보를 TCP Socket으로 서버끼리 공유하는 방법입니다. 사용자에게 빠른 응답을 줄 수 있고 서버 하나가 다운 되도 세션이 손실되지 않는다는 장점이 있지만 요즘같이 수많은 서버를 사용하는 환경에서는 부적합합니다. 한 서버가 모든 친구 서버들을 다 알아야 하거든요. 서버를 2대~3대 정도 사용하는 소규모 시스템에 적용 을 고려해볼 수 있습니다. Tomcat에 클러스터링 기능이 있습니다만 평판은 그렇게 좋아 보이지는 않습니다.

3. 넌 죽으면 안 돼

최초 요청을 받은 서버가 세션 정보를 생성해서 외부 저장소에 저장합니다. 외부 저장소에 직접 넣을 수도 있고, 외부 저장소를 관리하는 별도 서비스가 있어서 그쪽으로 정보를 줄 수도 있습니다. 다른 서버가 클라이언트로부터 요청을 받으면 세션 정보를 외부로부터 가져와서 처리해줍니다. 응답 속도가 느리고 세션 서버가 죽으면 모든 서비스가 중단 되는 리스크가 있지만, 서버를 스케일 아웃 하기 편하고 마이크로서비스 와 잘 어울립니다. 요즘은 스프링 세션을 활용하면 매우 간단하게 구현도 가능합니다. 서버 저장소로 Redis가 가장 인기 있어 보입니다. (실제 적용 통계자료는 찾을 수 없었으나 이와 관련된 문서가 가장 많이 보여서 이렇게 판단했습니다)

정리하며

개발자들이 의외로 세션에 관해서 헷갈리고 잘 모르는 경우가 많은 것 같습니다. ‘세션이라는 용어가 다양한 컨텍스트에서 사용하기 때문 아닐까’ 라는 생각이 듭니다. 이 글을 통해서 오해해서는 안 되는 것이 인증을 처리하기 위해서 세션이 무조건 필요하다고 생각하면 안 된다는 것입니다. 쿠키만 가지고도 충분히 인증정보를 다룰 수 있고 기타 필요한 정보도 저장할 수 있습니다. 다만 쿠키만 사용했을 때 많은 보안 취약점이 있고 불필요한 네트워크 트래픽을 유발할 수 있기 때문에 세션 사용을 고려해야 한다는 것입니다. 언제가 될지는 모르겠지만 보안 측면에서 세션을 어떻게 다룰 것인가에 관해 정리해보겠습니다.