Ruby의 심볼(symbol)은 다음과 같은 형태로 생겼습니다:
:testing
일부 사람들은 심볼을 변수와 혼동하기도 하지만, 사실 심볼은 변수와는 아무런 관련이 없습니다…
…심볼은 오히려 문자열과 훨씬 더 유사한 존재입니다.
그렇다면 Ruby에서 심볼과 문자열은 어떤 차이가 있을까요?
문자열은 데이터를 다룰 때 사용됩니다.
반면 심볼은 식별자(identifier)입니다.
이것이 바로 핵심적인 차이입니다:
심볼은 단순히 또 다른 종류의 문자열이 아니라, 서로 다른 목적을 가진 별개의 존재입니다.
심볼을 사용해야 하는 경우
심볼의 가장 흔한 용도 중 하나는 메서드명과 인스턴스 변수의 이름을 표현하는 것입니다.
예제:
attr_reader :title
attr_reader 뒤에 오는 :title은 @title 인스턴스 변수를 나타내는 심볼입니다.
또한 심볼을 해시(Hash)의 키로 사용할 수도 있습니다.
예제:
hash = {a: 1, b: 2, c: 3}
그렇다면 어떤 이점이 있을까요?
심볼은 시각적으로 더 깔끔하고, 불변(immutable)이며, 실제로 문자열 키와 심볼 키를 벤치마크로 비교해 보면 문자열 키가 약 1.7배 정도 느리다는 결과가 나옵니다.
여기서 불변(immutable)이라는 의미는 모든 심볼이 고유하며 변경할 수 없다는 뜻입니다:
:rubyguides.object_id # 437768 :rubyguides.object_id # 437768
이 두 심볼은 객체 ID(object id)가 동일하기 때문에 같은 객체입니다.
심볼은 send 같은 메타프로그래밍 메서드에서도 활용할 수 있습니다:
[1,2,3].send(:first)
정리하면:
메서드처럼 무언가의 이름 또는 라벨을 지정할 때는 심볼을 사용하고, 데이터(개별 문자) 자체가 중요할 때는 문자열을 사용하세요.
문자열과 심볼 간 변환하기
필요하다면 심볼을 문자열로 변환할 수 있습니다.
왜 그럴 필요가 있을까요?
심볼에는 String 클래스가 제공하는 메서드 중 일부만 존재하기 때문입니다.
또한 심볼은 변경할 수 없으므로, 심볼의 개별 문자를 다루고 싶다면 문자열로 변환해야 합니다.
이때는 to_s 메서드를 사용하면 됩니다.
예를 들어, method_missing을 사용하면 존재하지 않는 메서드의 이름이 심볼 형태로 전달됩니다. 이 이름이 특정 패턴(예: 물음표 ?로 끝나는지)과 일치하는지 검사하고 싶을 수 있습니다.
예제:
def method_missing(method_name, *args, &block)
if method_name.to_s[-1] == "?"
# do something
else
super
end
end
반대로 문자열 객체를 심볼 객체로 변환할 수도 있습니다.
이때 사용하는 메서드는 String#to_sym입니다:
"rubyguides".to_sym :rubyguides
심볼 배열 만들기
심볼 배열을 만들고 싶다면 다음 코드를 사용할 수 있습니다:
symbols = %i(a b c) [:a, :b, :c]
콜론(:)과 콤마(,)를 일일이 입력하지 않아도 되어 매우 편리합니다.
문자열 버전인 %w와 비슷한 문법입니다:
strings = %w(a b c) ["a", "b", "c"]
심볼 GC (고급)
심볼에 대해 알아두면 좋은 또 다른 사실은 심볼에도 여러 종류가 있다는 점입니다.
그 이유는 Ruby 2.2 이전에는 심볼이 가비지 컬렉션(GC) 대상이 아니었기 때문입니다. 즉, 문자열, 해시, 배열 같은 일반 Ruby 객체와 달리 더 이상 필요하지 않아도 메모리에서 정리되지 않았습니다.
직접 예제를 실행해 보세요:
p Symbol.all_symbols.size
# 2443
('aa'..'aj').map(&:to_sym)
GC.start
p Symbol.all_symbols.size
# 2453
새로운 심볼 10개를 생성했기 때문에 전체 심볼 수가 정확히 10개 증가한 것을 확인할 수 있습니다.
하지만 Ruby 2.2부터는 이러한 심볼들이 임시로 생성되어 코드의 다른 부분에서 참조되지 않으므로 메모리에서 제거됩니다.
Symbol GC가 활성화된 Ruby 버전에서 이 코드를 실행하면 두 카운트가 동일하게 나타날 것입니다.
일부 심볼은 메모리에서 절대 제거되지 않는데, 이를 "불멸 심볼(immortal symbol)"이라고 부릅니다.
ObjectSpace 모듈을 사용하면 이들을 세어 볼 수 있습니다:
require 'objspace'
ObjectSpace.count_symbols
{
:mortal_dynamic_symbol=>3,
:immortal_dynamic_symbol=>5,
:immortal_static_symbol=>3663,
:immortal_symbol=>3668
}
:a1처럼 리터럴로 직접 생성한 심볼은 자동으로 불멸 심볼이 된다는 점에 주목하세요. 새로운 메서드를 정의하면 그에 대응하는 immortal_static_symbol도 함께 생성됩니다.
그렇다면 소멸 가능한(mortal) 심볼은 어디에서 오는 걸까요?
to_sym 메서드를 통해 문자열을 심볼로 변환할 때 생성됩니다.
ObjectSpace.count_symbols를 사용하여 직접 확인해 볼 수 있습니다.
immortal_dynamic_symbol이 궁금하신 분들을 위해 설명하자면, 이는 mortal 상태에서 immortal 상태로 승격된 심볼을 의미합니다. mortal 심볼의 이름으로 메서드를 정의할 때 이러한 승격이 발생할 수 있습니다.
마무리
이번 글에서 배운 내용을 정리하면 다음과 같습니다:
- 심볼은 불변(immutable)이다
- 심볼은 값을 가리키는 포인터가 아니라 값 그 자체이다
- 문자열은 데이터를 위한 것이고, 심볼은 식별자(identity)를 위한 것이다
- 문자열과 심볼 간의 변환 방법
- Ruby 2.2에서 임시 심볼을 정리하기 위해 Symbol GC가 도입되었다
유익한 내용이었다면 주변에 공유해 주세요. 더 많은 사람들이 도움을 받을 수 있습니다 🙂