루비(Ruby)는 의도적으로 일부 오류와 예외를 개발자에게서 숨기곤 합니다.
때로는 이런 동작이 유용할 수 있습니다.
예를 들어 블록과 함께 Kernel#loop 메서드를 사용하면, StopIteration 예외가 발생했을 때 loop가 자동으로 종료됩니다.
하지만 다른 경우에는 디버깅 작업을 훨씬 더 어렵게 만들 수 있습니다.
구체적인 예시를 통해 살펴보겠습니다!
숨겨진 예외 #1: Comparable 모듈과 <=> 메서드
첫 번째 예시는 Comparable 모듈과 <=> 메서드와 관련이 있습니다.
예시 코드입니다:
class MyObject
attr_accessor :value
include Comparable
def initialize(value)
@value = value
end
def <=>(other)
raise ArgumentError, "can't compare #{other.class} with #{self.class}" unless other.is_a?(MyObject)
value <=> other.valuee
end
end
mo1 = MyObject.new(10)
mo2 = MyObject.new(10)
p mo1 == mo2
이 예시를 단계별로 분석해 보겠습니다.
먼저:
MyObjectvalue라는 attr_accessor가 하나 있으며, Comparable 모듈을 포함하고 있습니다. 이 모듈은 비교 메서드(==, <, > 등)를 클래스에 추가해 줍니다.
이러한 비교 메서드들은 모두 <=> 메서드를 기반으로 동작합니다.
Enumerable의 메서드들이 each 메서드를 기반으로 하는 것과 같은 원리입니다.
그다음:
같은 값(10)을 가진 두 객체(MyObject.new)를 생성합니다.
값이 같더라도 두 객체는 서로 다른 객체라는 점에 주목하세요. 이 부분이 중요합니다.
이제 이 두 객체 mo1과 mo2를 비교하면 false가 반환됩니다...
왜 그럴까요?
<=> 메서드에 오류가 있는데, 루비가 그 오류를 숨기고 있기 때문입니다!
코드를 잘 살펴보세요...
오류를 찾을 수 있나요?
찾았다면 훌륭합니다! 못 찾았어도 괜찮습니다 🙂
바로 여기 있습니다:
value <=> other.valuee
valuee가 보이시나요?
사실 오타가 있었던 것입니다!
원래대로라면 NoMethodError 예외가 발생해서 문제를 금방 알아챘을 것입니다. 하지만 이 예시에서는 그렇지 않습니다.
다행히도 Ruby 2.3부터는 이 동작이 변경되었습니다. 이제 오류가 더 이상 숨겨지지 않으므로 직접 확인할 수 있습니다.
아직 구버전 루비를 사용하고 있다면 업그레이드해야 할 또 하나의 이유가 되겠네요.
숨겨진 예외 #2: Numeric 객체와 coerce 메서드
숨겨진 예외의 또 다른 예시는 Numeric 객체(Float, Integer)와 coerce 메서드의 조합에서 나타납니다.
예시 코드입니다:
class MyObject
attr_accessor :value
def initialize(value)
@value = value
end
def +(other)
other = MyObject.new(other) if other.kind_of?(Numeric)
value + other.value
end
def coerce(other)
mo = MyObject.new
mo.valuee = other
[mo, self]
end
end
mo1 = MyObject.new 10
mo2 = MyObject.new 10
p mo1 + mo2
# 20
p mo1 + 20
# 30
이번에도 MyObject 클래스지만, 새로운 메서드인 +와 coerce가 추가되었습니다.
coerce 메서드는 호환되지 않는 두 타입을 같은 타입으로 변환하여 함께 사용할 수 있도록 도와줍니다.
이 경우 우리 클래스는 초 단위 시간, 가격 등 어떤 숫자 값을 나타낸다고 가정해 봅시다...
그리고 우리는 다음과 같은 연산이 가능하기를 원합니다:
mo1 + 20 20 + mo1
첫 번째(mo1 + 20)는 간단합니다. 우리가 클래스의 + 메서드를 직접 제어할 수 있기 때문입니다.
하지만 Integer 클래스는 어떨까요?
Integer의 + 메서드를 몽키 패치로 수정해서 구현할 수도 있지만, 그건 좋은 생각이 아닙니다 🙂
해결책은 무엇일까요?
바로 자신의 클래스에 coerce 메서드를 구현하는 것입니다.
Integer의 + 메서드는 연산 대상 객체가 이 메서드를 구현하고 있는지 확인하고, 구현되어 있다면 해당 메서드를 호출합니다.
이제 이 섹션 처음의 코드 예시를 기억하시나요?
다음 코드를 실행해 보면:
20 + mo1
mo1의 값이 10이므로 30이 출력되기를 기대합니다. 하지만 실제로 화면에 나타나는 것은:
MyObject can't be coerced into Fixnum
앞서 본 것과 같은 문제입니다!
coerce 메서드 내부에서 오류가 숨겨지고 있는 것입니다.
바로 이 부분입니다: mo.valuee = other
여기서도 오타가 있지만, 오류 메시지는 전혀 다른 내용을 알려줍니다!
MyObject can't be coerced into Fixnum
좋은 소식이 있습니다. 이 동작 역시 Ruby 2.5에서 변경되어, 숨겨진 오류가 하나 더 사라지게 됩니다. 따라서 더 이상 이런 문제로 고민할 필요가 없습니다.
마무리
이처럼 예외를 함부로 숨기면 안 되는 이유를 잘 보여주는 좋은 예시들이었습니다. 예외 처리에 대해 더 깊이 알고 싶다면 제 책 'Ruby Deep Dive'를 참고해 보세요.
이 글이 도움이 되었다면 주변 지인들과 공유해 주시면 감사하겠습니다. 더 많은 사람들이 유용한 정보를 얻을 수 있을 것입니다.
읽어주셔서 감사합니다!