Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Rails 세션은 어떻게 작동할까? 쿠키부터 캐시·DB 저장소까지

만약 여러분의 Rails 앱이 방문자가 누구인지 전혀 알 수 없다면 어떨까요? 같은 사람이 서로 다른 두 페이지를 요청했다는 사실조차 파악할 수 없다면? 응답을 반환하는 순간 저장해 둔 모든 데이터가 사라져 버린다면?

대부분 정적인 사이트라면 큰 문제가 없을 수도 있습니다. 하지만 대부분의 앱은 사용자에 대한 최소한의 데이터를 저장할 수 있어야 합니다. 사용자 ID일 수도 있고, 선호하는 언어일 수도 있으며, iPad에서 항상 데스크톱 버전의 사이트를 보길 원하는지 여부일 수도 있습니다.

session은 바로 이런 종류의 데이터를 담기에 완벽한 곳입니다. 한 번 이상의 요청에 걸쳐 유지하고 싶은 작은 데이터 조각들이죠.

세션은 사용하기 매우 쉽습니다:

session[:current_user_id] = @user.id

하지만 다소 마법처럼 느껴질 수 있습니다. 세션이란 정확히 무엇일까요? Rails는 어떻게 올바른 데이터를 올바른 사람에게 보여줄 수 있을까요? 그리고 세션 데이터를 어디에 저장할지는 어떻게 결정해야 할까요?

세션이란 무엇인가?

세션은 한 요청 동안 데이터를 저장해 두었다가, 이후 요청에서 다시 읽을 수 있도록 해주는 저장 공간일 뿐입니다.

컨트롤러 액션에서 데이터를 설정할 수 있습니다:

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

겉보기에는 별것 없어 보일 수 있습니다. 하지만 모든 것이 연결되려면 사용자의 브라우저와 Rails 앱 간의 긴밀한 협력이 필요합니다. 그리고 그 출발점은 바로 쿠키입니다.

웹페이지를 요청하면 서버는 응답 시 쿠키를 설정할 수 있습니다:

~ jweiss$ curl -I https://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
...

많은 쿠키가 그냥 알아볼 수 없는 문자열처럼 보입니다. 그리고 그래야 합니다. 쿠키 안의 정보는 사용자를 위한 것이 아니기 때문입니다. 쿠키가 무엇을 의미하는지 해석할 책임은 Rails 앱에 있습니다. 앱이 설정한 값이므로 앱이 읽을 수 있는 것이죠.

그럼 세션과는 무슨 관계일까?

쿠키가 있습니다. 한 요청에서 데이터를 넣으면, 다음 요청에서 같은 데이터를 얻을 수 있습니다. 그렇다면 이것과 세션은 무엇이 다를까요?

기본적으로 Rails에서는 큰 차이가 없습니다. Rails는 쿠키를 더 안전하게 만들기 위해 몇 가지 처리를 수행할 뿐입니다. 그 외에는 기대하는 대로 작동합니다. Rails 앱이 쿠키에 데이터를 넣으면, 같은 데이터가 쿠키에서 나옵니다. 이것이 전부라면 굳이 세션과 쿠키를 구분할 이유가 없겠죠.

하지만 쿠키가 세션 데이터에 항상 최선의 선택은 아닙니다:

  • 쿠키에는 약 4kb 정도의 데이터만 저장할 수 있습니다.

    대개는 충분하지만, 때로는 부족할 수 있습니다.

  • 쿠키는 모든 요청마다 함께 전송됩니다.

    쿠키가 크면 요청과 응답도 커지고, 그만큼 웹사이트가 느려집니다.

  • secret_key_base를 실수로 노출하면, 사용자가 쿠키 안의 데이터를 임의로 변경할 수 있습니다.

    여기에 current_user_id 같은 값이 들어 있다면, 누구나 원하는 사용자로 위장할 수 있게 됩니다!

  • 부적합한 종류의 데이터를 쿠키에 저장하면 보안상 위험할 수 있습니다.

주의해서 사용한다면 이런 문제들은 크지 않습니다.

하지만 위와 같은 이유로 세션 데이터를 쿠키에 저장할 수 없다면, Rails는 세션을 보관할 다른 장소들을 몇 가지 제공합니다:

대체 세션 저장소

쿠키 세션 저장소가 아닌 모든 세션 저장소는 거의 같은 방식으로 작동합니다. 다만 실제 예시를 통해 살펴보는 것이 이해하기 쉽습니다.

ActiveRecord로 세션을 관리한다고 가정해 보겠습니다:

  1. 앱에서 session[:current_user_id] = 1을 호출했는데 아직 세션이 존재하지 않는 경우:

  2. Rails는 sessions 테이블에 임의의 세션 ID(예: 09497d46978bf6f32265fefb5cc52264)를 가진 새 레코드를 생성합니다.

  3. {current_user_id: 1}(Base64 인코딩)을 해당 레코드의 data 속성에 저장합니다.

  4. 생성된 세션 ID인 09497d46978bf6f32265fefb5cc52264Set-Cookie 헤더를 통해 브라우저에 반환합니다.

다음 페이지를 요청할 때는,

  1. 브라우저가 Cookie: 헤더를 통해 같은 쿠키를 앱에 전송합니다.

    (예: Cookie: _my_app_session=09497d46978bf6f32265fefb5cc52264;
    path=/; HttpOnly)

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

  3. 앱은 쿠키에서 세션 ID를 꺼내 sessions 테이블에서 해당 레코드를 찾습니다.

  4. 그런 다음 해당 레코드의 data 속성에서 current_user_id 값을 반환합니다.

데이터베이스, Memcached, Redis 등 어디에 세션을 저장하든 대부분 이와 같은 과정을 따릅니다. 쿠키에는 오직 세션 ID만 담기고, Rails 앱이 그 ID를 이용해 세션 저장소에서 실제 데이터를 조회하는 구조입니다.

문제없이 작동한다면, 세션을 쿠키에 저장하는 것이 가장 간편한 방법입니다. 별도의 인프라나 설정이 전혀 필요하지 않으니까요.

하지만 쿠키 세션 저장소의 한계를 넘어서야 한다면 두 가지 선택지가 있습니다. 데이터베이스에 저장하거나, 캐시에 저장하는 것입니다.

캐시에 세션 저장하기

이미 partial이나 데이터를 캐싱하기 위해 Memcache 같은 도구를 사용하고 있을 수 있습니다. 그렇다면 캐시 스토어는 이미 설정이 되어 있기 때문에 두 번째로 손쉬운 세션 저장 위치입니다.

세션 저장소가 무한정 커지는 것을 걱정할 필요도 없습니다. 캐시가 너무 커지면 오래된 세션이 자동으로 제거되기 때문입니다. 게다가 캐시는 대부분 메모리에 유지되므로 속도도 빠릅니다.

하지만 완벽하지는 않습니다:

  • 오래된 세션을 실제로 유지해야 한다면, 세션이 캐시에서 강제로 삭제되는 것을 원치 않을 것입니다.

  • 세션과 캐시 데이터가 메모리 공간을 두고 경쟁합니다. 메모리가 부족하면 캐시 미스가 급증하고 세션이 조기에 만료될 수 있습니다.

  • 캐시를 재설정해야 하는 상황(예: Rails 업그레이드 후 기존 캐시 데이터가 더 이상 유효하지 않은 경우)에서는, 모든 사용자의 세션을 만료시키지 않고는 재설정할 방법이 없습니다.

그럼에도 불구하고 Avvo에서는 이 방식으로 세션 데이터를 저장하고 있으며, 지금까지 큰 문제 없이 잘 운영되고 있습니다.

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

세션 데이터를 적법하게 만료될 때까지 확실하게 보관하고 싶다면, Redis, ActiveRecord 등 어떤 형태든 데이터베이스에 저장하는 것이 좋습니다.

하지만 데이터베이스 세션 저장에도 단점이 존재합니다:

  • 일부 데이터베이스 스토어에서는 세션이 자동으로 정리되지 않습니다.

    따라서 만료된 세션을 직접 주기적으로 청소해 주어야 합니다.

  • 세션 데이터로 가득 찼을 때 데이터베이스가 어떻게 동작하는지 파악해야 합니다.

    Redis를 세션 저장소로 사용하나요? 모든 세션 데이터를 메모리에 유지하려고 할까요? 서버 메모리가 충분한가요, 아니면 ssh로 접속해 조치할 수 없을 정도로 스왑이 심해질까요?

  • 세션 생성 시점을 신중하게 관리하지 않으면, 쓸모없는 세션으로 데이터베이스가 가득 차게 됩니다.

    예를 들어, 모든 요청에서 실수로 세션을 건드리면 googlebot이 수십만 개의 무의미한 세션을 생성할 수 있습니다. 그건 분명 골칫거리가 되겠죠.

이런 문제들 대부분은 자주 발생하지 않지만, 반드시 인지하고 있어야 합니다.

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

쿠키 스토어의 한계에 부딪히지 않을 것이라고 확신한다면 쿠키를 사용하세요. 설정 부담이 거의 없고 유지보수도 간편합니다.

캐시와 데이터베이스 중 무엇에 저장할지는, 세션이 조기에 만료되었을 때 그 피해가 얼마나 클지에 대한 판단 문제입니다. 저는 세션 데이터를 비교적 일시적인 것으로 취급하기 때문에 캐시 스토어가 잘 맞았습니다. 그래서 보통 쿠키를 먼저 시도하고, 그다음 캐시, 마지막으로 데이터베이스 순으로 검토합니다.

여러분은 어떠신가요? 세션을 어떻게 저장하고 계신가요? 댓글로 의견을 나눠주세요!

Ruby와 Rails의 내부 동작 원리를 더 깊이 이해하고 싶다면 '젬(gem)은 어떻게 작동할까?' 글을 확인해 보세요. 웹의 근본적인 작동 방식에 관심이 있다면 '웹 서버 vs 앱 서버' 글도 함께 살펴보시길 추천합니다.