Ruby가 훌륭한 언어로 꼽히는 이유 중 하나는 거의 모든 것을 개발자의 필요에 맞게 커스터마이징할 수 있다는 점입니다. 이는 유용하면서도 동시에 위험한 특성입니다. 잘못 사용하면 스스로 발등을 찍기 쉽지만, 신중하게 활용하면 놀랍도록 강력한 솔루션을 만들어낼 수 있습니다.
이 글에서는 Ruby가 객체를 생성하고 초기화하는 내부 동작을 살펴보고, 기본 동작을 어떻게 수정할 수 있는지 단계별로 알아보겠습니다.
클래스로 새 객체를 만드는 기본 원리
먼저 Ruby에서 객체를 생성하는 기본적인 방법부터 확인해 보겠습니다. 새로운 객체(또는 인스턴스)를 만들려면 클래스에 new를 호출합니다. 흥미로운 점은, 다른 언어와 달리 new가 언어 차원의 키워드가 아니라 일반적인 메서드처럼 호출된다는 사실입니다.
class Dog
end
object = Dog.new새로 생성된 객체를 커스터마이징하려면 new 메서드에 인수를 전달할 수 있습니다. 전달된 인수는 그대로 이니셜라이저(initializer)로 넘어갑니다.
class Dog
def initialize(name)
@name = name
end
end
object = Dog.new('Good boy')여기서도 마찬가지로, Ruby의 이니셜라이저는 특별한 문법이나 키워드가 아니라 그저 하나의 메서드일 뿐입니다.
이렇게 생각해 보면 어떨까요? 다른 Ruby 메서드를 자유롭게 다룰 수 있듯이, 이 메서드들 역시 마음대로 조작할 수 있지 않을까요? 물론 가능합니다!
단일 객체의 동작 수정하기
예를 들어, 특정 클래스의 모든 객체가 하위 클래스에서 메서드가 재정의되더라도 항상 로그를 출력하도록 보장하고 싶다고 가정해 봅시다. 한 가지 방법은 객체의 싱글톤 클래스(singleton class)에 모듈을 추가하는 것입니다.
module Logging
def make_noise
puts "Started making noise"
super
puts "Finished making noise"
end
end
class Bird
def make_noise
puts "Chirp, chirp!"
end
end
object = Bird.new
object.singleton_class.include(Logging)
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise이 예제에서는 Bird.new로 Bird 객체를 생성한 뒤, 해당 객체의 싱글톤 클래스를 통해 Logging 모듈을 포함시켰습니다.
싱글톤 클래스란 무엇일까?
Ruby는 특정 단일 객체에만 존재하는 메서드를 정의할 수 있도록 허용합니다. 이를 지원하기 위해 Ruby는 객체와 실제 클래스 사이에 익명 클래스를 하나 끼워 넣습니다. 메서드가 호출될 때 싱글톤 클래스에 정의된 메서드가 실제 클래스의 메서드보다 우선순위를 갖습니다. 싱글톤 클래스는 객체마다 고유하게 존재하므로, 여기에 메서드를 추가해도 같은 클래스의 다른 객체에는 전혀 영향을 주지 않습니다. 클래스와 객체에 대해 더 깊이 알고 싶다면 Programming Ruby 가이드를 참고하세요.
하지만 객체가 생성될 때마다 매번 싱글톤 클래스를 수정하는 것은 번거로운 작업입니다. 그래서 이번에는 Logging 모듈 포함 로직을 이니셜라이저로 옮겨, 생성되는 모든 객체에 자동으로 적용되도록 만들어 보겠습니다.
module Logging
def make_noise
puts "Started making noise"
super
puts "Finished making noise"
end
end
class Bird
def initialize
singleton_class.include(Logging)
end
def make_noise
puts "Chirp, chirp!"
end
end
object = Bird.new
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise이 방법도 잘 작동합니다. 하지만 Duck처럼 Bird의 하위 클래스를 만들면, Logging 동작을 유지하기 위해 이니셜라이저에서 반드시 super를 호출해야 합니다. 메서드를 재정의할 때마다 super를 올바르게 호출하는 것이 좋은 습관이라고 주장할 수도 있지만, 이번에는 필수가 아닌 방법을 찾아보겠습니다.
하위 클래스에서 super를 호출하지 않으면 Logging 모듈의 포함이 사라져 버립니다:
class Duck < Bird
def initialize(name)
@name = name
end
def make_noise
puts "#{@name}: Quack, quack!"
end
end
object = Duck.new('Felix')
object.make_noise
# Felix: Quack, quack!그 대신 Bird.new 자체를 재정의해 봅시다. 앞서 언급했듯이 new는 클래스에 구현된 그저 하나의 메서드입니다. 따라서 이를 재정의하고, super를 호출한 뒤, 새로 생성된 객체를 필요에 맞게 가공할 수 있습니다.
class Bird
def self.new(*arguments, &block)
instance = super
instance.singleton_class.include(Logging)
instance
end
end
object = Duck.new('Felix')
object.make_noise
# Started making noise
# Felix: Quack, quack!
# Finished making noise그런데 이니셜라이저 안에서 make_noise를 호출하면 어떻게 될까요? 안타깝게도 이 시점에는 싱글톤 클래스에 아직 Logging 모듈이 포함되지 않았기 때문에 원하는 출력을 얻을 수 없습니다.
다행히 해결책이 있습니다. allocate를 호출하면 기본적인 .new 동작을 처음부터 직접 구현할 수 있습니다.
class Bird
def self.new(*arguments, &block)
instance = allocate
instance.singleton_class.include(Logging)
instance.send(:initialize, *arguments, &block)
instance
end
endallocate를 호출하면 해당 클래스의 초기화되지 않은 새 객체가 반환됩니다. 덕분에 우리는 먼저 추가 동작을 싱글톤 클래스에 포함시키고, 그다음에야 객체의 initialize 메서드를 호출할 수 있습니다. (initialize는 기본적으로 private 메서드이므로, 호출하려면 send를 사용해야 합니다.)
Class#allocate의 숨겨진 진실
다른 메서드들과 달리 allocate는 재정의가 불가능합니다. Ruby는 내부적으로 allocate에 대해 일반적인 메서드 디스패치 방식을 사용하지 않기 때문입니다. 그 결과, new까지 함께 재정의하지 않고 allocate만 재정의하는 것은 아무 효과가 없습니다. 다만 allocate를 직접 호출하는 경우에는 Ruby가 재정의된 메서드를 호출해 줍니다. 자세한 내용은 Ruby 공식 문서의 Class#new와 Class#allocate 항목을 참고하세요.
굳이 이렇게까지 해야 할까?
많은 기술이 그렇듯, Ruby의 객체 생성 방식을 수정하는 것은 위험할 수 있으며 예상치 못한 곳에서 문제가 발생할 수도 있습니다.
그럼에도 불구하고 객체 생성 과정을 변경하는 것이 합리적인 선택인 경우가 실제로 존재합니다. 대표적인 예가 ActiveRecord입니다. ActiveRecord는 데이터베이스에서 객체를 불러올 때와 메모리에서 새 객체를 빌드할 때 초기화 과정이 다르기 때문에, init_from_db라는 별도의 메서드와 함께 allocate를 활용해 초기화 프로세스를 분기 처리합니다. 또한 becomes 메서드를 통해 서로 다른 단일 테이블 상속(STI) 타입 간에 레코드를 변환할 때도 allocate를 사용합니다.
무엇보다 중요한 것은, 객체 생성 과정을 직접 다뤄보면 Ruby 내부에서 이것이 어떻게 작동하는지 훨씬 깊이 이해하게 되고, 문제를 바라보는 시야가 넓어져 다양한 해결책을 떠올릴 수 있게 된다는 점입니다. 이 글이 즐거운 학습 경험이 되었기를 바랍니다.
Ruby의 기본 객체 생성 방식을 변경해서 구현해 본 것이 있다면 @AppSignal로 트윗해 의견을 공유해 주세요. 여러분의 사례를 기다립니다!