레일즈 세션은 어떻게 동작하는가

How Rails Sessions Work

Posted by npmachine on April 9, 2017

Justin Weiss의 How Raills Sessions Work를 번역한 글입니다.


만약 레일즈 앱에서 누가 접속했는지를 알 수 없다고 한다면? 만약 두개의 페이지를 같은 사람이 요청했다는 사실을 모른다면? 만약 응답을 보내자마자 관련 데이터가 모두 사라진다고 한다면?

대부분의 정적 사이트는 괜찮을 수 있다. 그렇지만 많은 앱들은 사용자 정보를 저장할 수 있어야 한다. 이를테면 사용자 id나 선호하는 언어, 웹 사이트를 아이패드에서 데스크탑 버전으로 표시 여부 같은 정보 말이다.

세션은 위처럼 복수의 요청 사이에 유지하는 작은 양의 데이터를 집어넣기에 최적의 공간이다.

세션은 활용하기 쉽다.

session[:current_user_id] = @user.id

마치 작은 마법 같다. 세션이라는게 뭐지? 레일즈는 어떻게 사용자와 데이터를 짝 지을 수 있는거야? 세션 데이터를 어디에 저장할 지를 어떻게 결정하지?

세션은 무엇인가?

세션은 요청이 지속되는 동안 데이터를 저장하는 장소를 말한다. 그래서 후속 요청이 해당 데이터를 참조할 수 있다.

콘트롤러 액션에서 데이터를 저장할 수 있고,

# app/controllers/sessions_controller.rb
def create
  # ...
  session[:current_user_id] = @user.id
  # ...
end

그리고 다른 콘트롤러에서 그 정보를 읽을 수 있다.

# app/controllers/users_controller.rb
def index
  current_user = User.find_by_id(session[:current_user_id])
  # ...
end

그리 대단해 보이지 않을 수도 있다. 그렇지만 세션이 모든 것을 연결하여 사용자 브라우저와 레일즈 앱을 결합한다. 그리고 이는 쿠키로 시작한다.

웹 페이지를 요청하면, 서버는 응답에 쿠키를 설정한다.

~ jweiss$ curl -I http://www.google.com | grep Set-Cookie

Set-Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly

응답을 받은 브라우저는 이 쿠키를 저장한다. 브라우저는 쿠키가 유효한 동안에는 요청을 받을 때마다 서버에 쿠키를 되돌려 보낸다.

...
> GET / HTTP/1.1
> User-Agent: curl/7.37.1
> Host: www.google.com
> Accept: */*
> Cookie: NID=67=J2xeyegolV0SSneukSOANOCoeuDQs7G1FDAK2j-nVyaoejz-4K6aouUQtyp5B_rK3Z7G-EwTIzDm7XQ3_ZUVNnFmlGfIHMAnZQNd4kM89VLzCsM0fZnr_N8-idASAfBEdS; expires=Wed, 16-Sep-2015 05:44:42 GMT; path=/; domain=.google.com; HttpOnly
...

쿠키를 보면 헛소리처럼 보인다. 정상이다. 쿠키 안의 정보는 사용자를 위한게 아니기 때문이다. 쿠키는 레일즈 앱이 해석한다. 앱이 설정하였으니, 또한 앱이 읽을 수 있다.

쿠키가 세션과 무슨 관련이 있나?

쿠키가 있다. 하나의 요청을 처리하는 동안에 쿠키에 데이터를 집어 넣었다. 다음 요청에서 쿠키로부터 같은 데이터를 얻었다. 이러면 세션과 쿠키 사이에 차이점이 무엇인가?

기본적으로 레일즈에서는 차이점이 없다. 안전한 쿠키를 만들기 위해 레일즈가 하는 작업 외에는 생각한대로 작동한다. 레일즈 앱이 어떤 데이터를 쿠키에 집어 넣으면, 그 데이터가 쿠키에서 나온다.

하지만 세션 데이터를 사용하는데 쿠키가 언제나 정답은 아니다.

  • 쿠키에는 단지 4kb 데이터만 저장할 수 있다.

    대부분 충분하지만, 때때로 그렇지 않을 때가 있다.

  • 쿠키는 모든 요청에 함께한다.

    쿠키가 커지면 요청과 응답도 더 커지고, 따라서 웹사이트도 더 느려진다.

  • 만약 사고로 시크릿 키를 노출하게 되면, 사용자는 쿠키안의 데이터를 변조할 수 있다.

    이 쿠키가 현재 사용자 id 같은 데이터를 포함하고 있다면, 누구든지 자기가 원하는 다른 사용자로 둔갑할 수 있다.

  • 적합하지 않은 데이터를 쿠키에 저장할 경우 안전하지 않을 수 있다.

    주의 깊게 사용한다면 큰 문제는 아니다.

그렇지만 위의 이유로 쿠키 안에 세션 데이터를 저장할 수 없다면, 레일즈에는 세션을 유지하기 위한 몇가지 다른 선택지가 있다.

세션을 저장하는 다른 선택지들

다른 세션 저장소도 거의 비슷하게 동작한다. 실제 예제를 보는게 이해하기 쉽다.

액티브 레코드로 세션을 추적한다면

  1. session[:current_user_id] = 1을 호출하는데 세션이 없다면

  2. 레일즈는 임의의 세션 ID(09497d46978bf6f32265fefb5cc52264라고 가정하자)로 새로운 레코드를 세션 테이블에 생성한다.

  3. 해당 레코드의 데이터 속성에 Base64 인코딩된 {current_user_id: 1}를 저장한다.

  4. 그리고 생성된 세션 ID, 09497d46978bf6f32265fefb5cc52264Set-Cookie로 브라우저에 반환한다.

다음 번에 페이지를 요청하면,

  1. 브라우저는 Cookie: header에 같은 쿠키를 실어 앱으로 보낸다. (예를 들어: Cookie: _my_app_session=09497d46978bf6f32265fefb5cc52264; path=/; HttpOnly)

  2. session[:current_user_id]를 호출하면

  3. 쿠키에서 세션 ID를 얻어서 세션 테이블에서 해당 레코드를 찾는다.

  4. 해당 레코드의 data 속성에서 current_user_id를 반환한다.

세션을 데이터베이스, Redis, Memcached 등 어디에 저장해도 대부분 비슷한 과정을 거친다. 쿠키에는 session ID만 있고, 레일즈가 그 ID를 이용하여 세션 스토어에서 데이터를 찾는다.

쿠키 스토어, 캐쉬 스토어 혹은 데이터베이스 스토어?

쿠키에 세션을 저장하는 방법은 특별한 문제가 없다면 가장 간편한 방법이다. 별도의 설정이나 장비가 필요하지 않다.

그렇지만 쿠키 세션 스토어를 고도화할 필요가 있다면 두가지 선택이 있다.

세션을 데이터베이스에 저장하던가, 혹은 캐쉬에 저장하는 것이다.

캐쉬에 세션 저장하기

데이터나 파셜을 저장하기 위해 Memcached 같은 걸 이미 사용하고 있을지도 모른다. 만약 그렇다면 캐쉬 저장소를 세션 데이터를 저장하는 장소로 사용하기 위해 필요한 설정을 이미 완료한 셈이며, 이를 세션 데이터 저장소로 손쉽게 활용할 수 있다.

세션 저장소가 대책 없이 커지는 건 걱정할 필요 없다. 저장소가 너무 커지면 캐쉬는 오래된 세션을 자동으로 삭제한다. 그리고 캐쉬를 항상 메모리에 보관하기 때문에 빠르다.

그렇지만 완벽하지는 않다.

  • 만약 오래된 세션을 유지해야 한다면, 캐쉬에서 삭제하지 않도록 하고 싶을 거다.

  • 세션하고 캐쉬 데이터는 용량을 가지고 싸울거다. 메모리가 충분하지 않다면, 캐쉬 적중률이 떨어지고 세션이 일찍 만료되는 현상이 생긴다.

  • 캐쉬를 리셋하기 원한다면 (예를 들어 레일즈를 업그레이드 해서 캐쉬 데이터가 더 이상 맞지 않다고 하자), 모두의 세션을 만료시키지 않고 할 수 있는 방법은 없다.

아직은 이게 세션 데이터를 Avvo(역자주: Justin Weiss가 일하는 회사)에서 사용하는 방법이다. 그리고 지금까지 잘 동작하고 있다.

데이터베이스에 세션 저장하기

만약 세션 데이터를 정상적으로 만료시킬 때까지 유지하고 싶다면, 데이터베이스를 이용하는게 낫다. Redis나 액티브 레코드나 다른 거 등등.

그렇지만 데이터베이스 세션도 단점이 있다.

  • 일부 데이터베이스 저장소는 세션이 자동으로 삭제되지 않는다.

    그러므로 만료된 세션을 직접 처리하고 비워줘야 한다.

  • 세션 데이터가 모두 차면 데이터베이스가 어떻게 동작하는지 알고 있어야 한다.

    Redis를 세션 저장소로 사용하는가? 모든 세션 데이터를 메모리에 저장하려고 하는가? 서버가 충분한 메모리가 있는가? 아니라면 콘솔에서 손댈 수 없을 정도로 무척 비효율적으로 스와핑이 일어나는가?

  • 세션 데이터를 생성할 때 더 주의깊게 살펴야 한다. 그렇지 않다면 데이터베이스는 쓸모 없는 세션들로 가득찬다.

    예를 들어, 실수로 모든 요청이 올때마다 세션을 건드린다면, 구글봇이 수백, 수천의 쓸모 없는 세션을 만들어 낼 수 있다. 그리고 그건 분명 좋지 않다.

대부분의 문제들은 드물게 일어난다. 그렇다고 하더라도 그 가능성에 대해서 인지하고 있어야 한다.

그래서, 어떻게 세션을 저장해야 할까?

쿠키 저장소 용량 제한을 넘지 않을거라고 확신한다면, 쿠키로 저장해라. 설정도 필요 없고, 골치 아픈 유지보수도 없다.

세션 저장소를 캐쉬로 할지 데이터베이스로 할지는 세션 조기 만료가 얼마나 치명적인 문제인지에 달렸다. 나는 세션 데이터를 짧게 사용하기 때문에, 캐쉬 데이터가 잘 맞았다. 그러므로 나는 보통 쿠키 데이터를 먼저 시도해 보고, 그런 다음 캐쉬를, 그리고 데이터베이스를 사용한다.


npmachine

a mediocre engineer