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

Ruby 개발자를 위한 Rack(랙) 완벽 가이드: 웹 프레임워크의 숨겨진 핵심

Rails, Sinatra를 비롯한 모든 Ruby 웹 프레임워크의 이면에서는 과연 어떤 일이 일어나고 있을까요?

그 답은 바로 Rack입니다. 프레임워크와 서버가 서로 소통할 수 있게 만들어주는 핵심 구성 요소죠.

그렇다면 Rack은 정확히 무엇일까요?

Rack은 프레임워크(Rails)와 애플리케이션 서버(Puma) 사이에 위치한 중간 계층입니다.

Ruby 개발자를 위한 Rack(랙) 완벽 가이드: 웹 프레임워크의 숨겨진 핵심

쉽게 말해, 두 구성 요소가 원활하게 통신할 수 있도록 연결해주는 접착제 역할을 하는 셈입니다.

Rack을 사용하는 이유

Rack을 사용하면 서로 다른 프레임워크와 서버를 자유롭게 조합할 수 있습니다.

즉, 각 구성 요소가 플러그인처럼 교체 가능한 부품이 되는 것입니다.

Puma를 Rails와 함께 사용할 수도 있고, Sinatra 또는 그 외 Rack 호환 프레임워크와 함께 사용할 수도 있습니다. Rack 인터페이스만 구현되어 있다면 어떤 프레임워크와 어떤 서버를 조합하든 전혀 문제가 없습니다.

Rack 덕분에 모든 구성 요소가 각자의 역할에 충실하며, 전체 시스템도 안정적으로 동작합니다!

Rack 미들웨어란 무엇인가?

Rack은 모든 웹 요청(request)과 응답(response)의 한가운데 위치합니다.

그래서 원치 않는 요청의 접근을 차단하는 파수꾼 역할을 할 수도 있고, 느린 응답을 기록하고 추적하는 기록관 역할을 할 수도 있습니다.

이것이 바로 Rack 미들웨어(middleware)입니다!

요청-응답 주기의 일부로 호출되며, 그 과정에서 특정 작업을 수행할 기회를 얻는 작은 Ruby 프로그램이라고 생각하면 됩니다.

구체적으로 어떤 용도로 활용될까요?

  • 로깅(Logging)
  • 세션(Sessions) 관리
  • 프로파일링(요청 처리에 걸리는 시간 측정)
  • 캐싱(Caching)
  • 보안(IP 주소 기반 요청 차단 또는 요청 횟수 제한)
  • 정적 파일 제공(CSS, JS, PNG 등)

이처럼 매우 유용한 기능들이며, Rails 역시 자체 기능의 상당 부분을 미들웨어를 통해 구현하고 있습니다.

Rails 프로젝트 내에서 rake middleware 명령어를 실행하면 현재 사용 중인 미들웨어 목록을 확인할 수 있습니다.

앞서 언급했던 Rack 인터페이스는 대체 어떤 모습일까요?

예제를 통해 직접 확인해 보겠습니다.

나만의 Rack 애플리케이션 작성하기

Rack의 동작 원리를 이해하는 가장 좋은 방법은 직접 애플리케이션을 작성해 보는 것입니다.

바로 시작해 보죠!

Rack 애플리케이션은 call 메서드 하나만 가지고 있는 클래스입니다.

기본 구조는 다음과 같습니다:

require 'rack'

handler = Rack::Handler::Thin

class RackApp
  def call(env)
    [200, {"Content-Type" => "text/plain"}, "Hello from Rack"]
  end
end

handler.run RackApp.new

이 코드를 실행하면 8080 포트에서 서버가 시작됩니다(직접 실행해 보세요!).

그런데 반환되는 이 배열은 무엇을 의미할까요?

  • HTTP 상태 코드 (200)
  • HTTP 헤더 ("Content-Type")
  • 응답 본문 내용 ("Hello from Rack")

요청 정보에 접근하고 싶다면 env 인수를 활용하면 됩니다.

사용 방법은 다음과 같습니다:

req = Rack::Request.new(env)

다음과 같은 메서드들을 사용할 수 있습니다:

  • path_info (/articles/1)
  • ip (사용자의 IP 주소)
  • user_agent (Chrome, Firefox, Safari 등)
  • request_method (GET / POST)
  • body (요청 본문)
  • media_type (plain, json, html)

이 정보들을 활용해 나만의 Rack 애플리케이션을 만들 수 있습니다.

예를 들어, IP 주소가 5.5.5.5인 사용자의 접근을 차단해 보겠습니다.

코드는 다음과 같습니다:

require 'rack'

handler = Rack::Handler::Thin

class RackApp
  def call(env)
    req = Rack::Request.new(env)

    if req.ip == "5.5.5.5"
      [403, {}, ""]
    else
      [200, {"Content-Type" => "text/plain"}, "Hello from Rack"]
    end
  end
end

handler.run RackApp.new

효과를 눈으로 확인하고 싶다면 주소를 127.0.0.1로 변경해 보세요.

만약 동작하지 않는다면 ::1(localhost의 IPv6 버전)을 시도해 보시기 바랍니다.

Rack 미들웨어 작성 및 사용하기

이번에는 애플리케이션과 미들웨어를 하나의 체인으로 연결해 함께 동작시키는 방법을 알아보겠습니다.

핵심 도구는 바로 Rack::Builder입니다.

역시 예제를 보는 것이 가장 빠릅니다.

먼저 Rack 앱입니다:

require 'rack'

handler = Rack::Handler::Thin

class RackApp
  def call(env)
    req = Rack::Request.new(env)

    [200, {"Content-Type" => "text/plain"}, "Hello from Rack - #{req.ip}"]
  end
end

다음은 미들웨어입니다:

class FilterLocalHost
  def initialize(app)
    @app = app
  end

  def call(env)
    req = Rack::Request.new(env)

    if req.ip == "127.0.0.1" || req.ip == "::1"
      [403, {}, ""]
    else
      @app.call(env)
    end
  end
end

마지막으로 두 요소를 결합하는 방법입니다:

app =
Rack::Builder.new do |builder|
  builder.use FilterLocalHost
  builder.run RackApp.new
end

handler.run app

이 예제에는 두 개의 Rack 애플리케이션이 존재합니다:

  • IP 검사를 담당하는 앱 (FilterLocalHost)
  • 실제 콘텐츠(HTML, JSON 등)를 전달하는 앱

주목할 부분은 @app.call(env)입니다. 바로 이 코드가 FilterLocalHost를 미들웨어로 만들어주는 핵심입니다.

미들웨어에서는 두 가지 동작 중 하나가 일어납니다:

  • 직접 응답을 반환하여 미들웨어 체인을 종료
  • @app.call(env)를 호출해 요청을 다음 미들웨어 또는 앱 자체로 전달

미들웨어를 포함한 Rack 앱의 어느 지점에서든 응답 내용을 수정할 수 있다는 점도 기억해 두세요.

예시:

class UpcaseAll
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)

    response.upcase!

    [status, headers, response]
  end
end

이것이 바로 Rack의 동작 방식입니다 🙂

마무리

이 글에서는 Ruby 웹 프레임워크와 서버 간의 상호작용을 가능하게 하는 인터페이스인 Rack에 대해 살펴봤습니다. 나아가 직접 Rack 애플리케이션을 작성하면서 그 동작 원리까지 이해할 수 있었습니다.

궁금한 점이나 피드백이 있다면 아래 댓글로 자유롭게 남겨주세요.

읽어주셔서 감사합니다!