Ruby에서 상수란 무엇일까요?
상수는 항상 대문자로 시작하는 변수의 한 종류입니다. 메타프로그래밍을 사용하지 않는 한, 메서드 외부에서만 정의할 수 있습니다.
상수는 원래 변경되지 않아야 하는 값을 담기 위해 사용되지만, 흥미롭게도 Ruby는 상수 변경 자체를 막지는 않습니다.
상수는 다음과 같이 생겼습니다:
FRUIT = "orange"
그렇다면 지금부터, 상수는 매우 중요한 주제이므로 이 글에서 하나씩 자세히 살펴보겠습니다!
상수 정의 방법
상수를 선언하는 데에는 특별한 기호나 복잡한 문법이 필요하지 않습니다. 단지 첫 글자를 대문자로 쓰기만 하면 됩니다.
다음은 모두 유효한 상수입니다:
ABC = 1 Goo = 2 Foo = 3
주목할 점은 메서드 내부에서는 상수를 정의할 수 없다는 것입니다.
메서드 안에 정의하면 다음과 같은 애매한 에러 메시지가 출력됩니다:
def the_method ABC = 1 end # "dynamic constant assignment"
따라서 상수는 반드시 메서드 외부에 정의해야 합니다. 일반적으로는 클래스 최상단에 상수 정의를 모아두면 코드를 읽는 사람이 한눈에 파악할 수 있어 좋습니다.
class RubyBlog URL = "rubyguides.com" AUTHOR = "Jesus Castello" end
이렇게 정의한 상수는 클래스 메서드 내부에서 사용하거나, 클래스 외부에서는 아래와 같은 문법으로 접근할 수 있습니다:
p RubyBlog::AUTHOR # "Jesus Castello"
상수의 스코프에 대해서는 뒤에서 더 자세히 다룰 예정이니 끝까지 읽어주세요!
Uninitialized Constant(초기화되지 않은 상수) 에러
Ruby 개발 중 자주 마주치게 되는 대표적인 에러가 바로 이것입니다:
puts Foo # "uninitialized constant Foo (NameError)"
이 에러를 머릿속에서 "상수를 찾을 수 없음"이라고 번역하면 이해하기 쉬워집니다.
이 에러를 제대로 이해하기 위해 알아야 할 중요한 사실이 있습니다. 바로 Ruby의 클래스 자체가 상수라는 점입니다.
예시:
Array String Hash
이들은 모두 첫 글자가 대문자이기 때문에 상수입니다.
클래스를 정의한다는 것은 사실 Class 객체를 생성해서 상수에 할당하는 작업입니다. 그리고 그 상수가 곧 클래스 이름이 되는 것이죠.
왜 이것이 중요할까요?
"uninitialized constant" 에러가 발생하는 가장 흔한 원인은 해당 상수를 정의하고 있는 파일이나 gem을 require하는 것을 잊었기 때문입니다.
또는 단순히 상수 이름을 잘못 입력했을 가능성도 있습니다.
따라서 이 두 가지를 항상 유의하세요.
상수는 변경될 수 있다
앞서 언급했듯이, Ruby의 상수는 변경이 가능합니다.
예시:
ABC = 1 ABC = 2
다만 이 경우 다음과 같은 경고 메시지가 출력됩니다:
2: warning: already initialized constant ABC
프로그램은 여전히 정상적으로 동작하지만, 이런 경고는 피하는 것이 좋습니다.
상수 변경을 근본적으로 막을 방법은 없습니다. Ruby의 변수는 값을 담는 '컨테이너'가 아니라 객체를 가리키는 포인터, 즉 '레이블'에 불과하기 때문입니다.
가장 좋은 대안은 불변(immutable) 객체를 사용하는 것입니다.
예시:
AUTHOR = "Jesus Castello".freeze AUTHOR << "o" # RuntimeError: can't modify frozen String
관련 글: Ruby 가변성(mutability)과 freeze 메서드.
위 예시에서 AUTHOR 상수가 가리키는 대상 자체는 여전히 바꿀 수 있습니다. freeze가 보호해주는 것은 객체 자체의 수정뿐입니다.
상수 관련 메서드
상수를 다루기 위해 전용으로 제공되는 메서드들이 몇 가지 있습니다:
| 메서드 | 설명 |
|---|---|
| constants | 클래스에 정의된 상수들을 심볼 배열 형태로 반환합니다 |
| const_get | 상수의 값을 반환합니다. 심볼 또는 문자열을 인자로 받습니다 |
| const_set | 상수에 값을 설정합니다. 상수 이름(심볼)과 값, 두 개의 인자를 받습니다 |
| const_missing | method_missing의 상수 버전입니다 |
| const_defined? | 주어진 상수(심볼)가 정의되어 있으면 true를 반환합니다 |
| remove_const | 상수를 제거합니다 |
| private_constant | 상수를 비공개로 만들어, 클래스 외부에서 Class::ABC 문법으로 접근할 수 없게 합니다 |
이 메서드들을 활용하면 다양한 메타프로그래밍 트릭도 구현할 수 있습니다.
예시:
module Food
class Bacon; end
class Chocolate; end
end
puts "Classes defined inside #{Food}:"
puts Food.constants
또한 "Array" 같은 문자열을 사용해 실제 클래스를 가져올 수도 있습니다:
array_class = Object.const_get("Array")
다만 주의할 점이 있습니다. 문자열이 params 등 사용자 입력에서 온 것이라면 코드 인젝션 공격에 노출될 수 있습니다.
Rails에는 const_get을 대신 수행해주는 constantize 메서드가 있지만, 역시 별도의 보안 검사를 수행하지 않으니 유의해야 합니다.
치트 시트(Cheat Sheet)

Ruby 상수의 스코프
클래스 밖, 즉 코드의 최상위 레벨(top-level)에서 생성한 상수는 어디에서든 접근할 수 있습니다.
상수는 자식 클래스에서도 사용할 수 있습니다.
class A
FOO = 1
end
class B < A
def foo
puts FOO
end
end
B.constants
# [:FOO]
중첩된 모듈이나 클래스 바깥에서 정의된 상수 역시 중첩된 클래스 내부에서 접근 가능합니다.
module Food
STORE_ADDRESS = "The moon"
class Bacon
def foo; puts STORE_ADDRESS; end
end
end
fb = Food::Bacon.new
fb.foo
# "The moon"
모듈 믹스인(Mixin)
믹스인된 모듈의 상수 역시 사용할 수 있습니다:
module Mixin A = 123 end class Product include Mixin puts A end # 123
주의할 점은 이 동작은 include할 때만 유효하며, extend를 사용하면 동작하지 않는다는 것입니다.
예시:
class Product extend Mixin puts A end # uninitialized constant Product::A
추가로 알아둘 점:
모듈에서 include된 메서드를 호출하면, Ruby는 그 메서드가 정의된 위치를 기준으로 상수를 탐색합니다.
예시:
module Parent
def print_value
VALUE
end
end
class Child
include Parent
VALUE = 1
end
# 동작함
p Child::VALUE
# uninitialized constant Parent::VALUE
p Child.new.print_value
꼭 기억해두세요!
모듈 중첩(Nesting)
마지막으로 중첩된 클래스(모듈도 동일)를 사용한 예시를 살펴보겠습니다.
class A
FOO = 1
end
class A::B
class C
puts FOO
end
end
여기서 A::B라는 표기를 일종의 축약형으로 사용했습니다. 하지만 문제는 클래스 C가 FOO에 직접 접근할 수 없다는 것입니다.
그렇기 때문에 다음과 같은 방식의 중첩을 사용하는 것이 좋습니다:
class A
FOO = 1
end
class A
class B
class C
puts FOO
end
end
end
첫 번째 예시에서도 ::A::FOO를 사용하면 상수에 접근할 수 있지만, 클래스 이름이 변경되면 에러가 발생합니다.
::A::FOO 문법이 동작하는 이유는 Ruby에게 최상위 스코프, 즉 Array나 String 같은 상수가 정의되어 있는 곳을 찾아보라고 지시하기 때문입니다.
요약
이번 글에서는 흥미로운 동작 방식을 가진 변수 타입인 Ruby 상수에 대해 배웠습니다. 상수의 값은 변경할 수 있지만, 그럴 경우 경고 메시지가 출력됩니다.
또한 클래스 이름 자체가 상수라는 사실, 그리고 사용자 입력과 함께 const_get을 사용하는 것은 피해야 한다는 점도 배웠습니다.
이 글이 도움이 되었다면 주변에 공유하는 것도 잊지 마세요. 더 많은 사람들이 상수의 동작 원리를 이해할 수 있을 것입니다.