Ruby Magic 시리즈에 다시 오신 것을 환영합니다! 이번 에피소드에서는 Ruby가 구문 설탕(syntactic sugar)을 활용해 코드를 더 표현력 있고 읽기 쉽게 만드는 방법을 살펴봅니다. 글을 끝까지 읽고 나면 Ruby의 몇 가지 마법 같은 기능이 내부적으로 어떻게 동작하는지, 그리고 직접 이런 설탕을 활용한 메서드를 작성하는 방법까지 알게 될 것입니다.
Ruby 애플리케이션을 개발하다 보면 클래스 속성, 배열(Array), 해시(Hash)를 다른 언어에서는 낯설게 느껴지는 방식으로 다루게 됩니다. 과연 속성을 할당하고 배열이나 해시에서 값을 가져오는 메서드는 어떻게 정의할 수 있을까요?
Ruby는 이러한 메서드가 호출될 때 자연스럽게 동작하도록 약간의 구문 설탕을 제공합니다. 이번 글에서 그 원리를 자세히 탐구해 보겠습니다.
person1 = Person.new
person1.name = "John"
array = [:foo, :bar]
array[1] # => :bar
hash = { :key => :foo }
hash[:key] # => :foo
hash[:key] = :value구문 설탕이란 무엇일까?
구문 설탕(syntactic sugar)이란 더 읽기 쉽고 간결한 코드를 작성할 수 있도록 Ruby가 제공하는 ✨마법✨ 같은 기능을 말합니다. Ruby에서는 특정 기호나 공백을 생략하거나, 어떤 표현식을 헬퍼를 통해 간단하게 작성하는 방식으로 구현됩니다.
메서드 이름에 사용할 수 있는 특수 문자
먼저 메서드 이름부터 살펴보겠습니다. Ruby에서는 다른 언어에서 흔히 지원하지 않는 다양한 문자와 특수 기호를 메서드 이름에 사용할 수 있습니다. Rails 애플리케이션을 개발해 본 적이 있다면 save! 메서드를 접해 봤을 것입니다. 이는 Rails에만 있는 기능이 아니라, Ruby 메서드 이름에 ! 문자를 사용할 수 있음을 보여주는 좋은 예입니다.
같은 원리로 =, [, ], ?, %, &, |, <, >, *, -, +, / 같은 기호들도 메서드 이름에 포함할 수 있습니다.
이러한 문자들을 지원하기 때문에 메서드의 용도를 더 명확하게 드러내는 이름을 지을 수 있습니다:
- 속성 할당:
person.name = "foo" - 질문형 메서드:
person.alive? - 위험한 동작 수행:
car.destroy! - 객체가 실제와 다른 형태처럼 동작하게 만들기:
car[:wheels]
속성 메서드 정의하기
attr_accessor로 클래스에 속성을 정의하면 Ruby는 인스턴스 변수에 대한 리더(reader) 메서드와 라이터(writer) 메서드를 자동으로 생성합니다.
class Person
attr_accessor :name
end
person = Person.new
person.name = "John"
person.name # => "John"내부적으로 Ruby는 두 개의 메서드를 만듭니다:
attr_reader를 통해 생성된Person#name: 클래스의 속성/인스턴스 변수를 읽습니다.attr_writer를 통해 생성된Person#name=: 클래스의 속성/인스턴스 변수에 값을 씁니다.
이제 이 동작을 커스터마이징하고 싶다고 가정해 봅시다. attr_accessor 헬퍼를 사용하지 않고 메서드를 직접 정의해 보겠습니다.
class AwesomePerson
def name
"Person name: #{@name}"
end
def name=(value)
@name = "Awesome #{value}"
end
end
person = AwesomePerson.new
person.name = "Jane"
person.name # => "Person name: Awesome Jane"name= 메서드의 정의 방식은 person.name = "Jane"처럼 메서드를 호출하는 모습과 거의 동일합니다. 호출 시에는 등호(=) 주변의 공백을 정의하지 않고 괄호도 사용하지 않습니다.
선택적인 괄호와 공백
Ruby에서는 대부분의 경우 괄호가 선택 사항이라는 점을 이미 알고 계실 겁니다. 메서드에 인자를 전달할 때 반드시 괄호 ()로 감쌀 필요는 없으며, 가독성을 위해 감싸도 됩니다.
if문이 좋은 예입니다. 많은 언어에서 if문이 평가하는 표현식을 괄호로 감싸야 하지만, Ruby에서는 생략할 수 있습니다.
puts "Hello!" if (true) # 괄호 사용
puts "Hello!" if true # 괄호 생략메서드 정의와 다른 표현식에도 동일하게 적용됩니다.
def greeting name # 괄호 생략
"Hello #{name}!"
end
greeting("Robin") # 괄호 사용
greeting "Robin" # 괄호 생략
greeting"Robin" # 괄호와 공백 모두 생략마지막 줄은 읽기 어렵지만 정상적으로 동작합니다. 메서드를 호출할 때도 괄호와 공백은 선택 사항입니다.
다만 모든 괄호와 공백을 무조건 생략하는 것은 주의해야 합니다. 일부는 Ruby가 의도를 올바르게 이해하는 데 도움을 줍니다! 확실하지 않다면 인자를 괄호로 감싸서 어떤 인자가 어떤 메서드 호출에 속하는지 사람과 Ruby 모두 명확하게 알 수 있도록 하는 것이 좋습니다.
아래의 모든 호출 방식이 지원되지만, 일반적으로는 괄호를 생략하고 공백을 추가해 코드를 조금 더 읽기 쉽게 만듭니다.
# 앞서 정의한 메서드:
# def name=(value)
# @name = "Awesome #{value}"
# end
person.name = "Jane"
person.name="Jane"
person.name=("Jane") # 메서드 정의와 아주 비슷해 보이죠!이제 name 속성에 대한 커스텀 리더와 라이터 메서드를 정의했습니다. 필요에 따라 동작을 자유롭게 커스터마이징할 수 있으며, 콜백을 사용하지 않고도 속성을 할당하는 시점에 값을 직접 변환할 수 있습니다.
[ ] 메서드 정의하기
다음으로 살펴볼 것은 Ruby의 대괄호 메서드 [ ]입니다. 주로 배열의 인덱스나 해시의 키로 값을 가져오거나 할당할 때 사용됩니다.
hash = { :foo => :bar, :abc => :def }
hash[:foo] # => :bar
hash[:foo] = :baz # => :baz
array = [:foo, :bar]
array[1] # => :bar이 메서드들이 어떻게 정의되어 있는지 살펴보겠습니다. hash[:foo]를 호출할 때 우리는 Ruby의 구문 설탕을 사용하고 있는 것입니다. 이를 다른 방식으로 표현하면 다음과 같습니다:
hash = { :foo => :bar }
hash.[](:foo)
hash.[]=(:foo, :baz)
# 또는 심지어:
hash.send(:[], :foo)
hash.send(:[]=, :foo, :baz)일반적인 작성 방식(hash[:foo], hash[:foo] = :baz)과 비교해 보면 차이점이 보이기 시작합니다. 첫 번째 예제(hash.[](:foo))에서 Ruby는 첫 번째 인자를 대괄호 사이로 옮겨 hash[:foo] 형태로 만듭니다. hash.[]=(:foo, :baz)를 호출하면 두 번째 인자가 값으로 전달되어 hash[:foo] = :baz가 되는 것입니다.
이 원리를 알았으니 이제 Ruby가 이해할 수 있는 방식으로 직접 [ ]와 [ ]= 메서드를 정의할 수 있습니다.
class MyHash
def initialize
@internal_hash = {}
end
def [](key)
@internal_hash[key]
end
def []=(key, value)
@internal_hash[key] = value
end
end이 메서드들이 일반적인 Ruby 메서드라는 것을 알았으니, 다른 메서드와 마찬가지로 같은 로직을 적용할 수 있습니다. 심지어 [ ] 메서드에서 여러 개의 키를 허용하는 것처럼 기발한 동작도 만들 수 있습니다.
class MyHash
def initialize
@internal_hash = { :foo => :bar, :abc => :def }
end
def [](*keys)
@internal_hash.values_at(*keys)
end
end
hash = MyHash.new
hash[:foo, :abc] # => [:bar, :def]직접 만들어 보세요
Ruby의 구문 설탕에 대해 조금 알게 되었으니, 이 지식을 활용해 커스텀 라이터, Hash처럼 동작하는 클래스 등 다양한 메서드를 직접 만들어 볼 수 있습니다.
실제로 많은 젬(gem)이 대괄호 메서드를 정의해서 실제로는 Array나 Hash가 아닌 것을 마치 그것처럼 느껴지게 만든다는 사실에 놀라실지도 모릅니다. 한 가지 예로 Rails 애플리케이션에서 플래시 메시지를 설정할 때 flash[:alert] = "An error occurred"처럼 사용하는 경우가 있습니다. AppSignal 젬에서도 Config 클래스에서 설정값을 가져올 때 이 방식을 축약형으로 활용합니다.
지금까지 Ruby에서 메서드를 정의하고 호출할 때 사용되는 구문 설탕에 대해 간략히 살펴보았습니다. 이 글이 유익했는지, 궁금한 점이 있는지, 다음에 어떤 주제를 다뤘으면 좋겠는지 @AppSignal을 통해 알려주세요.