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

루비 URI.join이 직관적으로 작동하지 않는 이유

Honeybadger 팀이 또 하나의 이정표를 지났습니다. 드디어 영업 페이지가 메인 Rails 애플리케이션에서 분리된 것입니다. 몇 년 전부터 위시리스트에 올려두었던 작업이었지만, 우선순위가 밀려 쉽게 손대지 못하고 있었죠.

이 마이그레이션 작업 중에 특정 리다이렉트 링크를 만들기 위해 URI.join을 사용하게 되었습니다. 그런데 곧바로 문제에 부딪혔습니다. URI.join이 제가 기대한 방식으로 동작하지 않았던 것입니다.

저는 이 메서드가 여러 경로 조각을 받아 자연스럽게 이어 붙여 하나의 URL을 만들어 줄 것이라 생각했습니다.

# 기대했던 결과. 하지만 현실은 달랐습니다.
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/plans/change"

그러나 join이 실제로 반환한 값은 훨씬 이상했습니다. 경로 조각 하나가 통째로 사라지고, 마지막 조각인 "change"만 남은 것이죠.

# 실제 결과
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"

대체 왜 이런 결과가 나오는 걸까요?

흔한 오해

원인은 단순했습니다. 저는 URI.joinArray#join의 URL 전용 버전처럼, 조각들을 받아 하나의 완성된 URL로 합쳐 주기를 기대했던 것입니다.

하지만 실제로는 전혀 다른 일을 합니다.

join 메서드의 소스 코드를 보면, 인자들을 순회하면서 각각에 대해 merge를 호출할 뿐입니다.

# uri/rfc2396_parser.rb, 236행
def join(*uris)
  uris[0] = convert_to_uri(uris[0])
  uris.inject :merge
end

merge 메서드는 두 가지 작업을 수행합니다.

  1. "pages" 같은 문자열을 상대(relative) URI 객체로 변환합니다.
  2. 이 상대 URI를 기준(base) URI 위에서 해석하려고 시도합니다. 이 과정은 RFC2396 5.2절에 명시된 방식을 그대로 따릅니다.

좋습니다. 그런데 이것이 앞서 본 예상치 못한 동작을 어떻게 설명해 줄까요?

URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"

단계별로 따라가 보겠습니다. 위 코드는 사실 아래 코드와 동일합니다.

URI.parse("https://www.honeybadger.io/plans").merge("change")

즉, 상대 URI인 "change"를 절대 URI인 "https://www.honeybadger.io/plans"에 대해 해석하는 것입니다.

이때 RFC2396 5.2.6절의 규칙이 적용됩니다.

a) 기준 URI 경로의 마지막 세그먼트를 제외한 나머지 부분을 버퍼에 복사합니다. 다시 말해, 가장 오른쪽 슬래시 뒤의 모든 문자는 제외됩니다.

b) 참조(reference)의 경로를 버퍼 문자열 뒤에 덧붙입니다.

규칙대로 진행해 보면:

  1. 절대 URL에서 마지막 세그먼트("plans")를 제외하고 복사합니다. 결과는 "https://www.honeybadger.io/"입니다.
  2. 상대 경로 "change"를 덧붙입니다. 최종 결과는 "https://www.honeybadger.io/change"입니다.

이제야 세상이 다시 이치에 맞게 보입니다!

결론

URI.join으로 여러 경로 조각을 이어 URL을 만들 수는 있지만, 그것은 이 메서드의 설계 목적이 아닙니다. URI.join은 RFC 표준에 따라 URI를 재귀적으로 병합하는, 조금 더 복잡한 작업을 위해 만들어진 도구입니다.

새 영업 페이지로 리다이렉트할 URL을 만드는 제 목적에는 결국 Array#join이 더 잘 맞았습니다. :)

실용적인 팁

URL 경로를 안전하게 조합하고 싶다면 다음과 같은 방법들이 널리 쓰입니다.

  • 직접 헬퍼 작성: [base, *paths].join("/").squeeze("/")처럼 슬래시 중복을 정리하는 간단한 헬퍼를 만들어 쓰면 의도한 대로 동작합니다.
  • File.join 활용 시 주의: File.join/my//path 같은 이중 슬래시를 피할 수 있다는 장점이 있지만, 경로 구분자가 슬래시가 아닌 Windows 같은 환경에서는 결과가 깨질 수 있습니다.
  • Addressable 젬: Addressable::URI.join 역시 동일한 RFC 병합 규칙을 따르므로 결과는 비슷합니다. 대신 파싱과 검증 기능이 강력해 URL을 정교하게 다룰 때 유용합니다.