Ruby에서 같은 메서드가 다르게 불릴 때, owner와 ancestors부터 보기

루비색과 크림색 층 위를 빛나는 경로가 따라가는 메서드 탐색 개념 일러스트

메서드 이름은 같은데 예상한 코드가 실행되지 않을 때가 있다. 클래스 정의를 열어보면 그 이름이 분명히 있고, 다른 파일에도 같은 이름이 보인다. 어느 쪽을 고칠지 결정하기 전에 실제 호출이 어디에 도착하는지 확인할 수 있다. Ruby에서는 메서드의 소유자와 탐색 경로를 나란히 보는 것이 작은 출발점이 된다.

S M Tahosin의 메서드 탐색 설명 글을 읽으며 이 질문을 떠올렸다. 이 글은 원문의 모든 설명을 옮긴 요약이 아니다. Ruby의 Module 문서와 짧은 자체 예제를 참고해, 실제로 확인한 호출만 설명한다. 실행 환경은 로컬 Ruby 4.0.7이다.

이름이 아니라 도착한 곳을 보기

아래 예제에는 기본 문구를 돌려주는 모듈과 그 앞에 문구를 붙이는 모듈이 있다. Card 클래스는 하나를 include하고 다른 하나를 prepend한다. 코드의 목적은 업무에 쓸 카드 기능을 만드는 것이 아니라 호출 순서를 눈으로 구분하는 것이다.

module BaseLabel
  def label; "base"; end
end
module Stamp
  def label; "stamp -> #{super}"; end
end
class Card
  include BaseLabel
  prepend Stamp
end
x = Card.new
puts x.method(:label).owner
puts x.label

이 예제를 실행한 결과 owner는 Stamp였고, 반환된 문구는 stamp → base의 순서를 보였다. 코드 문자열의 화살표 표기는 ASCII이지만 여기서는 읽기 쉽게 표시했다. Stamp에서 시작한 뒤 super가 다음 구현으로 이어진 결과다. 같은 이름을 검색해서 발견한 첫 파일만으로 호출 대상을 판단할 수 없는 이유가 드러난다.

Card.ancestors도 함께 확인했다. 결과는 Stamp, Card, BaseLabel, Object, Kernel, BasicObject 순서였다. 이 예제에서 prepend한 모듈은 Card 앞에, include한 모듈은 뒤에 놓였다. 이 경로는 지금의 정의에 대한 관찰이며 모든 라이브러리의 내부 동작을 대신 설명하는 목록은 아니다.

한 객체만 달라지는 경우

이번에는 x에만 label을 정의해보자. 이미 만든 모든 카드의 동작을 바꾸려는 것이 아니라, 객체 하나에 메서드를 추가하는 예제다. 실제 실행 파일에서는 앞의 예제에 이어 아래 정의를 넣었다.

def x.label; "one card"; end
puts x.label
puts Card.new.label

출력은 차례로 one card와 stamp → base였다. x의 메서드 소유자는 x의 싱글턴 클래스였고, 새 Card 객체는 앞의 모듈 경로를 따랐다. 클래스의 ancestors만 보고서는 객체 하나에 생긴 차이를 놓칠 수 있다. 필요하면 x.singleton_class.ancestors도 확인할 수 있다.

이 차이는 수정 범위를 정할 때 중요하다. 특정 객체의 동작만 다르다면 클래스 파일 전체를 고쳐 해결하려는 시도가 너무 넓을 수 있다. 반대로 여러 객체가 공통으로 달라졌다면 포함된 모듈이나 상속 관계를 살펴볼 이유가 생긴다. 아직 원인을 보지 않았는데 이름이 같은 메서드를 모두 고칠 필요는 없다.

관찰 도구도 답하는 질문이 다르다

method(:label).owner는 현재 해석된 메서드가 어디에서 왔는지 알려준다. ancestors는 탐색에 관련된 조상 경로를 보여준다. 둘을 함께 보면 메서드 하나의 도착점과 그 주변 구조를 연결할 수 있다. 다만 경로를 보았다는 사실만으로 그 메서드가 올바른 값을 계산하는지까지 알 수는 없다.

super를 쓴 구현에서는 다음 구현과 주고받는 인자와 반환값도 확인해야 한다. 앞에 붙은 모듈 하나가 일을 마친 뒤 다음 호출을 하지 않을 수도 있다. 이름이 같은 함수가 여러 개 있다는 사실만으로 모두 실행된다고 생각하면 안 된다. 이 글의 예제는 의도적으로 단순하게 이어지도록 만들었다.

프레임워크의 동적 메서드 생성이나 method_missing, refinement가 얽힌 상황은 추가 확인이 필요하다. 이 예제에서는 그런 기능을 사용하지 않았다. 작은 예제의 결과를 복잡한 서비스 전체의 호출 보증으로 넓히지 않는 것이 좋다. 실제 문제에서는 문제를 재현하는 객체와 호출을 기준으로 관찰해야 한다.

실행 환경의 버전도 기록에 붙인다. 언어 문서와 현재 실행기가 다르면 버전 차이를 살펴볼 수 있기 때문이다. 여기서 사용한 4.0.7이라는 버전은 독자에게 업그레이드를 권하는 뜻이 아니다. 제시한 출력이 어느 환경에서 나왔는지 밝히는 정보다.

작은 수정의 영향 범위를 살피는 일에 이 관찰을 붙여볼 수 있다. 호출 대상이 보이면 고쳐야 할 곳과 그대로 둘 곳을 조금 더 구체적으로 정할 수 있다. 그 뒤에도 원하는 동작의 재현과 수정 후 확인은 별도로 필요하다.

다음에 메서드 이름이 여러 파일에서 발견된다면 곧바로 가장 그럴듯한 파일을 열어 수정하기 전에 한 번 물어보자. 지금 이 객체의 호출은 어디로 가는가. owner 한 줄과 짧은 출력 하나가, 검색 결과 여러 페이지보다 먼저 방향을 잡아줄 때가 있다.

공개 원문을 참고해 AI의 도움으로 구성·작성한 글입니다. 가상 예시는 실제 수행 결과와 구분했으며, 표지는 Google Gemini로 만든 개념 일러스트입니다.