이 글에서는 Ruby에서 메서드 위임(method delegation)을 구현하는 다양한 방법을 소개합니다.
delegate 메서드, Forwardable 모듈, SimpleDelegator 클래스를 활용해 코드를 더 깔끔하고 유연하게 만드는 법을 함께 살펴보겠습니다.
왜 메서드 위임이 필요할까요?
객체지향 프로그래밍(OOP)에서 클래스들이 서로 협력하는 방식에는 크게 두 가지가 있습니다.
- 상속(Inheritance)
- 컴포지션(Composition)
상속(Inheritance)
상속은 부모 클래스가 자신의 메서드, 상수, 인스턴스 변수 정의를 자식 클래스에게 물려주는 계층 구조를 만드는 방식입니다.
예를 들어, Ruby의 모든 객체는 기본적으로 Object 클래스를 상속받습니다. 그렇기 때문에 puts, class, object_id 같은 메서드를 별도 정의 없이 바로 사용할 수 있는 것이죠.
컴포지션(Composition)
컴포지션은 한 클래스가 다른 클래스의 객체를 생성하거나 전달받아, 그 객체에게 작업을 맡기는 방식입니다.
예를 들어, 컴퓨터는 수많은 부품(객체)으로 이루어져 있고, 각 부품은 하나의 일을 잘 처리하도록 설계되어 있습니다.
화면에 무언가를 출력하고 싶다면 컴퓨터는 그래픽 카드에게 "무엇을" 보여줄지만 지시할 뿐, "어떻게" 렌더링할지는 관여하지 않습니다.
이것이 바로 컴포지션입니다!
그런데 만약 여러분이 컴퓨터 역할을 하는 클래스라면, 외부에서 그래픽 카드의 메서드에 접근할 수 있도록 하려면 어떻게 해야 할까요?
두 객체 사이에 일종의 "다리"를 놓아야 합니다. 이것이 바로 메서드 위임입니다.
Ruby 메서드 위임 예제
두 개의 클래스가 있다고 가정해 보겠습니다.
ComputerMemory
다음 코드를 살펴보세요:
class Computer
def initialize
@memory = Memory.new
end
end
class Memory
def initialize
@data = []
end
def write(data)
@data << data
end
def read(index)
@data[index]
end
end
computer = Computer.new
메모리에는 오직 Computer 객체를 통해서만 접근하고 싶습니다. 이런 설계를 원하는 이유는 다양합니다.
- 메모리 유닛이 하나뿐이라 메모리 객체도 단 하나만 존재해야 합니다. 접근을 하나의 객체로 집중시키면 이를 보장할 수 있습니다.
- 누가(어떤 애플리케이션이) 메모리에 접근하는지, 그리고 메모리의 어느 영역까지 사용할 수 있는지 제어하고 싶습니다.
- 보안 또는 디버깅 목적으로 모든 메모리 접근을 기록(log)하고 싶습니다.
그런데 이렇게 호출하면 어떻게 될까요?
computer.write("rubyguides")
Computer 클래스에는 write 메서드가 없기 때문에 NoMethodError가 발생합니다.
undefined method `write' for #<Computer:0x000055c2e8f7d310> (NoMethodError)
Computer는 write가 무엇을 의미하는지 모르는 상태입니다. 하지만 직접 write 메서드를 만들어 주면 어떨까요?
방법은 다음과 같습니다:
class Computer
def initialize
@memory = Memory.new
end
def write(data)
@memory.write(data)
end
def read(index)
@memory.read(index)
end
end
요청을 @memory 객체에 그대로 전달하고 있습니다. 이것이 바로 메서드 위임입니다.
참고: 이렇게 하면 해당 메서드들이 클래스의 공개 인터페이스(public interface)에 포함되어 누구나 호출할 수 있게 됩니다. 항상 원하는 동작은 아닐 수 있다는 점을 기억하세요.
그렇다면 메서드 위임을 더 간편하게 처리할 지름길은 없을까요?
물론 있습니다! 하나씩 살펴보겠습니다.
Forwardable 모듈 사용하기
Ruby에 기본 포함된 Forwardable 모듈을 사용하면 위임 메서드를 손쉽게 정의할 수 있습니다.
사용 방법은 다음과 같습니다:
require 'forwardable'
class Computer
extend Forwardable
def_delegators :@memory, :read, :write
def initialize
@memory = Memory.new
end
end
앞서 직접 작성했던 write, read 메서드를 삭제해도 완전히 동일하게 동작합니다.
Forwardable은 메서드 인자는 물론 블록(block)까지 알아서 전달해 주기 때문에 신경 쓸 필요가 없습니다.
정말 편리하지 않나요? 🙂
Rails의 delegate 메서드 사용하기
Rails를 사용 중이거나 ActiveSupport 젬(gem)만 단독으로 불러오는 경우라면 delegate 메서드를 사용할 수 있습니다.
동작 방식은 다음과 같습니다:
class Computer
delegate :read, :write, to: :@memory
def initialize
@memory = Memory.new
end
end
delegate 메서드는 prefix 옵션을 지원합니다. 이를 사용하면 메서드 이름 앞에 접두사를 붙일 수 있어 이름 충돌을 피하고 의미를 명확히 할 수 있습니다.
예제:
class Computer
delegate :read, :write, prefix: "memory", to: :@memory
def initialize
@memory = Memory.new
end
end
결과적으로 두 개의 메서드가 생성됩니다:
memory_readmemory_write
SimpleDelegator로 모든 메서드 위임하기
지금까지 살펴본 기법들은 특정 메서드만 골라서 전달하는 방식이었습니다.
하지만 객체 전체를 감싸서(wrap) 그 객체의 모든 메서드를 그대로 노출하고 싶다면 어떻게 해야 할까요?
이럴 때 SimpleDelegator 클래스를 사용하면 됩니다!
사용 예시:
require 'delegate' class CoolArray < SimpleDelegator end cool = CoolArray.new([]) cool << "ruby" cool << "gems" p cool
이제 CoolArray는 new에 전달한 객체처럼 동작합니다.
이것이 왜 유용할까요? 원본 클래스를 수정하지 않고도 새로운 메서드를 추가해 기능을 확장할 수 있기 때문입니다. 기존 배열의 모든 기능을 그대로 유지하면서 자신만의 로직을 얹을 수 있는 것이죠.
마무리
이번 글에서는 Ruby에서의 객체 컴포지션 개념과, 다양한 기법을 활용한 메서드 위임 방법을 배웠습니다.
- 직접 위임: 래퍼 메서드를 직접 작성하는 가장 기본적인 방식
- Forwardable: Ruby 표준 라이브러리로 특정 메서드를 간결하게 위임
- delegate (Rails/ActiveSupport): prefix 옵션으로 더 명확한 인터페이스 설계 가능
- SimpleDelegator: 객체 전체를 감싸 모든 메서드를 위임
이 글이 도움이 되었다면 Ruby를 좋아하는 친구들에게 공유해 주세요.
읽어주셔서 감사합니다 🙂