Ruby 해부하기 ♦️ (2/3): 어디에나 있는 객체들

Demystifying Ruby ♦️ (2/3): Objects, Objects everywhere -

3줄 요약

  • Ruby는 숫자, 문자열, 클래스 등 모든 것이 객체로 취급되며, 각 객체는 고유 ID와 상태/행동을 가집니다.
  • 메서드 호출 시 싱글톤 클래스, 클래스, 모듈, 상속 체인, Kernel, Object, BasicObject 순으로 탐색합니다.
  • Ruby는 오픈 클래스와 메타프로그래밍을 통해 동적으로 코드를 변경하는 강력한 유연성을 제공하지만, 신중한 사용이 요구됩니다.

Ruby 언어의 근본적인 특징 중 하나는 **모든 것이 객체**라는 점입니다. 이러한 객체 지향적 특성은 개발자가 단순한 함수나 절차보다는 객체와 그 상호작용의 관점에서 사고하도록 이끌어 줍니다. 본문은 Ruby의 객체 모델을 심층적으로 탐구하며, 객체의 기본 성질, 변수와 메서드의 동작 방식, 그리고 Ruby의 강력한 메타프로그래밍 기능까지 상세히 설명합니다. 이 글을 통해 Ruby 객체가 어떻게 구성되고 상호작용하는지, 그리고 이러한 특성이 어떻게 유연성과 동적 특성을 부여하는지 이해할 수 있습니다.

먼저, Ruby에서는 정수(1), 문자열("Hello"), 심지어 Object 클래스 자체까지 모두 객체입니다. Object 클래스는 BasicObject를 상속받으며, BasicObject는 어떤 것도 상속받지 않는 최상위 객체입니다. 모든 Ruby 객체는 고유한 식별자인 객체 ID를 가집니다. 이 ID는 객체의 메모리 주소를 나타내며, equal?과 같은 메서드는 이 객체 ID를 비교하여 동일 객체인지 판단합니다. 일부 기본적인 객체(nil, true, false, Class 등)는 Ruby VM 내부에 하드코딩된 ID를 가지지만, 대부분의 객체는 동적으로 생성될 때 고유한 ID를 할당받습니다.

객체의 상태는 인스턴스 변수(@ 접두사)와 클래스 변수(@@ 접두사)에 저장됩니다. 인스턴스 변수는 각 객체 인스턴스마다 독립적으로 값을 가지며, 내부적으로 해시와 유사한 구조에 저장됩니다. 클래스 변수는 해당 클래스의 모든 인스턴스가 공유하는 값입니다. Ruby 객체는 instance_variable_get, instance_variable_set과 같은 저수준 메서드를 통해 내부 변수에 접근할 수 있습니다. 객체는 메모리에 할당될 때 클래스에 대한 포인터, 인스턴스 변수 저장 구조, 그리고 메타데이터를 포함하는 연속적인 메모리 블록을 가집니다.

메서드를 호출할 때 Ruby는 메서드 탐색 체인을 따릅니다. 이 체인은 다음 순서로 진행됩니다: 첫째, 해당 객체에 직접 정의된 싱글톤 클래스(Eigenclass)의 메서드를 확인합니다. 둘째, 객체의 클래스에 정의된 메서드를 찾습니다. 셋째, 포함된 모듈include된 역순으로 확인합니다. 넷째, 클래스의 상속 체인을 따라 슈퍼클래스들을 탐색합니다. 다섯째, 모든 클래스가 포함하는 Kernel 모듈을 확인합니다. 여섯째, Object 클래스를 확인합니다. 마지막으로, 최상위 클래스인 BasicObject를 확인합니다. 이 모든 단계에서도 메서드를 찾지 못하면 NoMethodError가 발생합니다.

Ruby의 또 다른 강력한 특징은 오픈 클래스메타프로그래밍입니다. Ruby에서는 심지어 내장 클래스까지 포함하여 어떤 클래스든 다시 열어서 새로운 메서드를 추가하거나 기존 메서드를 수정할 수 있습니다. 이는 ActiveSupport와 같이 핵심 Ruby 클래스를 확장하여 유틸리티 메서드를 추가하는 라이브러리에서 활발하게 사용됩니다. 메타프로그래밍은 코드가 코드를 동적으로 작성하거나 수정하는 기법입니다. Ruby는 define_method와 같은 도구를 통해 메서드를 동적으로 정의할 수 있습니다. attr_accessor가 내부적으로 이러한 메타프로그래밍 기능을 활용하여 접근자 메서드를 생성하는 것이 대표적인 예입니다. 또한, 존재하지 않는 메서드 호출을 가로채서 처리할 수 있는 method_missing과 같은 메커니즘도 제공됩니다. 이는 매우 유연하지만, 코드 가독성과 유지보수성을 해칠 수 있으므로 신중하게 사용해야 합니다.

결론적으로, Ruby의 '모든 것이 객체'라는 철학은 언어의 구조와 동작 방식에 깊은 영향을 미칩니다. 고유한 객체 ID, 유연한 변수 접근 방식, 그리고 체계적인 메서드 탐색 체인은 Ruby 객체 모델의 핵심을 이룹니다. 나아가 오픈 클래스와 메타프로그래밍 기능은 Ruby에게 탁월한 동적 유연성과 표현력을 부여합니다. 이를 통해 개발자는 매우 간결하고 강력한 코드를 작성할 수 있습니다. 그러나 이러한 강력한 기능은 동시에 예측하기 어려운 동작이나 디버깅의 어려움을 초래할 수 있으므로, '큰 힘에는 큰 책임이 따른다'는 말처럼 신중하고 책임감 있는 사용이 중요합니다. Ruby의 객체와 메타프로그래밍을 제대로 이해하고 활용하는 것은 Ruby 전문가로 나아가는 데 필수적인 과정입니다.