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

루비 MRI 소스 코드 탐색: 내부 동작 원리 파헤치기

루비를 한동안 사용해 왔다면, 코드가 겉으로 보이는 것과 달리 내부에서 어떻게 동작하는지 궁금해진 순간이 있었을 것입니다.

루비의 내부 구조를 깊이 이해하는 가장 좋은 방법 중 하나는 실제 소스 코드를 직접 읽어보는 것입니다. C 언어를 잘 모르더라도 흥미로운 부분을 충분히 발견할 수 있습니다.

루비의 소스 코드는 GitHub에 공개된 공식 저장소에서 확인할 수 있습니다.

탐색 효율을 높이려면 클래스와 메서드 이름을 손쉽게 검색할 수 있는 Codequery 같은 도구를 활용하는 것이 좋습니다.

핵심 클래스 탐색하기

대부분의 탐색은 저장소의 루트 폴더에서 이루어집니다. 여기에는 Object 클래스가 담긴 object.c, Array 클래스가 담긴 array.c처럼 모든 핵심 클래스의 소스 코드가 들어 있습니다.

그럼 hash.c 파일을 살펴보겠습니다.

파일 맨 아래쪽 4468번째 줄로 스크롤하면 익숙한 이름들을 발견하게 됩니다.

먼저 이 코드부터 볼까요?

rb_cHash = rb_define_class("Hash", rb_cObject);

이 한 줄에서는 rb_define_class 함수를 통해 Hash 클래스가 정의됩니다. 두 번째 인자인 rb_cObject는 이 클래스의 상위 클래스(슈퍼클래스)를 의미합니다.

클래스 정의 과정이 어떻게 처리되는지 알고 싶다면 rb_define_class를 검색해 보면 됩니다.

루비 MRI 소스 코드 탐색: 내부 동작 원리 파헤치기

rb_define_class의 첫 번째 단계는 해당 클래스가 이미 정의되어 있는지 확인하는 것입니다.

if (rb_const_defined(rb_cObject, id)) {
  // ...
}

if 블록 안에서 루비는 몇 가지 유효성 검사를 수행합니다. 예를 들어 다루고 있는 대상이 실제로 이미 정의된 클래스인지 확인하는 식입니다.

반대로 아직 정의되지 않은 클래스라면 다음과 같이 새로 정의됩니다.

klass = rb_define_class_id(id, super);

st_add_direct(rb_class_tbl, id, klass);
rb_name_class(klass, id);
rb_const_set(rb_cObject, id, klass);
rb_class_inherited(super, klass);

return klass;

각 메서드의 정의를 일일이 읽어볼 수도 있지만, 사실 이름만 봐도 기능을 충분히 짐작할 수 있을 만큼 직관적입니다.

st_add_direct에서 'st'는 '심볼 테이블(symbol table)'을 뜻하며, 결국 하나의 해시 테이블입니다. rb_const_set 함수는 Object 클래스에 상수를 설정하는데, 덕분에 해당 클래스가 어디서든 사용 가능해집니다.

그리고 rb_class_inherited는 상위 클래스의 inherited 메서드를 호출합니다. 이 메서드에 대한 자세한 내용은 공식 문서에서 찾아볼 수 있습니다.

코드의 다음 섹션은 메서드 정의들로 구성되어 있습니다. MRI는 여기서 rb_define_method를 사용합니다.

예시를 보겠습니다:

rb_define_method(rb_cHash,"index", rb_hash_index, 1);
rb_define_method(rb_cHash,"size", rb_hash_size, 0);
rb_define_method(rb_cHash,"length", rb_hash_size, 0);
rb_define_method(rb_cHash,"empty?", rb_hash_empty_p, 0);

각 인자는 다음과 같은 의미를 갖습니다:

  • 첫 번째 인자: 메서드가 정의될 대상 클래스
  • 두 번째 인자: 루비에서 노출될 메서드 이름, 세 번째 인자: 실제 구현을 담당하는 C 함수
  • 마지막 인자: 해당 루비 메서드가 요구하는 인자의 개수 (음수일 경우 선택적 인자를 의미)

클래스 메서드(싱글턴 메서드)를 정의할 때는 rb_define_singleton_method 함수가 사용됩니다.

rb_define_singleton_method(rb_cHash, "[]", rb_hash_s_create, -1);

흥미롭게도 rb_define_singleton_method의 본문은 단 한 줄입니다.

rb_define_method(singleton_class_of(obj), name, func, argc);

객체의 싱글턴 클래스를 얻어와서 일반 메서드 정의 함수에 그대로 넘기는 방식입니다. 탐색을 계속하고 싶다면 object.c 파일이 좋은 다음 목표가 될 것입니다.

표준 라이브러리 탐색하기

자, 오늘은 C 코드는 여기까지!

이번엔 루비 코드를 읽어보는 건 어떨까요?

루비 표준 라이브러리는 루비로 작성되어 있으며, 저장소의 /lib 디렉터리에서 찾을 수 있습니다.

표준 라이브러리에는 OpenStruct, Base64 인코딩, Set 자료구조 등이 포함되어 있습니다.

Set은 배열과 비슷하지만 모든 요소가 유일해야 한다는 특별한 성질을 가집니다. 즉, 중복된 요소를 허용하지 않습니다.

그렇다면 어떻게 이런 동작이 가능할까요? 특별한 알고리즘이 숨어 있는 걸까요?

set.rb 파일을 열어보면 금방 답을 알 수 있습니다. Set은 내부적으로 Hash 객체를 기반으로 동작합니다.

# Adds the given object to the set and returns self.  Use +merge+ to
# add many elements at once.
def add(o)
 @hash[o] = true
 self
end

alias << add

즉, 요소를 추가할 때 해시의 키로 값을 등록하기만 하면 됩니다. 중복 여부를 일일이 검사할 필요가 없습니다. 같은 요소를 다시 추가하면 기존 값이 그저 덮어써질 뿐이니까요. 해시의 키는 고유하다는 성질을 그대로 활용한 영리한 설계입니다.

Rubinius 탐색하기

루비 소스 코드를 탐색하는 또 다른 방법은 Rubinius 같은 대체 구현체를 살펴보는 것입니다.

Rubinius의 코드 구조는 MRI와 다르게 조직되어 있기 때문에, 이 경우에는 GitHub의 '파일 찾기(find file)' 기능을 활용하는 것이 편리합니다.

루비 MRI 소스 코드 탐색: 내부 동작 원리 파헤치기

예를 들어 Enumerable에 대해 더 알아보고 싶다면 'enumerable'이라고 입력하기만 하면 관련된 모든 파일을 한눈에 확인할 수 있습니다.

마무리

지금까지 살펴본 것처럼, 큰 어려움 없이도 루비가 내부적으로 어떻게 동작하는지 배울 수 있습니다. 직접 소스 코드를 탐험해 보고, 발견한 흥미로운 사실들을 주변과 나눠보세요!

이 글이 도움이 되었다면 뉴스레터 구독도 잊지 마세요. 아래 양식에 이메일을 남겨주시면 무료 업데이트와 독점 콘텐츠를 받아보실 수 있습니다.