초기에는 Die
클래스 예시와 같이 단순한 클래스의 경우, 특정 속성(예: value
)에 <=>
연산을 위임하는 방식으로 쉽게 구현할 수 있습니다. 그러나 여러 속성을 기반으로 복합적인 비교를 수행해야 할 때는 Array#<=>
를 사용하는 방법이 흔히 제안됩니다. 예를 들어, [self.value, self.faces] <=> [other.value, other.faces]
와 같이 배열을 구성하여 비교하는 방식은 코드를 간결하게 만들지만, 몇 가지 중요한 단점을 내포하고 있습니다.
첫째, Array#<=>
는 배열 내의 모든 값을 즉시 계산(eagerly computed)합니다. 이는 Die
클래스의 color_priority
와 같이 계산 비용이 드는 속성의 경우, 앞선 비교에서 이미 결과가 결정되었음에도 불구하고 불필요한 계산이 수행될 수 있음을 의미합니다. 둘째, 이 방식은 비교가 발생할 때마다 두 개의 배열을 새로 할당합니다. 이는 Bundler와 같은 애플리케이션에서 bundle update
명령 실행 시 전체 메모리 할당의 60%를 차지할 정도로 심각한 성능 병목 현상을 초래할 수 있습니다. 실제로 Bundler는 이 문제 해결을 위해 배열 할당을 제거하는 방향으로 <=>
연산자 구현을 변경하였으나, 이로 인해 코드의 간결성이 저해되는 부작용이 있었습니다.
이러한 문제에 대한 더 나은 해결책으로 Numeric#nonzero?
메서드를 활용한 ‘관용적인 spaceship’ 구현 방식이 제시됩니다. Numeric#nonzero?
는 <=>
연산자의 반환 값(정수)을 처리하기 위해 특별히 구현된 메서드입니다. 이 메서드는 0이 아닌 경우 자기 자신(self)을 반환하고, 0인 경우 nil
을 반환하는 독특한 특성을 가집니다. 이 특성 덕분에 (version <=> other.version).nonzero? || priority <=> other.priority
와 같은 형태로 체인 비교를 구성할 수 있습니다. 기존의 ||
(OR) 연산자를 직접 사용한 방식(version <=> other.version || priority <=> other.priority
)은 Ruby에서 0이 참(truthy) 값으로 간주되어 버전이 같을 때(0
반환) 뒤의 priority
비교가 수행되지 않는 버그가 발생했습니다. 그러나 nonzero?
는 0을 nil
(falsy)로 변환하여 이 문제를 해결하고, 올바른 단락 평가(short-circuiting)를 가능하게 합니다.
결과적으로 nonzero?
를 활용한 방식은 불필요한 배열 할당을 제거하여 성능을 향상시키고, 코드를 매우 간결하게 유지하며, 심지어 복잡한 비교 로직도 지연 평가를 통해 효율적으로 처리할 수 있게 합니다. 여러 필드를 순차적으로 비교해야 하는 ClassWithManyFields
와 같은 복잡한 시나리오에서도 이 방식은 코드의 가독성과 효율성을 크게 높여줍니다.