Ruby Magic의 새로운 에피소드에 오신 것을 환영합니다! 이번 호에서는 두 개발자(안녕하세요, Maud!) 사이의 논쟁에서 출발한 주제인 메타클래스(metaclass)를 다룹니다.
메타클래스를 살펴보는 과정에서 Ruby에서 클래스 메서드와 인스턴스 메서드가 실제로 어떻게 동작하는지 이해하게 될 것입니다. 아울러 명시적인 'definee'를 전달해 메서드를 정의하는 방식과 class << self, instance_eval을 사용하는 방식의 차이점도 함께 알아보겠습니다. 그럼 시작해볼까요?
클래스 인스턴스와 인스턴스 메서드
Ruby에서 메타클래스가 필요한 이유를 이해하려면 먼저 인스턴스 메서드와 클래스 메서드의 차이부터 짚고 넘어가야 합니다.
Ruby에서 클래스는 다른 객체를 만들기 위한 설계도를 담은 객체입니다. 클래스는 해당 클래스의 모든 인스턴스에서 사용할 수 있는 메서드를 정의합니다.
클래스 내부에 메서드를 정의하면 그 클래스에 인스턴스 메서드가 생깁니다. 이후 생성되는 해당 클래스의 모든 인스턴스는 이 메서드를 사용할 수 있습니다.
class User
def initialize(name)
@name = name
end
def name
@name
end
end
user = User.new('Thijs')
user.name # => "Thijs"이 예제에서는 사용자의 이름을 반환하는 #name이라는 인스턴스 메서드를 가진 User 클래스를 만듭니다. 그다음 이 클래스로 인스턴스를 생성해 user 변수에 저장합니다. user는 User 클래스의 인스턴스이므로 #name 메서드를 호출할 수 있습니다.
클래스는 자신의 인스턴스 메서드들을 메서드 테이블(method table)에 저장합니다. 해당 클래스의 모든 인스턴스는 자신이 속한 클래스의 메서드 테이블을 참조해 인스턴스 메서드에 접근합니다.
클래스 객체
클래스 메서드는 인스턴스를 따로 만들지 않고도 클래스에서 바로 호출할 수 있는 메서드입니다. 메서드 이름 앞에 self.를 붙여 정의하면 클래스 메서드가 됩니다.
클래스 자체도 하나의 객체입니다. 상수는 클래스 객체를 가리키므로, 클래스에 정의된 클래스 메서드는 애플리케이션 어디에서든 호출할 수 있습니다.
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]중요한 점은, self. 접두사로 정의된 메서드는 클래스의 메서드 테이블에 추가되지 않는다는 것입니다. 대신 그 클래스의 메타클래스에 추가됩니다.
메타클래스란 무엇인가?
Ruby의 모든 객체는 자신만의 숨겨진 메타클래스를 가지고 있습니다. 메타클래스는 싱글턴(singleton)이므로 단 하나의 객체에만 속합니다. 한 클래스의 인스턴스를 여러 개 만들면 이들은 같은 클래스를 공유하지만, 각각 서로 다른 메타클래스를 가집니다.
thijs, robert, tom = User.all
thijs.class # => User
robert.class # => User
tom.class # => User
thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>>
robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>>
tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>>이 예제에서 세 객체 모두 User 클래스에 속해 있지만, 각자의 싱글턴 클래스는 서로 다른 객체 ID를 가집니다. 즉, 각각 별개의 객체라는 의미입니다.
메타클래스에 접근할 수 있기 때문에 Ruby에서는 이미 존재하는 객체에 메서드를 직접 추가할 수 있습니다. 이렇게 해도 해당 객체의 클래스에는 아무런 변화가 없습니다.
robert = User.new("Robert")
def robert.last_name
"Beekman"
end
robert.last_name # => "Beekman"
User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>)이 예제에서는 robert 변수에 담긴 사용자 객체에 #last_name 메서드를 추가했습니다. robert가 User의 인스턴스임에도 불구하고, 새로 생성되는 User 인스턴스들은 #last_name 메서드를 사용할 수 없습니다. 이 메서드는 오직 robert의 메타클래스에만 존재하기 때문입니다.
그렇다면 self란 무엇일까?
메서드를 정의할 때 리시버(receiver)를 전달하면, 새 메서드는 클래스의 메서드 테이블이 아니라 그 리시버의 메타클래스에 추가됩니다.
tom = User.new("Tom")
def tom.last_name
"de Bruijn"
end위 예제에서는 메서드를 정의할 때 tom을 리시버로 전달해, #last_name 메서드를 tom 객체에 직접 추가했습니다.
클래스 메서드도 정확히 같은 원리로 동작합니다.
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end여기서는 .all 메서드를 정의할 때 self를 명시적인 리시버로 전달했습니다. 클래스 정의 본문 안에서 self는 그 클래스 자체(이 경우 User)를 가리키므로, .all 메서드는 User의 메타클래스에 추가됩니다.
User는 상수에 저장된 객체이므로, 이를 참조할 때마다 항상 같은 객체—그리고 같은 메타클래스—에 접근하게 됩니다.
메타클래스 열기
지금까지 클래스 메서드가 곧 클래스 객체의 메타클래스에 정의된 메서드라는 사실을 배웠습니다. 이 개념을 바탕으로, 코드에서 종종 마주치는 몇 가지 다른 클래스 메서드 정의 기법을 살펴보겠습니다.
class << self
요즘은 사용 빈도가 줄었지만, 일부 라이브러리에서는 class << self 구문으로 클래스 메서드를 정의합니다. 이 문법은 현재 클래스의 메타클래스를 열어 그 안에서 직접 작업할 수 있게 해주는 트릭입니다.
class User
class << self
self # => #<Class:User>
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]이 예제는 User의 메타클래스에 메서드를 추가함으로써 User.all이라는 클래스 메서드를 만듭니다. 앞서 본 것처럼 메서드마다 리시버를 명시적으로 전달하는 대신, self가 User 자신이 아닌 User의 메타클래스를 가리키도록 바꾸는 방식입니다.
앞에서 배웠듯이, 명시적인 리시버 없이 정의된 메서드는 항상 현재 컨텍스트의 클래스에 인스턴스 메서드로 추가됩니다. 이 블록 안에서 현재 클래스는 User의 메타클래스(#<Class:User>)입니다.
instance_eval
또 다른 방법은 instance_eval을 사용하는 것입니다. 동작 자체는 비슷하지만 한 가지 중요한 차이가 있습니다. 블록 안에서 정의된 메서드는 클래스의 메타클래스에 추가되지만, self는 여전히 원래의 클래스를 가리킵니다.
class User
instance_eval do
self # => User
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]이 예제에서도 이전과 마찬가지로 User의 메타클래스에 인스턴스 메서드를 정의하지만, self는 끝까지 User를 가리킵니다. 대부분의 경우 둘은 같은 객체를 가리키지만, '기본 definee(default definee)'와 self는 서로 다른 객체를 가리킬 수도 있다는 점을 기억해두세요.
마치며
이번 글에서 우리는 메서드를 가질 수 있는 대상의 정체와, 인스턴스 메서드가 실제로는 객체의 메타클래스에 정의된 메서드라는 사실을 확인했습니다. class << self는 단순히 self를 메타클래스로 교체해 메서드를 정의할 수 있게 해주는 문법이고, instance_eval도 self를 건드리지 않는다는 점만 빼면 거의 같은 일을 한다는 것도 알게 되었습니다.
메타클래스를 명시적으로 다룰 일은 흔치 않지만, Ruby는 내부적으로 메타클래스를 폭넓게 활용합니다. 메서드를 정의할 때 내부에서 어떤 일이 벌어지는지 이해하면, Ruby가 왜 그렇게 동작하는지—그리고 왜 클래스 메서드를 정의할 때 self. 접두사를 붙여야 하는지—명확하게 파악할 수 있습니다.
읽어주셔서 감사합니다. 이 글이 도움이 되셨다면, 월 1회 발행되는 새로운 글을 이메일로 받아볼 수 있도록 Ruby Magic을 구독해 보세요.