몽키 패치(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. 엣지 케이스까지 생각하라
Enumerable에 sum 메서드가 없는 이유는 저도 모릅니다. [1, 2, 3].sum, ["a", "b", "c"].sum, [Article.new, Article.new, Article.new].sum… 아, 잠깐.
클래스를 몽키 패치할 때 우리는 보통 지금 당장 편하게 만들고 싶은 '한 가지'에만 집중합니다. 숫자의 합을 구하고 싶은데, 배열이 숫자 외의 것도 담을 수 있다는 사실을 잊어버리는 겁니다.
당장은 문제없어 보입니다. 여러분 자신은 해시들의 평균을 계산할 일이 없을 테니까요. 하지만 어떤 상황에서는 호출 시 그냥 실패해 버리는 메서드를 객체에 붙여두면, 나중에 스스로를 혼란스럽게 만듭니다.
이 문제는 몇 가지 방법으로 대응할 수 있습니다. 좋은 순서대로 나열하면 다음과 같습니다:
예상 못한 입력을 합리적으로 처리하세요.
패치가 문자열을 다룬다면 특히 잘 통하는 방법입니다.
to_s를 먼저 호출하면 거의 모든 객체에서 그럴듯한 값을 얻을 수 있습니다. 『Confident Ruby』 책은 다양한 종류의 입력을 다루는 방법을 아주 많이 알려줍니다.에러를 더 명확하게 드러내세요.
예상 밖의 입력을 만나면 좋은 메시지와 함께
ArgumentError를 던지는 것만으로도 충분할 수 있습니다. 다른 사람이 의미 불명의NoMethodError를 해석하도록 방치하지 마세요.주석으로 기대하는 입력의 형태를 문서화하세요.
위의 두 방법이 가능하다면 그것이 더 좋습니다. 하지만 패치 내부에서 엣지 케이스를 검사할 수 없다면, 최소한이라도 문서로 남기세요. 호출자가 문제의 원인이 여러분의 패치임을 알아냈을 때, 최소한 여러분의 의도가 무엇이었는지는 짐작할 수 있게 됩니다.
역대 최애 몽키 패치
마지막으로 제가 가장 아끼는 몽키 패치, Hash#string_merge를 남기며 마무리하겠습니다:
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의 날카로운 도구가 그렇듯, 사용할 때는 각별한 주의가 필요합니다.
패치를 한곳에 모아 관리하고, 모듈로 그룹화하며, 예상 못한 상황까지 대비한다면 여러분의 몽키 패치는 가능한 한 안전한 상태를 유지할 수 있습니다.
여러분이 직접 작성했거나(혹은 목격한) 가장 훌륭한 몽키 패치는 무엇인가요? 댓글로 알려주세요!