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

루비 키워드 인자, 해시, 스플랫 완벽 활용 가이드

개발을 하다 보면 이런 패턴을 한 번쯤은 접해봤을 겁니다. 메서드의 마지막 인자로 options 해시를 받아서 추가 파라미터를 처리하는 방식이죠:

def hello_message(name_parts = {})
  first_name = name_parts.fetch(:first_name)
  last_name = name_parts.fetch(:last_name)

  "Hello, #{first_name} #{last_name}"
end

문제는 해시에서 파라미터를 일일이 꺼내야 한다는 점입니다. 정작 중요한 로직에 도달하기 전에 지저분한 준비 코드를 통과해야 하니까요.

하지만 루비 2.0 이상에서 이 메서드를 키워드 인자(keyword arguments) 방식으로 바꾸면, :first_name:last_name을 직접 꺼낼 필요가 없습니다. 루비가 알아서 처리해 줍니다:

def hello_message(first_name:, last_name:)
  "Hello, #{first_name} #{last_name}"
end

더 좋은 소식은, 애플리케이션이 이미 루비 1.9 이상의 해시 문법을 사용하고 있다면 호출하는 쪽 코드를 전혀 수정하지 않고도 키워드 인자를 사용할 수 있다는 점입니다:

def hello_message_with_an_options_hash(name_parts = {})
  first_name = name_parts.fetch(:first_name)
  last_name = name_parts.fetch(:last_name)

  "Hello, #{first_name} #{last_name}"
end

def hello_message_with_keyword_arguments(first_name:, last_name:)
  "Hello, #{first_name} #{last_name}"
end

hello_message_with_an_options_hash(first_name: "Justin", last_name: "Weiss")

hello_message_with_keyword_arguments(first_name: "Justin", last_name: "Weiss")

보이시나요? 두 호출 방식의 인자가 완전히 동일합니다!

키워드 인자 문법, 어디까지 밀어붙일 수 있을까?

그런데 아직 새로운 해시 문법으로 전환하지 않았다면 어떻게 될까요? 모든 코드를 변환할 수도 있지만, 적어도 루비 2.2.1 기준으로는 구식 해시 문법도 키워드 인자와 잘 호환됩니다:

irb(main):007:0> hello_message_with_keyword_arguments(:first_name => "Justin", :last_name => "Weiss")
=> "Hello, Justin Weiss"

좋네요! 그렇다면 인자 대신 해시 객체 자체를 넘기면 어떨까요?

irb(main):008:0> options = {:first_name => "Justin", :last_name => "Weiss"}
irb(main):009:0> hello_message_with_keyword_arguments(options)
=> "Hello, Justin Weiss"

오! 그럼 해시와 키워드 인자를 섞어서 전달하려면?

irb(main):010:0> options = {last_name: "Weiss"}
irb(main):011:0> hello_message_with_keyword_arguments(first_name: "Justin", options)
SyntaxError: (irb):11: syntax error, unexpected ')', expecting =>
	from /usr/local/bin/irb:11:in `<main>'

음, 여기까지는 무리였네요. 이 문제를 해결하려면 Hash#merge로 하나의 해시를 만들어 전달하는 방법도 있지만, 더 나은 방법이 있습니다.

만약 키워드 인자 대신 일반 인자를 사용한다면, 배열을 *(스플랫) 연산자로 펼쳐서 전달할 수 있습니다:

def generate_thumbnail(name, width, height)
  # ...
end

dimensions = [240, 320]
generate_thumbnail("headshot.jpg", *dimensions)

그렇다면 키워드 인자도 스플랫으로 펼칠 수 있을까요?

결론부터 말하면 가능합니다. 바로 ** 연산자를 사용하면 됩니다. 앞서 실패했던 예제를 **로 수정하면 다음과 같습니다:

irb(main):010:0> options = {last_name: "Weiss"}
irb(main):011:0> hello_message_with_keyword_arguments(first_name: "Justin", **options)
=> "Hello, Justin Weiss"

정말 극단적으로 가고 싶다면, 일반 인자, 키워드 인자, 스플랫을 모두 섞어 쓰는 것도 가능합니다:

def hello_message(greeting, time_of_day, first_name:, last_name:)
  "#{greeting} #{time_of_day}, #{first_name} #{last_name}!"
end

args = ["Morning"]
keyword_args = {last_name: "Weiss"}

hello_message("Good", *args, first_name: "Justin", **keyword_args) # => "Good Morning, Justin Weiss!"

물론, 이 정도 조합이 실제로 필요한 상황이라면 설계 단계에서 뭔가 잘못됐을 가능성이 높습니다!

키워드 인자를 손쉽게 받아내는 방법

*를 사용하면 메서드에 전달된 모든 인자를 배열로 받을 수 있다는 것, 알고 계셨나요?

def argument_capturing_method(*args)
  args
end

argument_capturing_method(1, 2, 3) # => [1, 2, 3]

이 방식은 키워드 인자에도 적용됩니다. 키워드 인자는 해시로 변환되어 args 배열의 마지막 요소로 들어갑니다:

argument_capturing_method(1, 2, 3, key: "value") # => [1, 2, 3, {:key=>"value"}]

하지만 이렇게 잡아낸 키워드 인자를 args.last[:key]처럼 읽는 것은 좋은 방법이 아닙니다. 대신 새로운 ** 문법을 사용하면 키워드 인자를 별도로 깔끔하게 분리할 수 있습니다:

def dual_argument_capturing_method(*args, **keyword_args)
  {args: args, keyword_args: keyword_args}
end

dual_argument_capturing_method(1, 2, 3, key: "value") # => {:args=>[1, 2, 3], :keyword_args=>{:key=>"value"}}

이 문법을 사용하면 첫 번째 일반 인자는 args[0]으로, :key 키워드 인자는 keyword_args[:key]로 각각 접근할 수 있습니다.

… 물론, 이 시점에서 우리는 다시 옵션 해시 시대로 돌아온 셈이지만요.

마치며

키워드 인자는 코드에서 수많은 파라미터 추출 보일러플레이트를 제거해 주는 훌륭한 기능입니다. 경우에 따라서는 기존 코드를 거의 수정하지 않고도 그 혜택을 누릴 수 있습니다.

다만 좀 더 범용적인 메서드를 작성할 때는 키워드 인자를 제대로 다루기 위한 몇 가지 새로운 기법을 익혀야 합니다. 이런 기법을 자주 사용하지는 않겠지만, 필요한 순간이 오면 이 강력하고 유연한 도구들이 여러분을 기다리고 있을 것입니다.