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

하나의 Rails 앱으로 여러 하위 도메인 지원하기

이번 글에서는 하나의 Rails 앱으로 여러 하위 도메인(subdomain)을 지원하는 방법을 알아보겠습니다. funkygames.co라는 게임 웹사이트가 있고, app.funkygames.co, api.funkygames.co, dev.funkygames.co 같은 여러 하위 도메인을 단일 Rails 애플리케이션으로 운영하고 싶다고 가정해 봅시다. 모든 하위 도메인에서 올바른 인증이 수행되고, 중복된 라우트가 발생하지 않도록 만드는 것이 목표입니다.

이를 위해 Rails의 강력한 라우팅 기능을 활용하고, 로컬 환경에서 하위 도메인을 설정하는 방법과 여러 하위 도메인에 대한 테스트 작성법까지 살펴보겠습니다.

사전 준비 사항

이 글에서는 모든 하위 도메인의 DNS 레코드가 이미 Rails 앱을 가리키도록 설정되어 있다고 가정합니다. 여기서는 Rails 측면의 설정만 다루겠습니다.

여러 하위 도메인 처리하기

Rails는 routes.rb 파일을 사용해 들어오는 요청을 특정 컨트롤러 액션에 매핑합니다. 간단한 앱이라면 routes.rb의 모든 매핑이 다음과 같이 라우트를 컨트롤러 액션에 연결합니다.

  get '/games/:id', to: 'games#show'

이 방식으로는 routes.rb에 정의된 모든 엔드포인트가 모든 하위 도메인에 적용됩니다. 즉 app.funkygames.co/games/1뿐 아니라 api.funkygames.co/games/1 요청까지 이 라우트가 처리하게 됩니다. 하지만 우리는 app 하위 도메인에서 오는 요청만 이 라우트가 처리하기를 원합니다. api 하위 도메인은 오직 API 라우트 전용으로 사용되어야 합니다. 따라서 들어오는 요청이 특정 조건을 충족할 때만 해당 라우트가 동작하도록 규칙을 추가해야 합니다.

Rails 라우팅은 constraints 헬퍼 메서드를 제공하여 특정 라우트에 추가 조건을 지정할 수 있습니다.

  get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }

이렇게 하면 app.funkygames.co/games/1로 들어온 요청만 GamesController의 show 액션이 처리하고, app 외의 다른 하위 도메인에서 온 요청은 이 라우트가 무시합니다.

그런데 이런 식으로 라우트마다 일일이 constraints를 지정하는 것은 매우 번거롭습니다.

  get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }
  get '/games/list', to: 'games#list', constraints: { subdomain: 'app' }
  post '/games/start', to: 'games#start', constraints: { subdomain: 'app' }

이럴 때 constraints 헬퍼의 블록 형태를 사용하면 하나의 하위 도메인에 대한 여러 라우트를 한 번에 정의할 수 있습니다.

  constraints subdomain: 'app' do
    get '/games/:id', to: 'games#show'
    get '/games/list', to: 'games#list'
    post '/games/start', to: 'games#start'
  end

여러 하위 도메인에 대한 라우트를 정의하려면 routes.rbconstraints 블록을 여러 개 추가하기만 하면 됩니다.

constraints subdomain: 'app' do
  ...
end
 
constraints subdomain: 'api' do
  ...
end
 
constraints subdomain: 'dev' do
  ...
end

내부 동작 원리

Rails 라우팅에는 요청 제약(request constraint)과 세그먼트 제약(segment constraint) 두 가지가 있습니다. 세그먼트 제약은 요청 경로(path)에 조건을 거는 반면, 요청 제약은 들어오는 요청 자체에 조건을 겁니다. 요청 제약의 해시 키는 Request 객체에서 문자열을 반환하는 메서드 이름이어야 하며, 값은 기대하는 값이 됩니다.

constraints subdomain: 'app' do
  ...
end

위 예제에서는 Request 객체의 subdomain 메서드를 사용해 그 결과가 app, api, dev 같은 문자열과 일치하는지 확인합니다.

자세한 내용은 Rails 라우팅 가이드를 참고하세요.

다단계 하위 도메인 처리

스테이징 환경에서 app.staging.funkygames.co처럼 두 단계 하위 도메인을 사용한다고 해봅시다. 위와 같은 설정 그대로라면 app 하위 도메인으로 향해야 할 요청들이 전부 404를 반환하는 문제를 금방 발견하게 됩니다. 디버깅해 보면 하위 도메인에 대한 제약 조건이 실패하고 있는 것을 확인할 수 있습니다.

request.subdomain #=> app.staging

하위 도메인이 app을 반환할 것으로 기대했지만 실제로는 app.staging을 반환하는 것입니다. 물론 환경별 코드를 추가하지 않고 이 문제를 해결하고 싶습니다. 요청의 하위 도메인 파싱은 config.action_dispatch.tld_length 옵션이 관리합니다. 이 설정의 기본값은 1로, 한 단계의 하위 도메인만 지원합니다. 우리는 두 단계 하위 도메인을 사용하므로 이 값을 2로 설정해야 합니다.

# config/application.rb
config.action_dispatch.tld_length = Integer(ENV['TLD_LENGTH'] || 1)

환경 변수로 설정하면 스테이징과 프로덕션 환경에서 동일한 코드를 사용할 수 있습니다. 이제 라우팅 설정은 app.staging.funkygames.co에서도 정상적으로 동작합니다.

세션 관리

여러 하위 도메인에서 오는 요청을 처리하도록 라우트를 정의했다면, 이제 모든 하위 도메인의 인증을 처리할 차례입니다. 방법은 두 가지입니다. 모든 하위 도메인에서 동일한 사용자 세션을 공유하거나, 하위 도메인마다 별도의 세션을 유지하는 것입니다.

인증 핵심 정리

Rails는 기본적으로 쿠키에 사용자 세션 키를 저장합니다. 사용자가 로그인하면 세션 정보는 선택한 세션 스토어에 저장되고, 세션 키는 쿠키 형태로 브라우저에 저장됩니다. 사용자가 다음에 웹사이트를 방문하면 브라우저가 동일한 세션 쿠키를 서버로 보내고, 서버는 해당 세션 쿠키에 대응하는 세션이 존재하는지를 기준으로 로그인 여부를 판단합니다.

Rails 앱의 기본 세션 설정은 다음과 같습니다.

Rails.application.config.session_store :cookie_store, key: "_funkygames_session"

_funkygames_session 키가 세션 쿠키의 이름으로 사용되며, 그 값은 세션 ID가 됩니다.

쿠키 기초

기본적으로 쿠키는 요청의 도메인 기준으로 설정됩니다. 따라서 app.funkygames.co로 앱에 접속하면 세션 쿠키는 app.funkygames.co에 설정됩니다. 각 하위 도메인이 자체적인 세션 쿠키를 갖게 되므로, 기본 설정에서는 사용자 세션이 하위 도메인 간에 공유되지 않습니다.

하위 도메인 간 세션 공유하기

하위 도메인 간에 사용자 세션을 공유하려면 세션 쿠키를 funkygames.co 도메인 자체에 설정해야 모든 하위 도메인이 접근할 수 있습니다. 이는 세션 스토어 설정에 domain 옵션을 전달함으로써实现할 수 있습니다.

Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all

domain:all로 전달하면 Rails에게 세션 쿠키를 요청 호스트(개별 하위 도메인을 포함할 수 있음)가 아닌 앱의 최상위 도메인인 funkygames.co에 설정하라고 지시하는 것입니다. 이렇게 하면 서로 다른 하위 도메인 간에 세션을 공유할 수 있습니다.

domain 옵션에 배열 형태로 도메인 목록을 전달하면 여러 도메인도 지원할 수 있습니다.

모든 하위 도메인에 쿠키를 올바르게 설정하려면 하나 더 설정해야 하는 옵션이 있습니다. 바로 tld_length입니다. domain: :all을 사용할 때 이 옵션은 도메인을 어떻게 파싱해 TLD(최상위 도메인)를 해석할지 지정합니다. app.funkygames.co의 경우 쿠키를 설정할 때 Rails가 TLD를 funkygames.co로 인식하도록 tld_length를 2로 설정해야 합니다. 따라서 여러 하위 도메인을 위한 최종 세션 스토어 설정은 다음과 같습니다.

Rails.application.config.session_store :cookie_store,
                                       key: "_funkygames_session",
                                       domain: :all,
                                       tld_length: 2

세션 스토어의 tld_length 옵션은 앞서 설명한 config.action_dispatch.tld_length와 다릅니다.

여러 하위 도메인 테스트 작성하기

라우트가 하위 도메인별로 동작하므로, 테스트 요청에 올바른 하위 도메인이 없으면 리퀘스트 스펙(request spec)이나 통합 테스트에서 404 오류가 발생합니다. Rails 통합 테스트는 host! 헬퍼를 제공하며, 이를 사용하면 테스트 파일 내의 모든 요청에 대해 적절한 하위 도메인을 설정할 수 있습니다.

# Rails 통합 테스트에서 하위 도메인 설정
setup do
  host! 'dev.example.com'
end
 
# RSpec 리퀘스트 스펙에서 하위 도메인 설정
before do
 host! 'dev.example.com'
end

이렇게 하면 이후 요청들은 routes.rb의 하위 도메인 라우팅에 따라 올바르게 컨트롤러 액션으로 라우팅됩니다.

여기서 도메인 자체는 중요하지 않으며, 테스트하는 코드에 맞는 올바른 하위 도메인만 맞춰주면 됩니다.

로컬 개발 환경에서 여러 하위 도메인 설정하기

로컬에서 하위 도메인을 설정하는 방법은 여러 가지가 있습니다. 가장 간단한 방법은 /etc/hosts 파일을 수정하는 것입니다.

127.0.0.1 dev.funkygames.local
127.0.0.1 app.funkygames.local
127.0.0.1 api.funkygames.local

이렇게 하면 로컬 환경에서도 하위 도메인 설정이 정상적으로 동작합니다. pow 같은 도구를 사용해 로컬 하위 도메인을 관리할 수도 있습니다.

제약 조건 기반 하위 도메인 라우팅의 함정들

제약 조건 기반 하위 도메인 라우팅은 대부분의 경우 잘 동작하지만, 특정 상황에서는 골치 아플 수 있습니다.

외부 API 다루기

서드파티 API와 연동 작업을 할 때는 .local이나 .dev 같은 로컬 개발용 TLD가 허용되지 않습니다. 이런 경우 ngrok 같은 도구를 사용해야 하는데, 하위 도메인 기반 라우팅은 이런 상황에서 동작하지 않으므로 ngrok을 통해서도 접근 가능하도록 특정 라우트를 화이트리스트에 등록해야 합니다.

하위 도메인 제약 조건 밖의 라우트

어떤 라우트는 하위 도메인 제약 조건 안에 넣을 수 없습니다. 대표적인 예가 healthcheckping 엔드포인트입니다. Rails 앱 앞단에 로드밸런서를 두었다면, 로드밸런서는 앱이 살아있는지 주기적으로 확인해야 합니다. 이때 사용되는 healthcheck 엔드포인트는 하위 도메인 제약 조건 아래에 둘 수 없는데, 로드밸런서는 요청 호스트 정보를 모르는 경우가 대부분이기 때문입니다.

root 라우트의 부재

Rails에는 앱의 기본 라우트 역할을 하는 특별한 root 라우트가 있습니다. 다른 어떤 라우트도 요청과 매칭되지 않으면 root 라우트가 사용됩니다. 그런데 모든 라우트를 하위 도메인 제약 조건 아래에 배치하면 root 라우트가 아예 정의되지 않는 상황이 생길 수 있습니다. 일부 젬(gem)은 root 라우트의 존재에 의존하므로, 이에 맞는 검증과 보완 장치를 마련해 두어야 합니다.

마무리

이번 글에서는 몇 줄의 설정만으로 여러 하위 도메인을 지원하는 Rails 앱을 구성하는 방법을 알아봤습니다. 로컬 환경과 여러 환경에서의 하위 도메인 설정 방법과 함께, 여러 하위 도메인을 위한 효과적인 테스트 작성 팁도 살펴봤습니다. Rails가 제공하는 기반 덕분에 다중 하위 도메인 앱의 구축과 테스트는 생각보다 훨씬 쉽습니다.

P.S. Ruby Magic 글이 발행되는 대로 바로 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!