Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby의 구문 설탕(Syntactic Sugar) 메서드 완벽 가이드

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을 통해 알려주세요.