루비(Ruby)의 클래스 변수는 악명이 높습니다. 숙련된 루비 개발자조차 그 동작을 직관적으로 예측하기 어려워합니다. 가장 대표적인 예는 상속과 관련이 있습니다.
class Fruit
@@kind = nil
def self.kind
@@kind
end
end
class Apple < Fruit
@@kind = "apple"
end
Apple.kind
# => "apple"
Fruit.kind
# => "apple"
자식 클래스에서 kind 변수를 변경했을 뿐인데 부모 클래스의 값까지 함께 바뀌어 버립니다. 상당히 엉망인 구조죠. 하지만 이는 언어가 원래 그렇게 동작하도록 만들어진 것입니다. 오래전 Smalltalk를 모방하기 위해 내려진 설계 결정의 결과입니다.
상황은 더 이상합니다
클래스 변수의 기묘한 동작 중에는 아키텍처적 설계 선택이라기보다 구현 과정에서 생긴 특수한 성질에 가까운 사례들도 있습니다. 이번 글에서는 그중에서도 특히 흥미로운 한 가지를 소개하려 합니다.
지금부터 두 개의 코드 조각을 비교해 보겠습니다. 겉보기에는 완전히 동일한 결과를 낼 것 같지만, 실제로는 전혀 다르게 동작합니다.
첫 번째 예제에서는 클래스 변수를 하나 설정하고, 그 값을 반환하는 메서드를 만듭니다. 여기에는 아무런 특별한 장치도 없으며, 모든 것이 예상대로 잘 동작합니다.
class Foo
@@val = 1234
# 클래스 메서드를 선언하는 축약 표현입니다.
class << self
def val
@@val
end
end
end
Foo.val
# => 1234
아마 모르셨을 수도 있는데, class << self는 반드시 클래스 정의 내부에 있어야 하는 것은 아닙니다. 아래 예제에서는 이를 클래스 정의 바깥으로 옮겼습니다. 클래스 메서드는 정상적으로 추가되지만, 정작 클래스 변수에는 접근할 수 없습니다.
class Bar
@@val = 1234
end
class << Bar
def val
@@val
end
end
Bar.val
# warning: class variable access from toplevel
# NameError: uninitialized class variable @@val in Object
메서드 안에서 클래스 변수에 접근하려는 순간 경고와 예외가 발생합니다. 대체 무슨 일이 벌어진 걸까요?
어휘적 스코프의 등장
루비의 기묘하고 혼란스러운 동작 가운데 99%가 어휘적 스코프(lexical scope)에서 비롯된다는 생각이 들 때가 많습니다.
이 용어가 낯설다면 간단히 설명하겠습니다. 어휘적 스코핑(lexical scoping)이란 코드가 작성된 물리적인 위치를 기준으로 요소들을 묶는 방식을 말합니다. 추상적인 객체 모델에서 논리적으로 어디에 속하는지와는 무관합니다. 역시 예제를 보는 것이 가장 빠릅니다.
class B
# x와 y는 같은 어휘적 스코프를 공유합니다.
x = 1
y = 1
end
class B
# z는 같은 클래스 안에 있지만 x, y와는 다른 어휘적 스코프를 가집니다.
z = 3
end
클래스는 어휘적으로 결정된다
그렇다면 앞서 본 클래스 변수 예제에서는 어휘적 스코프가 어떻게 개입하는 걸까요?
클래스 변수를 읽어오려면 루비는 먼저 어떤 클래스에서 값을 가져올지 판단해야 합니다. 이때 루비는 어휘적 스코핑을 이용해 대상 클래스를 찾습니다.
정상 동작하는 첫 번째 예제를 자세히 보면, 클래스 변수에 접근하는 코드가 물리적으로 클래스 정의 내부에 위치해 있음을 알 수 있습니다.
class Foo
class << self
def val
# 저는 Foo에 어휘적으로 스코프되어 있습니다!
@@val
end
end
end
반면 실패하는 두 번째 예제에서는 클래스 변수에 접근하는 코드가 해당 클래스에 어휘적으로 스코프되어 있지 않습니다.
class << Bar
def val
# 이곳 어디에도 Bar는 보이지 않습니다.
@@val
end
end
그렇다면 이 코드는 대체 어디에 스코프되어 있는 걸까요? 루비가 출력한 경고 메시지가 결정적인 단서를 제공합니다. 바로 warning: class variable access from toplevel이라는 문구입니다.
실패하는 예제에서 클래스 변수는 사실 최상위 객체(toplevel object)에 어휘적으로 스코프되어 있었습니다. 이로 인해 상당히 기괴한 동작이 발생할 수 있습니다.
예를 들어, main 컨텍스트에 어휘적으로 스코프된 코드에서 클래스 변수를 설정하려고 하면, 그 변수는 의도한 클래스가 아니라 Object에 설정되어 버립니다.
class Bar
end
class << Bar
def val=(n)
# 이 코드는 최상위 객체에 어휘적으로 스코프되어 있습니다.
# 따라서 @@val은 Bar가 아니라 Object에 설정됩니다.
@@val = n
end
end
Bar.val = 100
# 믿기 힘드시죠?
Object.class_variables
# => [:@@val]
추가 예제
클래스의 어휘적 스코프 밖에서 클래스 변수를 참조하는 방법은 생각보다 다양합니다. 그리고 그 어떤 방법도 좋은 결과를 낳지 못합니다.
몇 가지 예제를 직접 확인해 보겠습니다.
class Foo
@@foo = :foo
end
# 이 코드는 동작하지 않습니다.
Foo.class_eval { puts @@foo }
# 이것도 마찬가지입니다.
Foo.send :define_method, :x do
puts @@foo
end
# ..혹시 모듈을 활용할 생각은 아니었겠죠?
module Printable
def foo
puts @@foo
end
end
class Foo
@@foo = :foo
include Printable
end
Foo.new.foo
그렇다면 대안은?
클래스 수준의 상태를 저장해야 한다면 클래스 변수 대신 클래스 인스턴스 변수(클래스 메서드 안에서 @val처럼 사용)을 쓰는 것이 일반적인 권장 사항입니다. 클래스 인스턴스 변수는 각 클래스 객체에 독립적으로 저장되므로, 상속 계층 전체가 값을 공유하는 클래스 변수와 달리 훨씬 예측 가능한 동작을 보장합니다.
이제 왜 많은 루비 개발자들이 "루비에서는 클래스 변수를 절대 사용하지 마라"라고 입을 모아 말하는지 이해하게 되셨기를 바랍니다. :)