Rack::BodyProxy의 등장과 한계
Rack 미들웨어는 응답 본문이 스트리밍될 때 본문 내용 생성 전에 #call이 반환되어 후처리 작업이 어려웠습니다. 이를 해결하기 위해 Rack 명세에 #close 훅이 추가되었고, Rack::BodyProxy가 도입되었습니다.
BodyProxy는 본문 객체를 래핑하여 #close 호출 시 등록된 콜백을 실행, 본문 반복 후 작업을 수행하게 했습니다. 그러나 BodyProxy는 다음과 같은 한계를 가집니다.
- 객체 할당 오버헤드: 미들웨어마다 BodyProxy 객체를 중첩 할당하여 가비지 컬렉션 부하를 증가시키고 성능 병목을 유발합니다.
- 실행 시점 문제: BodyProxy는 본문 반복 직후 실행되지만, 웹 서버와 리버스 프록시 간의 연결이 완전히 닫히기 전일 수 있습니다. 이로 인해 지표 전송 같은 작업이 연결을 불필요하게 유지시켜 성능 저하를 초래하는 사례가 있었습니다 (GitHub, Shopify).
rack.response_finished의 부상
BodyProxy의 한계를 극복하기 위해 rack.after_reply 개념이 Puma에 2011년 도입되었고, 이후 Unicorn에도 추가되었습니다. 이는 Rack 3 SPEC의 선택적 부분인 rack.response_finished로 표준화되었습니다. 웹 서버는 env[“rack.response_finished”]를 통해 지원을 알리고, 미들웨어는 여기에 콜백을 등록하여 응답 본문 전송 및 연결 종료 후 작업을 수행할 수 있습니다.
rack.response_finished 콜백은 (env, status, headers, error) 인자를 받으며, BodyProxy와 달리 객체 할당 없이 후처리 작업이 가능합니다. 초기에는 채택이 더뎠지만, Falcon과 Pitchfork에 이어 Rails Event Reporter PR을 통해 ActionDispatch::Executor에 rack.response_finished 지원이 추가되면서 전환점을 맞았습니다. 이는 요청 컨텍스트를 정확한 시점에 클리어하고 요청 요약 로깅 등을 효율적으로 수행하게 했습니다.
현재 Rack 3.2에서는 Rack::ConditionalGet과 Rack::Head에서 BodyProxy가 제거되었으며, Rack::TempfileReaper, ActiveSupport::Cache::Strategy::LocalCache, 그리고 Puma에서도 rack.response_finished 지원이 활발히 추가되고 있습니다.