짧은 코드 한 줄이 주는 영감 - try() 놀이
March 14th, 2008
아주 짧은 코드가 감흥을 주는 경우가 있다. 내가 생각하지 못했던 참신한 아이디어를 봤을 때 나도 모르게 감탄을 하게 되는데 전에 한번 소개했던 Symbol#to_proc이나 Object#tap - 쉬어가기 메서드 등이 그런 경우였다. 보면 기분 좋아지는 코드들이 모여있는 것이 Ruby Facets라 Have Fun! 루비 - 루비 세미나 4회 발표 후기에서 이를 강력 추천한 일도 있었다.
최근에도 그런 코드를 하나 발견해 여기서 소개한다.
원조 try()
- class Object
def try(method)
send method if respond_to? method
end
end
위 코드가 어떻게 도움이 될까? 다음 코드를 보자.
- user = User.find_by_email(email)
user.destroy if user
이런 코드를 try를 이용하면 다음처럼 간결하게 표현할 수 있다.
- User.find_by_email(email).try(:destroy)
이제 메서드 연쇄의 재미를 더 살릴 수 있을 것 같다. 특히 유용한 경우는 레일스 뷰 템플릿에서다. @person ? @person.name : nil라고 쓰는 대신 @person.try(:name)라고면 써주면 되니까 말이다. 스프링노트에서는 이런 코드를 두고 여러 곳에서 사용한다.
- def try(method, default = nil)
respond_to?(method) ? send(method) : default
end
nil 대신 뷰에서 사용할 기본값을 넘겨주도록 수정했다.
더 재미있는 현상은 더 나은 try()라면서 들고나온 루비 커뮤니티의 반응이다. 조금씩 개선을 하는 모습들이 꽤나 재밌다.
NilClass#method_missing
이 코드는 try()보다 먼저 나온 코드지만, 비슷한 결과를 보여준다.
- class NilClass
def method_missing(*args)
nil
end
end
마치 레일스의 Whiny Nil처럼 NilClass#method_missing을 재정의해서 모든 메서드 호출 에러를 무시하도록 하고 있다. 그래서 nil인지 아닌지 검사할 필요없이 자연스럽데 메서드 연결을 하겠다는 의도다. person.name.upcase처럼 말이다.
그렇지만 별로 좋은 코드는 아니고, 오히려 위험한 코드에 가깝다. 차라리 NoMethodError 예외를 잡아서 처리하는 접근이 더 나은 것 같다. 비추하나 날려주고 다음 코드로~
respond_to?는 잘못된 해석
애초에 풀려던 문제(@person ? @person.name : nil)와 답이 틀렸다는 주장도 있다. try라는 이름에 걸맞을려면 respond_to?보다는 객체가 닐인지만 검사하는게 더 나은 구현이라는 것이다. 코드는 이렇다.
- class Object
def try(method)
self.send(method) unless self.nil?
end
end
은근히 설득력 있다. 동적으로 메서드를 처리하는 경우도 많으니 이 코드가 더 나을 수도 있겠다.
그루비 방식
사실 그루비에는 이런 기능이 Safe navigation이라는 이름으로 언어 자체에 포함되어 있다. 이런 식이다.
- def foo = null
def bar = foo?.something?.myMethod()
assert bar == null
그래서 루비에서도 이 문법을 차용해오자는 의견도 있다. 구현은 method_missing을 이용해 처리해줄 수 있을 것 같다. 재미있는 생각이다. 같은 글에 있는 또 다른 아이디어는 이 메서드의 결과는 nil이 될 수도 있으나 이는 일반적인 경우이니 상관없다는 의미로 _를 붙이자는 것도 있다. 그다지 좋은 방법은 아닌 것 같지만 그래도 같은 문제를 두고 참 많은 생각을 하는구나 싶다.
- @person.company_.name_
중첩과 기본값을 추가
이번에는 try를 여러번 적용하는 경우다. 여기에 기본값도 더해졌다. 예를 들어 이런 코드가 있다고 하자.
- @person.try(:address).try(:street) || 'unknown'
위 코드를 이렇게 바꿔쓸 수 있다.
- @person.try(:address, :street, :default => 'unkown')
아이러니하게도 더 길어졌다. 어떤게 더 의도를 잘 드러내는지는 노코멘트. 뭐 한 5번 중첩되면 이 버전이 유용할 것 같기는 하다.
- class Object
def try(*args)
options = {:default => nil}.merge(args.last.is_a?(Hash) ? args.pop : {})
target = self # Initial target is self.
while target && mtd = args.shift
target = target.send(mtd) if target.respond_to?(mtd)
end
return target || options[:default]
end
end
비평
마지막으로 Never Send a Symbol to do a Method's Job이라는 글은 이런 시도에 대한 비판적인 시각도 볼 수 있다. 객체의 메서드를 호출하는 기본적인 문법을 깨뜨리지말자는 것이다. 루비처럼 동적인 언어로 개발을 할 때 어느 정도 선이 '읽기 수월한 좋은 코드'인지는 논란의 여지가 있다. 이런 시각도 있고, 저런 시각도 있고. 그래서 팀 내에서 합의가 이뤄진 DSL(예를 들면 루비 온 레일스)가 중요한 것일테지만 말이다.
프로그래밍 놀이
어떤 날은 하루에 천 줄의 코드를 만들 수도 있다. 또 어떤 날은 한 줄의 코드를 만들기가 쉽지 않을 때도 있다. 코드란 양이라는 통계로는 측정할 수 없는 어떤 것이라는 생각이 든다. 그래서 더 재미있는 일일테지만 말이다.




March 17th, 2008 at 01:54 AM (myRuby.net) NilClass#method_missing 딱 보고그 심플한 해결방식에 '호오~' 했다가,좀더 고민해보고 '훠이~'하게 되네요 ^^;;
March 17th, 2008 at 01:56 AM 루비는 볼때마다 느끼는게, 대중화 되기엔 너무 철학적임. 3개월 과정으로 뚝딱 해치우기엔 아직 너무 어려운거 아닐까요?
March 18th, 2008 at 12:39 PM 내일 탄천 번개할건데 나와요.
March 21st, 2008 at 02:14 AM 영감~
April 1st, 2008 at 07:05 AM 예전 마소의 1라인 콘테스트가 생각납니다. 석기시대 얘기지만… [글보러가기]