메서드 호출(Calling Methods): 객체에 메시지 보내기

메서드 호출(Calling Methods): 객체에 메시지 보내기

Ruby에서는 메서드를 호출하는 일이 곧 "객체에 메시지를 보내 작업을 시키는 것"이에요. 인자를 어떻게 나열하는지, 블록을 어떻게 붙이는지에 따라 호출 모양이 달라져요. 이번에는 메서드를 호출할 때 알아 두면 좋을 규칙과 블록 전달까지 차근차근 살펴볼게요.

출처: 공식문서 - Calling Methods

본문

리시버(Receiver)

self가 기본 리시버예요. 리시버를 지정하지 않으면 self가 사용돼요. 리시버를 지정하려면 .를 써요.

my_object.my_method

이 코드는 my_objectmy_method 메시지를 보내요. 어떤 객체든 리시버가 될 수 있지만, 메서드의 가시성(visibility)에 따라 메시지를 보내다가 NoMethodError가 날 수도 있어요.

메서드 호출 체이닝

한 메서드 호출 바로 뒤에 다른 메서드 호출을 이어 붙이면 호출을 "체인"으로 이을 수 있어요. 아래 예시는 Array들의 메서드를 이어 부른 모습이에요.

a = [:foo, 'bar', 2]
a1 = [:baz, nil, :bam, nil]
a2 = a.append(*a1).compact
a2 # => [:foo, "bar", 2, :baz, :bam]

서로 다른 클래스의 메서드도 체이닝할 수 있어요. 아래는 HashArray의 메서드를 이어 부른 모습이에요.

h = {foo: 0, bar: 1, baz: 2}
h.to_a.reverse # => [[:baz, 2], [:bar, 1], [:foo, 0]]

안전 탐색 연산자

&.는 "안전 탐색 연산자(safe navigation operator)"라고 불러요. 리시버가 nil일 때 메서드 호출을 건너뛰게 해줘요. 호출을 건너뛰면 nil을 반환하고, 메서드의 인자도 평가하지 않아요.

인자

메시지를 보낼 때 쓸 수 있는 인자는 세 종류예요. 위치 인자(positional), 키워드(명명된) 인자, 그리고 블록 인자죠. 각 메시지는 이 중 하나, 둘, 또는 전부를 쓸 수 있지만 반드시 정해진 순서로 나열해야 해요. Ruby의 모든 인자는 참조로 전달되고 지연 평가되지 않아요. 각 인자는 ,로 구분해요.

my_method(1, '2', :three)

인자는 표현식일 수도, 해시 인자일 수도, 키워드 인자일 수도 있어요.

'key' => value
key: value

Hash와 키워드 인자는 연속되어야 하고 모든 위치 인자 뒤에 와야 해요. 다만 서로 섞이는 건 허용돼요.

위치 인자

메시지의 위치 인자는 메서드 이름 뒤에 와요.

my_method(argument1, argument2)

괄호는 대부분 생략할 수 있어요.

my_method argument1, argument2

하지만 모호함을 피하기 위해 괄호가 필요한 경우도 있어요. 아래 코드는 어떤 메서드에 argument3을 보내야 할지 Ruby가 알 수 없어서 SyntaxError가 나요.

method_one argument1, method_two argument2, argument3

메서드 정의에 *argument가 있으면 남는 위치 인자들이 argumentArray로 담겨요. 메서드 정의에 키워드 인자가 없으면, 키워드형·해시형 인자들은 마지막 인자 하나의 Hash로 합쳐져요.

def my_method(options)
  p options
end

my_method('a' => 1, b: 2) # prints: {'a'=>1, :b=>2}

위치 인자 수가 너무 많으면 ArgumentError가 나요.

기본 위치 인자

메서드가 기본 인자를 정의하면 모든 인자를 채워 보낼 필요가 없어요. Ruby가 빠진 인자를 왼쪽에서 오른쪽으로 채워 넣어요.

def my_method(a, b, c = 3, d = 4)
  p [a, b, c, d]
end

두 개만 보내면 [1, 2, 3, 4]가 출력돼요. Ruby는 위치 인자의 중간에도 기본값이 올 수 있게 허용해요.

def my_method(a, b = 2, c = 3, d)
  p [a, b, c, d]
end

키워드 인자

키워드 인자는 모든 위치 인자 뒤에 오고, 위치 인자처럼 ,로 구분돼요. 주지 않은 키워드 인자는 메서드 정의의 기본값을 사용해요. 메서드가 나열하지 않은 키워드 인자를 주고 그 메서드가 임의 키워드 인자도 받지 않는다면 ArgumentError가 나요.

my_method(positional1, keyword1: value1, keyword2: value2)

키워드 인자의 값은 생략할 수 있어요. 값이 생략되면 키 이름으로 컨텍스트에서 값을 가져와요.

keyword1 = 'some value'
my_method(positional1, keyword1:)
# ...은 아래와 같아요
my_method(positional1, keyword1: keyword1)

블록 인자

블록 인자는 호출하는 스코프의 클로저(closure)를 메서드로 보내요. 블록 인자는 메시지를 보낼 때 항상 맨 마지막에 와요. 블록은 do ... end 또는 { ... }로 보내요.

my_method do
  # ...
end
my_method {
  # ...
}

do end{ }보다 우선순위가 낮아요. 그래서 아래 코드는 블록을 method_2로 보내요.

method_1 method_2 {
  # ...
}

반면 do ... end를 쓰면 블록이 method_1로 가요. 첫 번째 경우에도 괄호를 쓰면 블록이 method_1로 간다는 점을 기억해 두세요.

블록은 메서드가 보내 준 인자를 받을 수 있어요. 블록 인자는 메서드가 인자를 정의하는 것과 비슷하게 정의하고, 여는 do{ 다음의 | ... | 안에 둬요.

my_method do |argument1, argument2|
  # ...
end
블록 지역 인자

블록 인자 목록에서 ;를 사용하면 블록 지역 인자(block-local argument)를 선언할 수 있어요. 블록 지역 인자에 값을 할당해도 호출자 스코프 바깥의 같은 이름 지역 변수를 덮어쓰지 않아요.

def my_method
  yield self
end

place = "world"

my_method do |obj; place|
  place = "block"
  puts "hello #{obj} this is #{place}"
end

puts "place is: #{place}"

이 코드는 다음과 같이 출력돼요.

hello main this is block
place is: world

블록의 place 변수는 블록 바깥의 place 변수와 다른 변수예요. 블록 인자에서 ; place를 빼면 결과는 다르게 나와요.

hello main this is block
place is: block

위치 인자 풀기(Unpacking)

아래 같은 메서드가 있을 때,

def my_method(argument1, argument2, argument3)
end

*(splat) 연산자로 Array를 인자 목록으로 바꿀 수 있어요.

arguments = [1, 2, 3]
my_method(*arguments)

둘 다 my_method(1, 2, 3)과 같아요. *는 배열뿐 아니라 어떤 객체에도 적용할 수 있어요. 객체가 to_a 메서드에 응답하면 이 메서드를 호출해서 Array를 받아내고, 그 요소들을 개별 위치 인자로 전달해요. *nil은 빈 인자 목록으로 풀리므로 조건부 풀기(conditional unpacking)가 가능해요.

my_method(*(some_arguments if some_condition?))

배열의 객체 수가 메서드의 인자 수와 맞지 않으면 ArgumentError가 나요. splat 연산자가 호출 맨 앞에 오면 괄호를 써야 해요. 괄호가 없으면 풀기 연산자인지 곱셈 연산자인지 모호해져서, verbose 모드에서 경고가 나요.

my_method *arguments  # warning: '*' interpreted as argument prefix
my_method(*arguments) # no warning

키워드 인자 풀기

**(키워드 splat) 연산자로 Hash를 키워드 인자로 바꿀 수 있어요.

def my_method(first: 1, second: 2, third: 3)
end

arguments = {first: 3, second: 4, third: 5}
my_method(**arguments)

*와 달리 **to_h에 응답하지 않는 객체에 쓰면 에러를 내요. 예외는 nil인데, to_h를 명시적으로 정의하지 않았지만 **에 쓸 수 있고 키워드 인자를 아무것도 추가하지 않아요.

Proc을 블록으로 변환

블록을 쓰는 메서드에 proc이나 lambda를 &(블록 변환) 연산자로 블록 인자로 넘길 수 있어요.

def my_method
  yield self
end

argument = proc { |a| puts "#{a.inspect} was yielded" }
my_method(&argument)

블록 변환 연산자가 호출 맨 앞에 오면 경고를 피하려고 괄호를 써야 해요.

my_method &argument  # warning
my_method(&argument) # no warning

더 알아보기