오늘은 Ruby의 self에 대해 이야기해 보려고 합니다. Ruby로 프로그래밍을 한 지 어느 정도 된 개발자라면 이미 self의 개념이 몸에 배어 있을 것입니다. 코드를 읽거나 작성할 때마다 self는 늘 머릿속 어딘가에 존재하죠.
하지만 경험이 적은 Rubyist에게 self는 다소 혼란스러운 존재입니다. 값은 끊임없이 바뀌는데도 코드에는 명시적으로 드러나지 않으며, 그저 알고 있으라고 기대하기 때문입니다.
사실 초보자가 겪는 많은 문제는 self에 대한 이해 부족에서 비롯됩니다. 인스턴스 변수가 어디선가 "사라졌다"고 느낀 적이 있거나, 믹스인(mixin)에서 어떤 데이터가 보이는지 헷갈린 적이 있다면, 그 순간의 self를 제대로 파악하지 못했기 때문일 가능성이 큽니다.
이 글에서는 일상적인 다양한 상황 속에서 self가 어떻게 동작하는지 하나씩 살펴보겠습니다.
self란 무엇인가?
"Ruby에서는 모든 것이 객체다"라는 말을 들어본 적이 있을 겁니다. 그 말이 사실이라면, 우리가 작성하는 모든 코드는 결국 어떤 객체에 "속해 있다"는 뜻이 됩니다.
self는 현재 실행 중인 코드를 "소유하고 있는" 객체를 가리키는 특별한 변수입니다. Ruby는 거의 모든 곳에서 self를 활용합니다:
- 인스턴스 변수(
@myvar)를 참조할 때 - 메서드와 상수를 탐색(lookup)할 때
- 메서드, 클래스, 모듈을 정의할 때
이론상으로는 self가 꽤 단순해 보입니다. 하지만 실전에서는 까다로운 상황이 자주 발생하곤 하죠. 그래서 이 글을 준비했습니다.
self 예제
이제 몇 가지 예제를 단계별로 살펴보겠습니다. 앞부분이 너무 기초적이라고 느껴지더라도 끝까지 읽어 주세요. 점점 고급 주제로 넘어갑니다.
인스턴스 메서드 내부에서
아래 코드에서 reflect는 인스턴스 메서드입니다. 이 메서드는 Ghost.new로 생성한 객체에 속하므로, self는 그 객체를 가리킵니다.
class Ghost
def reflect
self
end
end
g = Ghost.new
g.reflect == g # => true
클래스 메서드 내부에서
이번에는 reflect가 Ghost의 클래스 메서드입니다. 클래스 메서드는 클래스 자체가 메서드를 "소유"하며, self는 그 클래스를 가리킵니다.
class Ghost
def self.reflect
self
end
end
Ghost.reflect == Ghost # => true
모듈 안의 "클래스" 메서드도 마찬가지로 동작합니다. 예를 들면 다음과 같습니다:
module Ghost
def self.reflect
self
end
end
Ghost.reflect == Ghost # => true
Ruby에서는 클래스와 모듈 역시 객체로 취급된다는 점을 기억하세요. 따라서 이 동작은 첫 번째 예제의 인스턴스 메서드 동작과 크게 다르지 않습니다.
클래스 또는 모듈 정의 내부에서
Rails 같은 프레임워크와 Ruby가 잘 맞는 이유 중 하나는, 클래스와 모듈 정의 내부에서 임의의 코드를 실행할 수 있기 때문입니다. 클래스/모듈 정의 안에 넣은 코드는 다른 Ruby 코드처럼 그대로 실행되며, 유일한 차이는 self의 값뿐입니다.
아래에서 볼 수 있듯이, self는 현재 정의 중인 클래스 또는 모듈을 가리킵니다.
class Ghost
self == Ghost # => true
end
module Mummy
self == Mummy # => true
end
믹스인 메서드 내부에서
믹스인된 메서드도 self 관점에서는 일반적인 인스턴스 메서드나 클래스 메서드와 똑같이 동작합니다. 당연한 결과입니다. 그렇지 않다면 믹스인이 해당 클래스와 상호작용할 방법이 없겠죠.
인스턴스 메서드
reflect 메서드가 모듈 안에서 정의되었더라도, 그 self는 해당 모듈이 포함(include)된 클래스의 인스턴스입니다.
module Reflection
def reflect
self
end
end
class Ghost
include Reflection
end
g = Ghost.new
g.reflect == g # => true
클래스 메서드
extend를 사용해 클래스에 클래스 메서드를 믹스인하면, self는 일반 클래스 메서드에서와 완전히 동일하게 동작합니다.
module Reflection
def reflect
self
end
end
class Ghost
extend Reflection
end
Ghost.reflect == Ghost # => true
메타클래스 내부에서
클래스 메서드를 한꺼번에 여러 개 정의할 때 흔히 사용하는 아래와 같은 축약 문법을 본 적이 있을 겁니다.
class Ghost
class << self
def method1
end
def method2
end
end
end
class << foo 문법은 사실 꽤 흥미로운 기능입니다. 이 문법을 사용하면 객체의 메타클래스(metaclass), 즉 "싱글턴 클래스(singleton class)" 또는 "이젠 클래스(eigenclass)"라고 불리는 것에 접근할 수 있습니다. 메타클래스에 대해서는 다음 글에서 더 깊이 다룰 계획입니다. 지금은 메타클래스가 특정 객체에만 속하는 메서드들이 저장되는 장소라는 것만 알아두면 충분합니다.
class << foo 블록 안에서 self에 접근하면 메타클래스를 얻게 됩니다.
class << "test"
puts self.inspect
end
# => #<Class:#<String:0x007f8de283bd88>
클래스 밖에서
클래스 밖에서 코드를 실행할 때도 Ruby는 여전히 self를 제공합니다. 이때 self는 Object의 인스턴스인 "main"을 가리킵니다:
puts self.inspect # => main
마치며
self는 눈에 보이지 않지만 Ruby 코드의 모든 곳에서 작동하는 핵심 개념입니다. 코드를 작성할 때 "지금 이 코드는 누구의 것인가?"를 항상 스스로에게 물어보는 습관을 들이면, 인스턴스 변수가 사라지는 문제나 믹스인 관련 혼란을 대부분 피할 수 있습니다. 다양한 상황에서 self가 무엇을 가리키는지 puts self.inspect로 직접 확인해 보는 것도 좋은 연습 방법입니다.