프로그래밍 언어 루비 2.7 릴리스 및 주요 변경 사항
패턴 매칭 문법, REPL 개선, 위치 인자와 키워드 인자 분리, 번호 파라미터 등

루비 2.7 릴리스

루비Ruby는 마츠모토 유키히로Yukihiro Matsumoto 씨가 만든 프로그래밍 언어로, 2019년에도 어김없이 12월 25일 크리스마스에 루비 메이저 릴리스가 있었습니다. 2018년에도 같은 날 2.6이 릴리스 되어서, 44bits에서도 변경사항들을 소개했었습니다.

2.7 버전은 2.x 버전의 실질적인 마지막 메이저 릴리스입니다. 이 글에서는 루비 2.7 설치 방법과 문법과 관련된 주요한 변경사항을 소개합니다.

루비 2.7 설치 및 공식 도커 이미지

ruby-install을 사용하면 바로 루비 2.7을 설치해서 사용해볼 수 있습니다. ruby-install은 설치에 대해서는 공식 저장소를 참고해주세요. 맥OS에서는 홈브류Homebrew로 간단히 설치할 수 있습니다. ruby-install에서는 루비의 공식 저장소를 기반으로 최신 버전을 가져오므로 별도의 업데이트 없이 바로 루비 2.7을 설치하는 것이 가능합니다.

$ brew install ruby-install
$ ruby-install --latest ruby
## 혹은
$ ruby-install ruby 2.7

루비를 직접 설치하지 않고도 공식 도커 이미지를 사용해 루비 2.7을 바로 테스트해볼 수 있습니다. rubylang/ruby을 사용합니다.

$ docker run -it rubylang/ruby:2.7.0-bionic ruby --version
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]

루비 언어 문법 변화 및 개선 내용

패턴 매칭(Experimental)

아직 실험적인 기능이지만, 패턴 매칭 기능이 루비에도 공식적으로 도입되었습니다. 패턴 매칭은 함수형 언어들에서 주로 사용되는 기법으로 데이터가 특정한 구조를 따르는지를 매칭해서 값을 가져올 수 있는 기능입니다. 루비에서는 case ... in이라는 새로운 문법으로 도입되며, dig 메서드를 사용하지 않고도 중첩되어있는 값을 꺼내오거나, JSON 데이터를 다룰 때 편리하게 사용할 수 있습니다.

간단한 예를 들어보면 다음과 같이 사용할 수 있습니다.

case ["hello", {name: "world"}]
in [hello, {name: name}]
  name
end
=> "world"

여기서 case에는 평가될 값이 오고, in에는 패턴이 옵니다. case에 넘겨진 값이 in의 패턴에 매칭될 경우 그 아래에 있는 내용들이 실행됩니다. 패턴 매칭 문법에 대한 내용은 좀 더 많은 내용을 담고 있어서 자세한 내용은 루비 커미터 가즈키 츠지모토 씨의 발표 내용(영어)를 참고해주세요. 이후에 좀 더 자세히 다뤄보도록 하겠습니다.

REPL 개선

루비 2.7에서는 루비의 공식 REPL인 IRB의 커다란 개선이 있었습니다. REPL은 프로그래밍 언어의 명령어를 한 줄씩 해석하고 결과를 돌려주는 애플리케이션으로 주로 동적 프로그래밍 언어에서 많이 사용되어 왔습니다. 최근에는 자바 1.9에도 REPL이 공식적으로 들어갈 정도로 널리 사용되는 애플리케이션입니다.

이번 2.7에서는 IRB에 멀티라인 에디팅 기능과 rdoc 참조 기능이 추가되었습니다. 특히 멀티라인 에디팅 부분은 아주 흥미로운 변화였습니다. 이러한 REPL을 구현할 때는 일반적으로 GNU Readline 라이브러리가 사용됩니다. 루비스트라면 루비 빌드할 때 Readline 라이브러리가 제대로 설치되어있지 않아서 설치가 되지 않거나, IRB에서 입력이 깨질 때 Readline을 재설치하라는 등의 이야기를 본 적이 있을 것입니다. Readline은 단순히 의존성 문제 뿐만 아니라, 기능적인 제약으로도 이어져왔습니다. 새로운 IRB는 사쿠라 이토 야나기 씨가 Readline을 루비로 재구현하고 기능을 추가한 reline이 사용했습니다.

reline을 사용해 멀티라인 에디팅이 구현되었습니다. 이는 단순히 기존의 다중행 입력이 아니라, 다중행 편집을 지원합니다. 예를 들어 IRB 상에서 def ... end 구문을 사용해 메서드를 정의하는 경우 def를 종료하는 end가 오기 전까지의 내용을 블럭으로 인식하고 위아래 커서를 이동하며 편집할 수 있습니다. 이 기능은 공식 릴리스 문서에서도 스크린캐스트로 자랑하고 있습니다.

GC 컴팩션

루비의 GC(가비지 컬렉션)는 장기간에 걸쳐서 개선되고 있습니다. 루비 2.0까지는 Mark and Sweep 방식을 사용해왔습니다만, 대규모 웹 애플리케이션을 운영하는 경우 리퀘스트 처리중에 GC가 발생해 병목이 되는 등의 문제가 있었습니다. 루비 2.1에서는 힙 영역을 구분해서 GC를 실행하는 차세대 GC가 도입되었고 지속적으로 개선 중에 있습니다. 2.7 에서 추가된 GC 컴팩션 기능은 사용자가 직접 실행할 수 있는 메서드(GC.compact)로 조각나 있는 힙 메모리를 한 데 모으는 작업을 수행합니다.

깃허브GitHub의 아론 패터슨Aaron Patterson 씨가 구현한 내용으로 자세한 내용은 밋업 영상과 이슈에서 확인할 수 있습니다.

위치 인자와 키워드 인자 분리

이 변화는 루비 3.0의 준비 작업 중 하나입니다. 먼저 위치 인자는 위치가 고정된 인자로 메서드에서 정의하는 일반적인 인자를 의미합니다. 그런데 루비에 키워드 인자 방식이 도입되면서, 인자 해석 방식은 아주 복잡해졌습니다. 루비에서는 오래 전에 해시 오브젝트를 마치 키워드인자인 것처럼 메서드로 넘겨서 처리하던 방식을 자주 사용되었습니다만, 이 방식이 키워드 인자 문법으로 들어왔습니다.

예를 들어 다음 코드를 하나 살펴보겠습니다.

def foo(arg, key: 1)
  puts arg
  puts key
end

이 함수를 파라미터 없이 호출하면 에러가 발생합니다. 메서드 시그니처에 arg가 지정되어있지 않으니 당연한 일입니다. 그런데 foo(key: 2)라고 호출하면 어떻게 해석될까요? 이건 꽤나 애매합니다. 왜냐면 메서드 시그니처 뒤에 지정된 key 키워드 인자에 값이 들어가야할 것 같습니다. 그런데 그렇게 해석되면 이 메서드 호출은 arg 인자가 없으니 에러가 발생해야합니다. 실제로 실행해봅니다.

> foo(key: 2)
{:key=>2}
42
=> nil

에러는 나지 않습니다…만, 결과가 참 골치 아픕니다. key: 2가 해시로 해석되어 arg의 값이 되어버렸습니다. 😠

내부적인 규칙은 있습니다만, 위치 인자와 키워드 인자를 넘기는 방식이 명시적으로 분리되어있지 않아서 혼돈에 빠지기 딱 좋습니다. 루비에서도 이러한 논의가 이미 진행되고 있었고, 3.0에서는 위치 인자와 키워 드인자 사용 방식이 명확하게 분리될 예정입니다.

혹시 큰 변화 때문에 걱정이 될 수도 있습니다만, 2.7에서는 기본적으로 2.6 방식대로 동작합니다. 단, 3.0에서 지원하지 않을 방식으로 사용하는 경우 경고가 출력됩니다. 미리 미리 3.0을 준비해야할 것으로 보입니다.

공식 릴리스 문서의 예제를 좀 더 살펴보겠습니다.

def foo(key: 42); end; foo({key: 42})   # 경고
def foo(**kw);    end; foo({key: 42})   # 경고
def foo(key: 42); end; foo(**{key: 42}) # OK
def foo(**kw);    end; foo(**{key: 42}) # OK 

해시로 키워드 인자를 넘기는 방식이 자주 사용되었습니다만, 앞으로(3.0부터) 해시를 키워드 인자로 넘기려면 반드시 더블 스플랫 연산자(**)를 사용해야합니다.

def foo(h, **kw); end; foo(key: 42)      # 경고
def foo(h, key: 42); end; foo(key: 42)   # 경고
def foo(h, **kw); end; foo({key: 42})    # OK
def foo(h, key: 42); end; foo({key: 42}) # OK

이 경우는 위에서 예로 들었던 경우와 같습니다. 여기서는 h라는 위치 인자를 반드시 받아야하기 때문에 foo에 넘겨지는 첫번째 값은 위치 인자에 대입됩니다. 현재는 key: 42가 자동적으로 해시로 변환됩니다만, 앞으로는 해시를 명시적으로 사용해야만 위치인자 h 값이 될 수 있습니다(위의 예제들은 모두 키워드 인자를 넘기는 게 아니라, h에 해시 오브젝트를 넘기는 예제입니다).

def foo(h={}, key: 42); end; foo("key" => 43, key: 42)   # 경고
def foo(h={}, key: 42); end; foo({"key" => 43, key: 42}) # 경고
def foo(h={}, key: 42); end; foo({"key" => 43}, key: 42) # OK

이걸 지금 보니, 머리가 아파오네요. 위의 두 줄은 신기하게도 지금은 정상적으로 동작합니다. 인자가 어디로 들어갈지 유추해보시기 바랍니다. 앞에서 잠깐 얘기하지만 이는 해시 오브젝트와 키워드 인자가 거의 같은 의미로 받아들여졌기 때문에 생긴 문제입니다. 앞으로는 맨 마지막 줄과 같이 해시를 위치 인자에 넘기는 것과 키워드 인자는 명시적으로 분리됩니다.

def foo(h, **nil); end; foo(key: 1)       # ArgumentError
def foo(h, **nil); end; foo(**{key: 1})   # ArgumentError
def foo(h, **nil); end; foo("str" => 1)   # ArgumentError
def foo(h, **nil); end; foo({key: 1})     # OK
def foo(h, **nil); end; foo({"str" => 1}) # OK

키워드 인자를 명시적으로 받지 않는다고 선언할 수 있는 **nil 문법도 추가됩니다.

좀 더 자세한 내용은 루비 2.7 릴리스 문서와 공식 웹 페이지에 위치 인자와 키워드 인자 분리에 대한 글이 올라와있으니 참고해주시기 바랍니다.

블럭에 번호 파라미터(Numbered parameters) 문법 추가

블럭에서 사용할 수 있는 번호 파라미터(Numbered parameters) 문법이 실험적으로 추가되었습니다.

루비에서는 블럭에서 인자를 넘겨받는 경우 인자에 이름을 반드시 지정해야만 접근하는 것이 가능합니다.

> (1..10).map{|i| i * 20}
=> [20, 40, 60, 80, 100, 120, 140, 160, 180, 200]

이제 i와 같이 이름을 붙이지 않더라도, _1, _2와 같은 번호 파라미터를 사용해서 바로 블럭에 넘겨진 값에 접근하는 것이 가능해졌습니다. 위의 코드는 아래와 같이 작성할 수 있습니다.

> (1..10).map{_1 * 20}
=> [20, 40, 60, 80, 100, 120, 140, 160, 180, 200]

단, _1과 같은 변수는 은 루비에서 예약어가 아니므로 블럭 외부에서 변수에 대입하는 것이 가능합니다. 이 때는 블럭에서 _1에 접근할 경우 외부에서 정의된 값이 사용되며, 경고가 출력됩니다. 같은 맥락에서 유추할 수 있듯이, 블럭이 중첩된 상태에서 연속적으로 숫자 파라미터를 사용하면 문제가 될 수 있습니다. 이 경우 이미 번호 파라미터가 사용되었다고 에러가 발생합니다.

비긴리스 범위 문법 추가

루비 2.6에서 도입된 엔드리스 범위(endless range)에 대응하는 비긴리스 범위(beginless range) 문법이 추가되었습니다. 단 비긴리스 범위에서는 each 메서드를 사용할 수 없습니다.

alphabet = ("a".."k").to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"]

alphabet[..4]
=> ["a", "b", "c", "d", "e"]

범위를 사용한 플립플롭 연산자의 비추천이 해제

2.6에서 비추천이 되었다고 전해드린 범위를 사용한 플립플롭 연산자가 다시 비추천이 아니게 되었습니다. 없애지 말아달라는 요청이 많이 있었던 걸로 보입니다.

> ("a".."z").each {|c|  puts c if (c=="d")..(c=="f")}
d
e
f
=> "a".."z"

메서드가 받은 모든 인자를 위임하기 위한 문법 ... 추가

메서드가 전달받은 인자를 다시 다른 메서드로 전달하는 것은 생각보다 까다롭습니다. 루비 2.7부터는 ...을 사용해 인자 위임이 단순해졌습니다.

def hello(...)
  world(...)
end

이 코드는 hello에 넘긴 모든 인자를 다시 world에 넘겨줍니다. 단, ... 형태로 전달되는 경우 hello 내에서 인자를 참조하는 것은 간단하지 않으므로 위임하는 용도로만 사용해야할 것 같습니다.

특수변수 $;, $, 비추천(deprecated)

루비는 펄Perl의 영향으로 소위 매직 변수라고 불리는 $와 특수문자가 조합된 변수를 지원해왔습니다. $:String#split에 영향을 주는 변수이고, $,Array#join에 영향을 주는 변수입니다. 하지만 최근에는 이러한 변수들을 사용하지 않는 추세에 있습니다. 이제 루비 $:이나 $,에 값을 대입하는 경우 경고가 출력됩니다.

다중행 메서드 체인중에 일부 행을 코멘트하는 것이 가능해짐

이건 정말… 멋지네요. 요즘에도 유행하는지는 모르겠지만 루비에서는 열거자(Enumerable)가 굉장히 많이 활용되기 때문에 리스트 처리를 하는 경우 메서드 체이닝을 활용하면 매우 유용합니다.

특별히 의미는 없는 예입니다만…

(1..10).to_a
  .map{|i| i * 10}
  .map{|i| i / 2}

메서드 체이닝이 길어지는 경우 위와 같이 여러 줄에 걸쳐서 코드를 작성하게 됩니다. 이 때 중간에 있는 라인을 코멘트하는 경우 메서드 체인이 제대로 해석되지 않고, 에러가 발생했습니다.

(1..10).to_a
  # .map{|i| i * 10}
  .map{|i| i / 2}
=> syntax error, unexpected '.', expecting end-of-input

2.7부터는 이 구문에서 에러가 발생하지 않습니다(단, 중간 라인이 비어있다면 에러가 발생합니다). 사실 점을 라인 마지막에 찍어서 메서드 체이닝을 사용하는 경우 원래 에러가 나지 않았습니다.

self.를 리시버로 프라이빗 메서드 호출 가능하도록 변경

조금 설명이 미묘한 경우입니다만, 루비의 경우 프라이빗 메서드로 정의한 경우 리시버에 메서드를 호출하는 방식이 허용되지 않았습니다. 예를 들어 루비 2.6에서 다음 코드는 에러가 발생합니다.

class HelloWorld
  def hello
    self.world
  end

  private
  def world
    puts "Hello, world"
  end
end

hw = HelloWorld.new
hw.hello
=> NoMethodError (private method `world' called for #<HelloWorld:0x000056239a672320>)

이 코드는 hello 메서드에서 self.world가 아니라 world라고 실행하면 동작합니다. 왜 동작하지 않느냐고 묻는다면, 루비 동작 방식이 그랬습니다. 2.7부터는 리시버가 self.인 경우에 한해서 프라이빗 메서드를 호출할 수 있도록 변경되었습니다. 따라서 위 코드도 정상 동작합니다.

마치며

이 외에도 2.7에서는 다양한 변경이 있었습니다. 더 자세한 내용은 루비 2.7 릴리스 공식 문서와 NEWS 문서에서 확인할 수 있습니다.

이와 더불어 작년에 이어서 올해도 루비 커미터 ko1 님과 마메 님이 NEWS 문서 해설하는 글을 공개했습니다. 이 내용은 일본어로 작성되었으며, 쿡패드에서 영어 번역도 공개했습니다. 루비 2.7 릴리스와 관련된 자세한 정보를 찾고 계시다면 이 글을 추천드립니다.

또한 2.7의 JIT 개선과 관련해서는 k0kubun 님의 글(영어)을 참고해주세요.

루비의 추후 발전 방향에 대해서는 루비를 만든 유키히로 마츠모토まつもと ゆきひろ, Yukihiro Matsumoto 씨의 루비콘프RubyConf 2019 키노트에서도 살펴볼 수 있습니다. 이 발표에서는 루비 3.0 개발이 공식화되었으며, 정적 분석(Static Analysis), 성능(Performance), 동시성(Concurrency)이 중점적으로 개선될 예정이라고 합니다.

추후 3.0이 될 2.8 버전도 개발이 시작되었습니다.

루비 3.0에서는 루비 2.6에서 실험적으로 도입된 JIT의 본격화를 비롯해, Fiber를 자동화하는 AutoFiber, 다수의 CPU 코어를 사용할 수 있는 Guild 등의 도입을 준비중이라고 합니다. 앞에서 정적 분석(Static Analytics)이라고 언급한 부분은 타입을 의미하는 것으로 보이는데, 어떤 모습이 될지 내년 크리스마스가 기대되네요! 🎄

깃허브(GitHub) 에코시스템 - 코드 클라이메이트(Code Climate), 젬나시움(Gemnasium), 트래비스CI(TravisCI), 커버럴스(Coveralls)

기사, 2013-10-02 - 깃허브 프로젝트와 연동할 수 있는 다양한 서비스들이 존재합니다. 이 글에서는 코드 메트릭스 측정을 위한 코드 클라이메이트, 의존성의 최신 여부를 알려주는 젬나시움, 지속적 통합을 서비스로 제공하는 트래비스CI, 테스트 커버리지 관리를 위한 커버럴스 서비스를 소개합니다.

만들면서 배우는 아마존 VPC(Amazon VPC) 입문: AWS 네트워크의 기초

기사, 2019-05-14 - 아마존 버추얼 프라이빗 클라우드(Amazon VPC)는 아마존 웹 서비스(Amazon Web Service)의 가장 기본이 되는 네트워크 서비스이자 리소스입니다. AWS에서 제공하는 대부분의 리소스들은 아마존 VPC를 기반으로 실행됩니다. 따라서 VPC를 이해하고 있어야 AWS 서비스를 십분 활용할 수 있습니다. 이 글에서는 기본 VPC와 같은 구성을 직접 만들어보면서 VPC의 기본 개념과 리소스들을 소개합니다.

아마존 EC2(Amazon EC2) 인스턴스 타입 검색 기능 추가

새소식, 2020-01-10 - 지난 10월 22일 아마존 EC2에는 인스턴스 타입을 검색하고 비교할 수 있는 검색 메뉴가 추가되었습니다. 아마존 EC2는 다양한 인스턴스 타입을 제공해서 선택하는 것도 쉽지 않았습니다. 인스턴스 검색 기능을 사용해 현재 상황에서 적절한 인스턴스 타입을 선택하는데 도움이 될 것으로 보입니다. 이 기능은 웹 콘솔과 API로 제공됩니다.