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

코드를 엉망으로 만들지 않는 Ruby 몽키 패치 3가지 방법

몽키 패치(Monkey Patching). Ruby를 처음 접하는 순간, 그 강력함에 매료됩니다. 핵심 클래스에 메서드를 바로 추가할 수 있으니까요! Time.now.advance(days: -1)처럼 길게 쓰지 않아도 1.day.ago 한 줄이면 끝입니다. 덕분에 Ruby 코드는 읽기도 쓰기도 즐겁습니다. 하지만…

패치가 Hash를 변경하면서 이상한 버그가 튀어나오기 시작합니다.

어떤 코드가 실제로 실행됐는지 파악할 수 없어서, 문제가 생겼을 때 디버깅조차 어렵습니다.

그리고 결국 깨닫게 됩니다. 모든 문제의 원인은 6개월 전, 단 한 줄의 코드를 다섯 글자 줄여보겠다고 Enumerable을 몽키 패치했던 그 순간이었다는 사실을요.

그렇다면 대안은 무엇일까요? 편의성을 아예 포기해야 할까요? GroupableArray.new([1, 2, 3, 4]).in_groups_of(2) 같은 코드를 감수해야 할까요? blank?가 Ruby 코어에 들어올 때까지 사용자 입력 처리를 포기해야 할까요?

몽키 패치를 완전히 버릴 필요는 없습니다. 문제는 어떻게 해야 나중에 자신의 코드를 보고 "내가 왜 이런 짓을 했지?"라며 자책하지 않는 몽키 패치를 작성할 수 있는가입니다.

1. 모듈에 담아라

클래스를 몽키 패치할 때 클래스를 다시 열어(reopen) 그냥 밀어 넣는 식으로 작성하면 안 됩니다:

class DateTime
  def weekday?
    !sunday? && !saturday?
  end
end

왜일까요?

  • 두 라이브러리가 같은 메서드를 몽키 패치하면 서로를 인식하지 못합니다.

    먼저 적용된 패치는 조용히 덮어씌워져 영원히 사라집니다.

  • 에러가 발생하면 마치 DateTime 내부에서 문제가 생긴 것처럼 보입니다.

    기술적으로는 맞는 말이지만, 원인 추적에는 거의 도움이 되지 않습니다.

  • 몽키 패치를 끄기가 훨씬 번거로워집니다.

    패치 없이 코드를 실행하려면 전체를 주석 처리하거나, 해당 파일을 로드하지 않도록 분기 처리해야 합니다.

  • 실수로 require 'date'를 빠뜨리면, 패치가 아니라 DateTime 클래스 자체를 재정의해 버립니다.

대신 몽키 패치를 모듈에 넣으세요:

module CoreExtensions
  module DateTime
    module BusinessDays
      def weekday?
        !sunday? && !saturday?
      end
    end
  end
end

이렇게 하면 관련된 패치들을 함께 묶어 정리할 수 있습니다. 에러가 발생했을 때도 문제의 코드가 어디에서 왔는지 명확하게 드러납니다. 무엇보다 그룹별로 하나씩 적용할 수 있습니다:

# 실제로 DateTime을 몽키 패치
DateTime.include CoreExtensions::DateTime::BusinessDays

더 이상 패치가 필요 없다면 이 한 줄만 주석 처리하면 끝입니다.

2. 한 곳에 모아라

핵심 클래스를 몽키 패치한다는 것은 Ruby의 기본 API를 확장하는 일입니다. 코어 패치를 사용하는 앱마다 고유의 분위기가 생기죠. 따라서 새로운 코드베이스에 투입되었을 때 빠르게 이 변화들을 학습할 수 있는 방법이 반드시 필요합니다. 몽키 패치가 어디에 살고 있는지 알아야 합니다.

저는 주로 Rails의 몽키 패치 컨벤션을 따릅니다. 패치는 lib/core_extensions/class_name/group.rb 경로에 위치시킵니다. 예를 들어 위의 패치는:

module CoreExtensions
  module DateTime
    module BusinessDays
      def weekday?
        !sunday? && !saturday?
      end
    end
  end
end

lib/core_extensions/date_time/business_days.rb 파일에 들어갑니다.

새로 합류한 개발자라면 lib/core_extensions 디렉터리의 파일만 훑어봐도 여러분이 Ruby에 무엇을 추가했는지 파악할 수 있습니다. 그 결과 여러분이 만든 편리한 메서드들이 오히려 걸림돌이 되는 게 아니라 실제로 활용되기까지 합니다.

3. 엣지 케이스까지 생각하라

Enumerablesum 메서드가 없는 이유는 저도 모릅니다. [1, 2, 3].sum, ["a", "b", "c"].sum, [Article.new, Article.new, Article.new].sum… 아, 잠깐.

클래스를 몽키 패치할 때 우리는 보통 지금 당장 편하게 만들고 싶은 '한 가지'에만 집중합니다. 숫자의 합을 구하고 싶은데, 배열이 숫자 외의 것도 담을 수 있다는 사실을 잊어버리는 겁니다.

당장은 문제없어 보입니다. 여러분 자신은 해시들의 평균을 계산할 일이 없을 테니까요. 하지만 어떤 상황에서는 호출 시 그냥 실패해 버리는 메서드를 객체에 붙여두면, 나중에 스스로를 혼란스럽게 만듭니다.

이 문제는 몇 가지 방법으로 대응할 수 있습니다. 좋은 순서대로 나열하면 다음과 같습니다:

  • 예상 못한 입력을 합리적으로 처리하세요.

    패치가 문자열을 다룬다면 특히 잘 통하는 방법입니다. to_s를 먼저 호출하면 거의 모든 객체에서 그럴듯한 값을 얻을 수 있습니다. 『Confident Ruby』 책은 다양한 종류의 입력을 다루는 방법을 아주 많이 알려줍니다.

  • 에러를 더 명확하게 드러내세요.

    예상 밖의 입력을 만나면 좋은 메시지와 함께 ArgumentError를 던지는 것만으로도 충분할 수 있습니다. 다른 사람이 의미 불명의 NoMethodError를 해석하도록 방치하지 마세요.

  • 주석으로 기대하는 입력의 형태를 문서화하세요.

    위의 두 방법이 가능하다면 그것이 더 좋습니다. 하지만 패치 내부에서 엣지 케이스를 검사할 수 없다면, 최소한이라도 문서로 남기세요. 호출자가 문제의 원인이 여러분의 패치임을 알아냈을 때, 최소한 여러분의 의도가 무엇이었는지는 짐작할 수 있게 됩니다.

역대 최애 몽키 패치

마지막으로 제가 가장 아끼는 몽키 패치, Hash#string_merge를 남기며 마무리하겠습니다:

lib/core_extensions/hash/merging.rb
module CoreExtensions
  module Hash
    module Merging
      def string_merge(other_hash, separator = " ")
        merge(other_hash) {|key, old, new| old.to_s + separator + new.to_s}
      end
    end
  end
end

{}.string_merge({:class => "btn"}) # => {:class=>"btn"}

h = {:class => "btn"} # => {:class=>"btn"}
h.string_merge({:class => "btn-primary"}) # => {:class=>"btn btn-primary"}

HTML 요소에 CSS 클래스를 붙이는 작업이 훨씬 깔끔해집니다.

합리적인 몽키 패칭

핵심 클래스의 몽키 패칭이 나쁘기만 한 것은 아닙니다. 잘 활용하면 코드가 더 'Ruby답게' 느껴지게 만들어 줍니다. 다만 Ruby의 날카로운 도구가 그렇듯, 사용할 때는 각별한 주의가 필요합니다.

패치를 한곳에 모아 관리하고, 모듈로 그룹화하며, 예상 못한 상황까지 대비한다면 여러분의 몽키 패치는 가능한 한 안전한 상태를 유지할 수 있습니다.

여러분이 직접 작성했거나(혹은 목격한) 가장 훌륭한 몽키 패치는 무엇인가요? 댓글로 알려주세요!