루비 스타일(3) – 말하는 듯한 코딩

20141221174044_JNBCLiYX_SBS_20141221_173820.874

말하는 것처럼 노래해야 마음을 울릴 수 있어요.
-- 박진영

I. 루비 코드의 생명은 가독성

루비 뿐 아니라 다른 많은 객체지향 언어들도 마찬가지이지만, public으로 노출되는 메소드들, 그중에서도 가장 많은 기능들을 제공하는 핵심 메소드들은 머리 속으로 컴파일러를 돌려보지 않아도 그냥 이해되는 형태로 구현이 되는 경향이 강합니다. 무슨 얘기냐면, 수면 밑의 복잡한 로직들은 protect/private 메소드로 감추고(말하자면 레고 블럭 단위들), 이 메소드들을 조립하는 것으로 큰 흐름을 구성해서 중요 메소드들을 구현하는 형태이죠. 이런 메소드들의 구현부를 보면 마치 사람의 언어로(물론 영어로..^^; ) 적은 것처럼 쉽고 명료합니다.

실제 코딩에서 자주 볼 수 있는 예를 한 번 들어봅시다.
stand-alone application이 아닌 Rails app에서는 레고 블럭을 조합해서 비즈니스 로직을 만들어내는 역할을 컨트롤러가 하고 있습니다. 아직 레일즈에 익숙하지 않은 개발자들이 흔히 쓰는 코드들 중에 이런 형태의 컨트롤러 코드를 많이 볼 수 있는데.. (예제 원문은 여기 참조)

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.where(active: true).order(:last_login_at)
  end
end

이런 컨트롤러 코드의 경우, 재사용성도 떨어지고 테스트하기도 어렵습니다. 무엇보다 User라는 모델에 어떤 필드가 있고 어떤 값을 갖고 있는지 컨트롤러 차원에서도 알고 있어야 한다는 게 더 심각한 문제입니다. 따라서 아래와 같이 리팩토링할 필요가 있습니다.

먼저 모델 내의 데이터와 관련된 query method는 모두 감추는 메소드를 모델에 추가합니다. 고맙게도 레일즈에서는 scope를 이용해서 쉽게 구현할 수 있습니다.

# app/models/user.rb
class User < ActiveRecord::Base
  scope :active, -> { where(active: true) }
  scope :by_last_login_at, -> { order(:last_login_at) }
end

그에 맞춰서 컨트롤러도 수정해줍니다.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.active.by_last_login_at
  end
end

훨씬 직관적이죠? 모델과 관련된 것은 모델 내부에서 처리하도록 하고, 컨트롤러는 가장 high-level의 로직만 다루도록 하면 읽기도 쉽고 구조도 깔끔해지고 테스트도 편리한, 일석삼조의 효과를 얻을 수 있습니다.
(단, 모든 order 쿼리 호출만을 저렇게 scope로 감싸는 건 항상 좋은 방법은 아니라고 생각합니다. 여러 번 호출되는 경우에 중복 제거 차원에서 감싸준다는 느낌이 좋을 것 같습니다.)

뿐만 아니라, 마찬가지 방법으로 추상화의 정점에 있는 메소드들 뿐 아니라 그보다 더 낮은 레벨에 있는 메소드들도 비슷한 방식으로 추상화 시켜서 할 수 있는 한 나눌 수 있을 때까지 나눠서 차츰차츰 복잡도가 올라가도록 구현하는 방식을 선호하는 루비 개발자들이 많습니다.

이렇게 하다보니 루비로 짠 코드의 경우 메소드의 길이가 매우 짧고 메소드 안의 내용도 매우 단순해지는 경향이 있습니다. (당장 rails 내부 소스를 한 번 보세요. 라인 수가 긴 메소드가 별로 없습니다.) 심지어 모든 메소드는 5라인 이내로 작성되어야 한다는 원칙을 고수하는 분들도 있습니다. Sandi Metz’ Rule을 한 번 읽어보세요. 꼭 그대로 따라하지 않는다고 해도 매우 시사하는 점이 많은 원칙들입니다. Sandi의 경우 본인의 원칙은 4라인이지만 다른 사람들을 봐줘서 버퍼로 1줄 더 준거라고 강연에서 얘기한 적이..-_-; 저도 그 정도는 아니지만 메소드 길이는 가급적 4~10 라인 정도로 유지하는 편입니다.

제가 이 얘기를 서두에 먼저 하는 이유는 제가 제시할 해법의 방향을 설명하기 위해서 입니다. 사실 지난 포스팅에서 보여드린 예제만 해도 이미 훌륭하게 문제를 해결하고 있습니다. 이제까지 제가 몇 가지 다른 코드 예시를 보여드리면서 여러가지 지적을 한 이유는 그 해법들이 잘못 되었으니 더 나은 코드를 제시해주겠다는 의미라기보다는, 능숙한 루비 개발자들의 코드에서 공통적으로 보이는 특징들(말하는 것 같은 코딩 적절히 잘게 나눠져서 추상화 되는 클래스와 메소드의 구조, map/reduce의 적절한 활용)에 대해서 생각해보자는 것이었습니다. 이제부터 보여드릴 해법도 할 수 있는 한 실제 코딩에서 사용될 것 같은 평이하고 잘 모듈화된 형태로 보여드리고자 합니다.

II. TDD 소개, 그리고 환경 준비

테스트 주도 개발(TDD)은 선제적으로 테스트를 만들어서 중요한 에러나 실수를 미연에 막아줄뿐만 아니라, divide & conquer 형태로 문제를 쪼개어 해결하는 데에 탁월한 도움을 주는 방법이기 때문에, 단도직입적으로 말하자면 그냥 코딩하는 것보다 TDD가 더 코딩이 빠르기 때문에, TDD를 합니다.
물론 누가 봐도 자명한 코드에도 억지로 테스트를 추가하거나, 맹목적으로 100% 테스트 커버리지를 위해서 의미없는 테스트 코드를 추가하는 것은 매우 어리석은 일이겠지만, 시의적절한 코드와 mock, spy 등의 도움을 충분히 받은 테스트의 가치는 매우 큽니다. 많은 사람이 협업하는 거대한 시스템에서도, 혼자 작업하는 프로젝트에서도 항상 큰 효과를 봐왔기 때문에 제게는 TDD가 아닌 형태로 진행하는 개발 자체를 생각하기 어렵습니다.

그럼 피보나치 문제를 풀기 위한 테스트 환경을 구축하는 것부터 시작해봅시다. 먼저 어떤 테스트 라이브러리를 쓸 것인가부터 결정해야겠군요.
저는 rspec을 매우 사랑하는 사람이지만 MVC 프레임웍 내에서 말하는 것 같은 테스트 코딩 behavier 정의가 매우 편리한 환경에서라면 몰라도 이런 간단한 곳에서는 굳이 시나리오의 description까지 적어가면서 테스트를 짜는 건 솔직히 오버인 것 같습니다.

좀 더 솔직히 얘기하자면.. rspec의 DSL이 정말 자연어로 말하듯이 코딩을 짜기 위한 완벽한 방법이긴 하지만, 사실 S/W 엔지니어들은 사람의 언어보다 프로그래밍 언어를 더 편하게 생각하기 때문에 괜히 사람들이 우리를 오타쿠 내지는 nerd라고 부르는 게 아닙니다.. 자연어에 가까운 로직 흐름이 오히려 덜 직관적으로 느껴집니다. 그러다보니 잘 코딩하자고 도입한 방법론/라이브러리가 단기적으론 사람 잡는 경우가 생깁니다. 저도 지금은 rspec 없이는 못 살게 됐지만 손에 익숙해지기 전까지 상당한 고생을 했습니다.
음.. 생각해보니 구글링을 해봐도 좋은 spec 구현에 대한 딱히 좋은 예제도 찾기 힘든데.. rspec 책을 사면 되지만 매뉴얼 따윈 안 보고 사는 게 엔지니어의 긍지인지라.. rspec 기초에 대한 포스팅도 나중에 해보고 싶다는 생각이 드네요.

어쨌든 그런 이유로 minitest로 테스트를 써보도록 합시다. (물론 minitest로도 rspec과 매우 유사한 spec 정의가 가능하지만 마찬가지 이유로 pass하겠습니다.^^ 테스트 환경을 포함한 모든 소스코드는 RoRLab Github에서 찾으실 수 있습니다.)

rspec 기반이긴 하지만 기본 실행 환경 구축에 대해서는 김대권 님의 좋은 발표자료가 있으니 참고 바랍니다.

III. 문제 해결은 어떻게 하는가?

문제 해결의 방식에 정답이란 있을 수 없습니다. 다만 널리 쓰이는 방법론이자, 여러 사람들에 의해서 좋다고 소문난 방법들 중 하나를 소개해보겠습니다.

Kent Beck의 명저 “TDD(테스트주도개발)”을 봐도 잘 나오지만, todo list 형태로 구현할 기능들을 먼저 적어놓고 개발을 하는 방법을 많이 선호합니다. 이렇게 하면 큰 맥락의(macro한) 문제 해결과 micro한 세부 구현을 나눠서 생각할 수 있어서 효과적으로 집중하기에 특히 좋습니다. 물론 todo list를 처음부터 완벽하게 적을 필요는 없고, 현 수준에서 생각이 가능하면서 우선적으로 구현할 항목들을 먼저 적고, 문제를 해결해나가면서 바꾸고 세분화 시키고 합칠 수 있습니다. 이에 대한 자세한 설명은 TDD 책에 아주 잘 나와있기 때문에 여기서 다루지는 않겠습니다.

우리의 피보나치 문제에서는 아래와 같은 todo 리스트가 나올 수 있겠군요.

  • n번째 피보나치 수를 구하는 기능 만들기
  • 피보나치 수열을 계산하는 기능 추가
  • 수열을 만드는 조건을, 마지막 값이 max 값을 넘지 않는 것으로 한정
  • 만들어진 수열에서 짝수 합계를 구한다

위의 리스트들은 순서대로, 1)항목을 검증하는 테스트 코드 작성 -> 2) 테스트를 통과하는 코드 작성 의 형태로 반복해가면서 todo 항목들을 지워가는 과정인데.. 아무리 복잡하고 어려운 문제도 직관적으로 해결할 수 있게 해주는 좋은 생각의 틀(framework)이라고 생각합니다.

추가적으로 제가 해법 정리를 위해 주로 사용하는 방법은 최종적으로 답을 주는 메소드를 의사 코드(pseudo code)로 만들고, 그 안에서 호출되는 메소드들을 점진적으로 구현해가는 방법입니다. 이번 문제에서 제가 만든 high-level 코드는 아래와 같습니다.

sequence = natural_numbers.convert_fibonacci_sequence.take(n < max)
sequence.sum_even_numbers 

IV. 드디어 문제 해결~

자 그럼 하나씩 todo list를 제거해 나가 봅시다.

1. n번째 피보나치 수를 구하는 기능 만들기

먼저 테스트를 만듭니다. test 폴더에 test_fibonacci_calculator.rb 파일을 아래와 같이 만듭니다.

require "minitest/autorun"
require_relative "../lib/fibonacci_calculator"

class TestFibonacciCalculator < Minitest::Test
  def test_fibonacci_number
    assert_equal 1, FibonacciCalculator.number(1)
    assert_equal 2, FibonacciCalculator.number(2)
  end
end

그리고 테스트를 돌립니다. FibonacciCalculator라는 클래스가 없으니 당연히 에러가 납니다. 이번엔 lib 폴더에 얼른 테스트만 통과하는 코드를 추가합니다.

module FibonacciCalculator
  module_function

  def number(n)
    n
  end
end

드디어 테스트가 통과되었습니다!

기왕 테스트를 만들 것 같으면 제대로 된 로직 검증을 위해 많은 케이스를 추가하지, 왜 장난하는 것도 아니고 n = 1,2일때만 동작하는 테스트를 만들어 놓고 그것만 만족하는 단순한 코드만 만드는 이유가 뭘까요?
실은 여기에 TDD의 묘미가 있습니다. 테스트 없이 코딩을 하다보면 알고리즘에 집중을 하기 어렵게 만드는 잡생각들이 계속 들게 마련인데.. (‘로직과 관계 없이 지금 코드에 실수가 있으면 어떻게 하나’ 같은 불안감 말이죠.) 일단 기본적인 틀이 동작한다는 걸 확인하고 나서 안심을 먼저 한 상태에서는 중요한 알고리즘만 고민할 수 있으니 집중도 더 잘 되고 마음도 편안합니다. Kent Beck 같은 거물 개발자도 이런 바보같아 보이는 방법을 고수하는데에는 이유가 있습니다.

자, 그럼 이제 제대로 된 테스트를 추가해봅시다. 이전에 이미 두 줄이나 중복된 테스트 코드를 넣었는데 또 넣으려니 찜찜하죠? 조금 더 깔끔하게 추가해봅시다. 기왕 중복이 없어진 김에 테스트할 숫자도 늘려봅시다.

class TestFibonacciCalculator < Minitest::Test
  FIBONACCI_SAMPLES = [1, 2, 3, 5, 8, 13, 21, 34, 55].freeze
  MAX_NUMBER = FIBONACCI_SAMPLES.last

  def test_number
    FIBONACCI_SAMPLES.each.with_index(1) { |n, i| assert_equal n, FibonacciCalculator.number(i) }
  end
end

보통 #each에 #with_index를 또 호출하는 방법보다는 #each_with_index가 효율적으로 동작하기 때문에 후자를 선호합니다만, 이 경우 인덱스의 초기값을 정해줄 수 없다는 단점이 있어 .each.with_index 형태로 호출했습니다.
테스트 코드가 추가되자마자 다시 테스트는 실패로 바뀌게 됩니다. 얼른 lib의 코드를 고쳐봅시다. 이번엔 피보나치 함수를 제대로 구현해보죠.

module FibonacciCalculator
#...
  def number(n)
    n <= 2 ? n : number(n - 1) + number(n - 2)
  end
#...
end

이제 피보나치 숫자가 제대로 구해지는 것을 확인할 수 있습니다. 첫번째 todo 가 해결되었습니다!

2. 피보나치 수열을 계산하는 기능 추가

마찬가지로 테스트부터 추가해봅시다.

class TestFibonacciCalculator < Minitest::Test
#...
  SAMPLE_SIZE = FIBONACCI_SAMPLES.size
#...
  def test_series
    assert_equal FIBONACCI_SAMPLES, FibonacciCalculator.series(SAMPLE_SIZE)
  end
#...
end

그리고 테스트를 성공시키기 위한 코드를 추가합니다.

module FibonacciCalculator
#...
  def series(n)
    (1..n).map { |i| number(i) }
  end
#...
end

됐군요!

3. 수열을 만드는 조건을, 마지막 값이 max 값을 넘지 않는 것으로 한정

테스트 코드를 추가합니다.

class TestFibonacciCalculator < Minitest::Test
#...
  def test_series_less_than
    assert_equal FIBONACCI_SAMPLES, FibonacciCalculator.series_less_than(MAX_NUMBER)
  end
#...
end

사실 2번에서부터 하고 싶었던 일인데.. 제가 처음에 보여드렸던 의사 코드를 보면 알겠지만.. (무한대의) 자연수 수열에서 피보나치를 구하고 그 안에서 원하는만큼 선택을 한다는 개념으로 찾는 게 의미가 깔끔하지 않나 싶습니다. 그럼 이렇게 구현할 수 있겠습니다.

module FibonacciCalculator
#...
  def series_less_than(max_number)
    natural_numbers.map { |i| number(i) }.take_while { |n| n <= max_number }
  end
#...
end

자 그럼 무한한 자연수는 어떻게 구할까요? 간단합니다.

def natural_numbers
  (1..Float::INFINITY)
end 

왜 숫자 클래스(Fixnum)이 아니고 부동 소수점(Float)이냐구요? 정수의 무한대는 Bignum 을 쓰기 때문에 좀 문제가 있습니다.^^; 그런데 그냥 이렇게 처리했다간 무한루프에 빠지고 맙니다. 무한대의 숫자까지 range가 주어졌으니 당연한 일이죠.
하지만 Ruby 2.0 부터 추가된 Lazy Enumerator라는 마법같은 인터페이스를 이용하면 머리 속으로만 될 것 같았던 기능을 실현할 수 있습니다. #lazy 메소드를 호출하면 나중에 #force 메소드가 호출될 때까지 실제 계산을 유보하는 특수한 Enumerator를 리턴해줍니다. 이것을 활용하면 아래와 같이 만들 수 있습니다. (기왕 하는 김에 아까 만들었던 #series도 조금 더 직관적으로 바꿔봅시다. 아까 만들어둔 테스트 코드가 있으니 고치다 실수가 난다해도 바로 알 수 있기 때문에, 안심하고 기존 코드도 손을 댈 수 있습니다.)

module FibonacciCalculator
#...
  def series(n)
    natural_numbers.map { |i| number(i) }.first(n)
  end

  def series_less_than(max_number)
    natural_numbers.map { |i| number(i) }.take_while { |n| n <= max_number }.force
  end

  def natural_numbers
    (1..Float::INFINITY).lazy
  end
#...
end

아름답게 해결이 되었습니다. 참고로, #series에서는 #force 메소드가 호출되지 않았는데도 결과가 잘 나옵니다. #first, #reduce 등의 메소드는 자신이 호출되는 시점에서 더 lazy하게 연산을 미룰 수 없게 되기 때문에 내부적으로 #force를 호출해주기 때문입니다. 즉, 아래의 두 코드는 완전히 동일한 역할을 합니다.

(1..Float::INFINITY).lazy.first(5) # => [1, 2, 3, 4, 5]

(1..Float::INFINITY).lazy.take(5).force # => [1, 2, 3, 4, 5]

4. 만들어진 수열에서 짝수 합계를 구한다

자, 이제 마지막 테스트 코드를 추가해봅시다.

class TestFibonacciCalculator < Minitest::Test
#...
  def test_even_sum
    assert_equal 0, FibonacciCalculator.even_sum(1)
    assert_equal 2, FibonacciCalculator.even_sum(3)
    assert_equal 44, FibonacciCalculator.even_sum(MAX_NUMBER)
  end
#...
end

짝수합을 구하는 코드는 단순히 해결이 될 것 같습니다.

module FibonacciCalculator
#...
  def even_sum(max_number)
    series_less_than(max_number).select(&:even?).reduce(:+)
  end
#...
end

지난 포스팅에서 얘기했던 map/reduce 구조가 나옵니다. 참 쉽죠?
그런데 여전히 테스트는 실패합니다. max_number가 1일 때 결과값이 nil 이 나와버리는군요. ruby의 경우 항상 nil이 나올 수 있는 경우에 대한 상황을 염두에 두고 있어야 합니다. 아닌 경우 낭패를 볼 경우가 많습니다.

series_less_than(max_number).select(&:even?).reduce(:+) || 0

간단하게 || 0 을 추가하는 것으로 쉽게 해결이 되겠군요. =)
이렇게 해서 완성된 클래스는 아래와 같습니다. 매우 단순하게 해결이 되었습니다!

module FibonacciCalculator
  module_function

  def number(n)
    n <= 2 ? n : number(n - 1) + number(n - 2)
  end

  def series(n)
    natural_numbers.map { |i| number(i) }.first(n)
  end

  def series_less_than(max_number)
    natural_numbers.map { |i| number(i) }.take_while { |n| n <= max_number }.force
  end

  def even_sum(max_number)
    series_less_than(max_number).select(&:even?).reduce(:+) || 0
  end

  def natural_numbers
    (1..Float::INFINITY).lazy
  end
end

마지막으로 아래와 같이 호출하면 문제의 해답이 나옵니다.

p FibonacciCalculator.even_sum(400_000)

어떻습니까. 솔직히 최적의 속도로 실행되는 코드는 아니지만 기능들이 잘 쪼개져 있습니다. 물론 효율만 생각하면 #number, #even_sum 두 개의 메소드로 축약하는 것도 가능합니다만, 그에 비해 이렇게 하면 이런 이점들이 있습니다.

  • 높은 가독성: 제일 상위 메소드의 내용만 보면 전체 흐름이 금방 이해가 가고, 더 low-level한 내용은 하위 메소드를 파고들어가면서 단계적으로 이해할 수 있으니 타인이 보기에도 몇 달 뒤의 내가 보기에도 편합니다.
  • 테스트 가능성: 전체 기능이 구현이 안 된 상태에서 단계적으로 조금씩 조금씩 테스트를 하면서 붙여나갈 수 있습니다.
  • 최적화 용이성: 루비 VM은 이전에 비해 비약적으로 성능이 개선되긴 했지만 여전히 타 언어에 비해 느리기 때문에 항상 최적화에 열려있어야 합니다. 기능이 잘 쪼개져 있으면 벤치마크를 통해 어디가 가장 시간이 오래걸리는지도 쉽게 감지할 수 있고 꼭 필요한 부분만 최적화를 시킬 수 있어 코드의 가독성이 크게 떨어지지 않는 방향으로 코드를 손 볼 수 있습니다.

제가 세 번의 긴 글을 통해 얘기하고자 하는 결론은 이겁니다.
제가 생각하는 좋은 루비 코드 스타일, 말하는 듯한 코딩은 머리 속 컴파일러를 발동시키지 않는 잘 추상화 된 high-level한 코드, 그리고 그를 뒷받침 해주는 잘 분산된 클래스/메소드 구조라고 생각합니다. 끗!

V. 번외편: 또다른 좋은 해법들

말씀 드렸듯이 제가 제시한 해법은, 최대한 실제 코드에서 하는 것과 같은 메소드 분포를 갖도록 하는 데에 주안점을 뒀습니다.

근데 피보나치 합 같은 문제는 간단하게 짝수 합만 구하는 것이니 그냥 해법 자체에만 집중해서 문제를 풀어봐도 되지 않을까요? ^^;

(한 때 세계에서 가장 큰 Ruby on Rails 애플리케이션 중 하나였던) KakaoStory 팀에서 활약하고 있는 김기용 군은 이런 해법을 보여주더군요. 훌륭한 해법입니다!

e = Enumerator.new do |yielder|
  a, b = 1, 2
  loop do
    yielder << a
    a, b = b, a + b
  end
end.lazy

p e.take_while { |i| i < 400_000 }.select(&:even?).reduce(:+)

laziness와 generator를 동시에 사용한 아름다운 방법입니다.

아래와 같이 피보나치 수를 구하는 brilliant한 해법도 있습니다.

fibonacci = Hash.new { |h,k| h[k] = k <= 2 ? k : h[k-1] + h[k-2] }

fibonacci[6] # => 13
fibonacci[50] # => 20365011074

훌륭합니다! 익명 함수를 이용해서 피보나치를 구하는 지극히 루비스러운 해법입니다. 게다가 같은 값이 두번째 호출될 때부터는 함수 호출이 아닌 저장된 값을 쓰기 때문에 속도도 훨씬 빠릅니다. (이를 이용한 이후 구현은 동일하니 생략합니다.^^)

자, 그럼 마지막으로 지난 시간의 문제를 한 번 풀어보겠습니다. 어떻게하면 단 한 줄로 피보나치 짝수합을 구할 수 있을까요?

아까 만든 FinbonacciCalculator.even_sum에서 호출되는 함수들을 모두 합치면 대충 되지 않을까요? 거의 될 것 같은데.. number 함수까지 한 줄로 만들기가 조금 애매합니다.

(1..Float::INFINITY).lazy.map { |i| number(i) }.take_while { |n| n <= 400_000 }.select(&:even?).reduce(:+)

그럼 방향을 바꿔봅시다. #with_object 메소드를 이용해서 초기값인 [1, 2]를 미리 배열에 넣어주고 그걸 버퍼로 활용하면 될 것 같습니다.

(1..Float::INFINITY).lazy.with_object([1, 2]).map { |_, last| last[1] = last[0] + (last[0] = last[1]); last[0] }.take_while { |x| x <= 400_000 }.select(&:even?).reduce(:+)

정신없죠? 조금 줄을 나눠볼까요?

(1..Float::INFINITY).lazy.
  with_object([1, 2]).
  map { |_, last| last[1] = last[0] + (last[0] = last[1]); last[0] }.
  take_while { |x| x <= 400_000 }.
  select(&:even?).
  reduce(:+)

map 안의 block 파라미터의 첫번째는 1부터 무한대의 자연수가 Fixnum 형태로 1씩 증가하며 주어지게 되는데, 실제 계산에서는 사용하지 않는 변수이므로 _로 처리했습니다. 그리고 두 번째 last의 경우 with_object에서 정의한 배열을 의미합니다. last 배열에 피보나치 숫자를 구하기 위한 마지막 피보나치 수와 그 전 숫자를 저장해놓고 이를 이용해 map으로 수열을 계속 붙여나갑니다.
아.. 그렇구나.. 라고 생각하는데 뭔가 이상합니다. 분명 한 라인이라고 했는데 map 안에 세미콜론(;)이 있군요. 이거 사기 아녜요? 그런 식이면 열 줄짜리 코드를 만들어 놓고 세미콜론으로 이어도 되는거잖아요? 맞습니다. 죄송합니다..
하지만 방법은 있습니다. 바로 지난 포스팅에서 말씀드렸던 #tap이라는 쓸데 없는 마법의 메소드입니다! #tap은 단순히 self를 리턴해주는 함수가 아니라, block이 실행된 후의 self 값을 리턴해주는 메소드입니다. 그러므로 아래의 두 코드는 완전히 같은 코드입니다.

last[1] = last[0] + (last[0] = last[1]); last[0]

last[0].tap { last[1] = last[0] + (last[0] = last[1]) }

자 드디어 40만을 넘지 않는 피보나치 수의 짝수 합의, 단 한 줄 구현이 완성 됐습니다~ =)

(1..Float::INFINITY).lazy.with_object([1, 2]).map { |_, last| last[0].tap { last[1] = last[0] + (last[0] = last[1]) } }.take_while { |x| x <= 400_000 }.select(&:even?).reduce(:+)

그럼 다음 포스팅에서는 Ruby on Rails 코딩에서 종종 맞닥뜨리는 루비의 독특한 문법에 대해서 함께 생각해보겠습니다.