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

Ruby rescue의 숨겨진 비밀: === 연산자로 동적 예외 매처 만들기

Ruby에서 예외 처리를 다뤄본 개발자라면 rescue에 특정 예외 클래스를 지정해 원하는 예외만 잡을 수 있다는 것을 알고 있을 겁니다.

begin
  raise ArgumentError
rescue ArgumentError
  # `ArgumentError`를 잡습니다
end

또한 "부모" 클래스를 rescue하면 그 "자식" 클래스들까지 모두 함께 잡히는 것도 익숙할 것입니다.

begin 
  raise ArgumentError
rescue StandardError
  # `ArgumentError`는 `StandardError`를 상속하므로
  # 함께 잡힙니다
end

여기서 말하는 "부모"와 "자식"은 단순히 클래스 상속 관계를 의미합니다. Ruby 소스 코드 어딘가에는 다음과 같은 정의가 존재합니다.

class ArgumentError < StandardError
   ...
end

흥미로운 트릭

그렇다면 이런 질문이 생깁니다. Ruby는 어떻게 주어진 예외가 지정한 클래스를 상속하는지 알아낼 수 있을까요?

가장 직관적인 방법은 is_a?kind_of? 메서드를 사용하는 것입니다. 내부 구현을 상상해 보면 대략 이런 모습일 겁니다.

if the_exception.is_a?(StandardError)
   # rescue 실행
end

하지만 실제 Ruby의 동작은 다릅니다. Ruby는 더 흥미로운 === 연산자를 사용합니다.

if StandardError === the_exception
   # rescue 실행
end

a === b를 사용해 본 적이 없다면, 이 연산자는 보통 "b가 a가 정의한 그룹에 본질적으로 속하는가?"라는 질문에 답합니다. 몇 가지 예시를 살펴보겠습니다.

(1..10) === 5             # true
('a'..'f') === "z"        # false

String === "hello"        # true
String === 1              # false

/[0-9]{3}/ === "hello123" # true
/[0-9]{3}/ === "hello"    # false

=====처럼 그저 평범한 Ruby 메서드일 뿐이므로, 우리가 직접 정의할 수도 있습니다.

class RedThings
   def self.===(thing)
     thing.color == :red
   end
end

여기서 두 가지 사실을 알 수 있습니다. 첫째, rescue===를 사용해 어떤 예외를 잡을지 판단합니다. 둘째, 우리는 === 메서드를 직접 정의할 수 있습니다. 즉, 어떤 예외를 잡을지 실행 시점에 동적으로 결정하는 클래스를 만들 수 있다는 뜻입니다.

class SevereMatcher
  def self.===(exception)
    exception.message =~ /severe/    
  end
end

begin
  raise RuntimeError, "Something severe happened"
rescue SevereMatcher
  # 클래스와 무관하게, 메시지에 "severe"라는 단어가
  # 포함된 모든 예외를 잡습니다.
end

이 트릭을 일단 알고 나면, 한계는 오직 여러분의 상상력뿐입니다.

마치며

솔직히 인정하건대, 실제 프로젝트에서 동적 예외 매처를 만들 일은 많지 않을 수 있습니다. 하지만 kind_of? 대신 ===를 사용하는 것 같은 사소해 보이는 구현 디테일 하나가 Ruby를 얼마나 유연하고 흥미로운 언어로 만드는지 보여주는 훌륭한 예시입니다. 언어의 내부 동작을 이해하면 이런 우아한 해결책을 떠올릴 수 있게 되니, 평소 접하는 API의 동작 원리를 깊이 파헤쳐 보는 습관을 들여볼 만합니다.