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

HTTP 헤더가 nginx에서 Ruby 앱으로 전달되는 방법

요즘 거의 모든 웹 개발은 프레임워크를 기반으로 이루어집니다. Rails, Sinatra, Lotus 중 무엇을 사용하든, 쿠키나 기타 헤더가 nginx나 Apache에서 애플리케이션 서버를 거쳐 여러분의 앱까지 어떻게 전달되는지 신경 쓸 필요가 없습니다. 그냥 알아서 전달되니까요.

하지만 이 여정을 조금 더 깊이 들여다볼 가치가 충분합니다. 헤더의 이야기 속에는 웹의 역사에 관한 흥미로운 정보가 많이 숨어 있기 때문입니다.

HTTP 헤더란 무엇일까?

웹 브라우저가 요청(request)을 보낼 때마다 HTTP 헤더라고 불리는 데이터들을 함께 전송합니다. 여기에는 쿠키, 사용자 에이전트(user agent) 정보, 캐싱 정보 등 정말 유용한 내용이 가득 담겨 있습니다.

브라우저의 개발자 도구에서 요청을 살펴보면 어떤 헤더들이 전송되는지 확인할 수 있습니다. 아래 예시처럼 말이죠. 보시다시피 헤더는 특별한 것이 아닙니다. 단지 특정 형식으로 포맷된 텍스트일 뿐입니다.

HTTP 헤더가 nginx에서 Ruby 앱으로 전달되는 방법

헤더가 앱으로 전달되지 '않는' 방식

Rack 앱을 작성해 본 경험이 있다면 env 해시를 본 적이 있을 겁니다. 이 해시에는 앱의 환경 변수가 담겨 있는데, 안을 들여다보면 일반적인 시스템 환경 변수 외에도 모든 요청 헤더가 함께 포함되어 있는 것을 알 수 있습니다.

# config.ru
run lambda { |env| [200, {"Content-Type" => "text/plain"}, [env.inspect]] }

# 출력 결과:
# { "HTTP_HOST"=>"localhost:9000", "HTTP_CONNECTION"=>"keep-alive", "HTTP_PRAGMA"=>"no-cache", "HTTP_CACHE_CONTROL"=>"no-cache", "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "HTTP_UPGRADE_INSECURE_REQUESTS"=>"1", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", ... }

그런데 이것은 nginx가 헤더를 앱에 전달하는 방식이 아닙니다. :)

애플리케이션 서버

오늘날 대부분의 Ruby 웹 앱은 Unicorn 같은 애플리케이션 서버 위에서 실행됩니다. 앱 서버는 nginx가 직접 생성(spawn)하는 프로세스가 아니기 때문에, nginx가 앱 서버의 환경 변수를 설정해 줄 수 없습니다.

그렇다면 헤더는 어떻게 nginx에서 Unicorn으로 전달될까요? 의외로 간단합니다. nginx가 요청을 앱 서버로 넘길 때, 헤더를 포함한 전체 HTTP 요청을 그대로 전송하는 것입니다.

이를 실제로 확인해 보기 위해, nginx가 보내는 모든 내용을 STDOUT에 출력하는 아주 간단한 애플리케이션 서버를 만들어 보았습니다.

require "socket"

# 소켓을 생성하고 파일 시스템에 저장
server = UNIXServer.new('/tmp/socktest.sock')

# nginx의 연결 대기
socket = server.accept

# 소켓에서 모든 내용 읽기
while line = socket.readline
  puts line.inspect
end

socket.close

nginx 설정을 Unicorn 대신 이 서버에 연결하도록 변경하면, 앱 서버로 전달되는 정보가 정확히 무엇인지 눈으로 확인할 수 있습니다. 바로 평범한 HTTP 요청입니다. 헤더를 포함해서요.

HTTP 헤더가 nginx에서 Ruby 앱으로 전달되는 방법

간단한 업스트림(upstream) 앱 서버를 직접 작성하는 방법이 궁금하다면, unix sockets에 관한 필자의 글을 참고해 보세요.

그렇다면 왜 굳이 환경 변수를 신경 쓰는 걸까?

1993년, NCSA는 "Common Gateway Interface", 줄여서 CGI라 불리는 사양(specification)을 발표했습니다.

HTTP 헤더가 nginx에서 Ruby 앱으로 전달되는 방법

CGI는 Apache 같은 웹 서버가 디스크에 있는 임의의 프로그램을 실행하여 동적 웹페이지를 생성할 수 있게 해주는 메커니즘이었습니다. 사용자가 페이지를 요청하면, Apache는 실제로 셸을 호출해 프로그램을 실행하고 그 결과를 반환했습니다. Apache가 앱 프로세스를 직접 생성했기 때문에, 당연히 환경 변수도 설정해 줄 수 있었습니다.

CGI 표준은 HTTP 헤더를 환경 변수 형태로 전달하도록 규정하고 있습니다. 그리고 기존 시스템 환경 변수와의 이름 충돌을 피하기 위해, 헤더 이름 앞에 반드시 "HTTP_" 접두사를 붙이도록 명시하고 있습니다.

그 결과 다음과 같은 환경 변수들이 만들어집니다:

HTTP_ACCEPT="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
HTTP_ACCEPT_CHARSET="ISO-8859-1,utf-8;q=0.7,*;q=0.7"
HTTP_ACCEPT_ENCODING="gzip, deflate"
HTTP_ACCEPT_LANGUAGE="en-us,en;q=0.5"
HTTP_CONNECTION="keep-alive"
HTTP_HOST="example.com"
HTTP_USER_AGENT="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0"

요즘은 새로운 개발에서 CGI를 사용하는 경우가 거의 없지만, HTTP 헤더가 환경 변수 형태로 저장되는 모습은 여전히 어디서나 볼 수 있습니다. 심지어 그것이 사실은 '가짜' 환경 변수일 때조차 말입니다.

앱 서버는 어떻게 이를 흉내 내는가

애플리케이션 서버는 원시(raw) HTTP 요청에서 헤더를 직접 파싱합니다. 그렇다면 이 헤더들은 어떻게 환경 변수 자리에 들어가게 될까요? 비밀은 간단합니다. 앱 서버가 스스로 그 자리에 넣어주는 것입니다.

필자가 webrick 소스 코드를 조금 뒤져본 결과, 결정적인 증거를 찾을 수 있었습니다:

self.each{|key, val|
  next if /^content-type$/i =~ key
  next if /^content-length$/i =~ key
  name = "HTTP_" + key
  name.gsub!(/-/o, "_")
  name.upcase!
  meta[name] = val
}

결국 이렇게 만들어진 "가짜" 환경 변수들은 진짜 환경 변수들과 하나로 병합된 뒤 Rack 앱에 전달되고, 다시 Rails로 넘어갑니다. 그리고 Rails는 환경 해시에서 이 값들을 다시 꺼내 쓰는 것이죠. :) 결국 CGI 시대의 유산이 오늘날까지 이어지고 있는 셈입니다.