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

루비 젬(Gem)은 어떻게 작동할까? 내부 동작 원리 완벽 이해

루비에서 젬(gem)은 대부분의 경우 그냥 잘 작동합니다. 하지만 루비의 '마법'에는 큰 문제가 하나 있습니다. 바로 문제가 발생했을 때 그 원인을 찾기가 무척 어렵다는 점입니다.

젬 관련 문제를 자주 겪지는 않겠지만, 한번 문제가 생기면 구글 검색이 의외로 별 도움이 되지 않습니다. 에러 메시지가 너무 일반적이라 여러 가지 원인 중 무엇이든 가능하기 때문입니다. 젬이 루비와 실제로 어떻게 상호작용하는지 이해하지 못하면, 혼자서 이런 문제를 디버깅하기가 매우 힘듭니다.

젬은 마법처럼 보일 수 있지만, 조금만 파고들면 사실 아주 쉽게 이해할 수 있습니다.

gem install은 무엇을 하는 걸까?

루비 젬은 사실 약간의 부가 데이터와 함께 압축된 코드 덩어리에 불과합니다. gem unpack 명령으로 젬 안의 코드를 직접 확인해볼 수 있습니다:

~/Source/playground jweiss$ gem unpack resque_unit
Fetching: resque_unit-0.4.8.gem (100%)
Unpacked gem: '/Users/jweiss/Source/playground/resque_unit-0.4.8'
~/Source/playground jweiss$ cd resque_unit-0.4.8 
~/Source/playground/resque_unit-0.4.8 jweiss$ find .
.
./lib
./lib/resque_unit
./lib/resque_unit/assertions.rb
./lib/resque_unit/errors.rb
./lib/resque_unit/helpers.rb
./lib/resque_unit/plugin.rb
./lib/resque_unit/resque.rb
./lib/resque_unit/scheduler.rb
./lib/resque_unit/scheduler_assertions.rb
./lib/resque_unit.rb
./lib/resque_unit_scheduler.rb
./README.md
./test
./test/resque_test.rb
./test/resque_unit_scheduler_test.rb
./test/resque_unit_test.rb
./test/sample_jobs.rb
./test/test_helper.rb
~/Source/playground/resque_unit-0.4.8 jweiss$

gem install은 가장 단순하게 말하면 대략 이런 작업을 수행합니다. 젬을 내려받아 시스템의 특정 디렉터리에 파일들을 풀어넣는 것이죠. gem environment 명령을 실행하면(INSTALLATION DIRECTORY: 항목 확인) gem install이 젬을 설치하는 위치를 알 수 있습니다:

~ jweiss$ gem environment
RubyGems Environment:
  - RUBYGEMS VERSION: 2.2.2
  - RUBY VERSION: 2.1.2 (2014-05-08 patchlevel 95) [x86_64-darwin14.0]
  - INSTALLATION DIRECTORY: /usr/local/Cellar/ruby/2.1.2/lib/ruby/gems/2.1.0
  ...
~ jweiss$ ls /usr/local/Cellar/ruby/2.1.2/lib/ruby/gems/2.1.0
bin		    bundler		doc		    gems
build_info	cache		extensions	specifications

설치된 모든 젬 코드는 gems 디렉터리 아래에 저장됩니다.

이 경로는 시스템마다 다르고, 루비를 설치한 방식(rvm은 Homebrew와 다르고, rbenv와도 다르며 기타 등등)에 따라서도 달라집니다. 따라서 젬 코드가 어디에 있는지 알고 싶을 때는 gem environment가 유용하게 쓰입니다.

젬 코드는 어떻게 require될까?

여러분이 젬 안의 코드를 손쉽게 사용할 수 있도록, RubyGems는 루비의 require 메서드를 오버라이드합니다(core_ext/kernel_require.rb에서 처리). 주석이 꽤 명확합니다:

core_ext/kernel_require.rb
  ##
  # When RubyGems is required, Kernel#require is replaced with our own which
  # is capable of loading gems on demand.
  #
  # When you call <tt>require 'x'</tt>, this is what happens:
  # * If the file can be loaded from the existing Ruby loadpath, it
  #   is.
  # * Otherwise, installed gems are searched for a file that matches.
  #   If it's found in gem 'y', that gem is activated (added to the
  #   loadpath).
  #

예를 들어 active_support를 로드하고 싶다고 해봅시다. RubyGems는 먼저 루비의 기본 require 메서드로 로드를 시도하고, 그 결과 다음과 같은 에러가 발생합니다:

LoadError: cannot load such file -- active_support
	from (irb):17:in `require'
	from (irb):17
	from /usr/local/bin/irb:11:in `<main>'

RubyGems는 이 에러 메시지를 보고, 대신 젬 안에서 active_support.rb 파일을 찾아야 한다는 것을 파악합니다. 이를 위해 설치된 젬들의 메타데이터를 훑으며 해당 파일을 포함한 젬을 검색합니다:

irb(main):001:0> spec = Gem::Specification.find_by_path('active_support')
=> #<Gem::Specification:0x3fe366874324 activesupport-4.2.0.beta1>

그다음 젬을 활성화(activate)하는데, 이 과정에서 젬 안의 코드가 루비의 로드 경로(load path, 즉 require로 불러올 수 있는 디렉터리 목록)에 추가됩니다:

irb(main):002:0> $LOAD_PATH
=> ["/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby/2.1.0/x86_64-darwin14.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby/2.1.0/x86_64-darwin14.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/2.1.0/x86_64-darwin14.0"]
irb(main):003:0> spec.activate
=> true
irb(main):004:0> $LOAD_PATH
=> ["/usr/local/Cellar/ruby/2.1.2/lib/ruby/gems/2.1.0/gems/i18n-0.7.0.beta1/lib", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/gems/2.1.0/gems/thread_safe-0.3.4/lib", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/gems/2.1.0/gems/activesupport-4.2.0.beta1/lib", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby/2.1.0/x86_64-darwin14.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/site_ruby", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby/2.1.0/x86_64-darwin14.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/vendor_ruby", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/2.1.0", "/usr/local/Cellar/ruby/2.1.2/lib/ruby/2.1.0/x86_64-darwin14.0"]

이제 active_support가 로드 경로에 올라왔으므로, 젬 안의 파일들을 다른 루비 코드처럼 자유롭게 require할 수 있습니다. 심지어 RubyGems가 덮어쓰기 전의 원래 버전 require도 사용할 수 있습니다:

irb(main):005:0> gem_original_require 'active_support'
=> true

멋지죠?

조금의 지식이 큰 힘이 된다

RubyGems는 복잡해 보일 수 있습니다. 하지만 가장 기본적인 수준에서 보면, 그저 루비의 로드 경로를 대신 관리해줄 뿐입니다. 물론 전부 쉬운 것은 아닙니다. 이 글에서는 젬 간의 버전 충돌 관리, 젬 바이너리(rails, rake 같은 것들), C 확장 등 다른 많은 주제는 다루지 않았습니다.

하지만 이 정도 수준이라도 RubyGems를 이해하고 있으면 큰 도움이 됩니다. 조금씩 소스 코드를 읽고 irb에서 실험해보다 보면 젬의 소스까지 깊이 들어갈 수 있게 됩니다. 젬이 어디에 위치하는지 파악하면 RubyGems가 젬을 제대로 인식하는지 확인할 수 있고, 젬이 로드되는 방식을 알게 되면 겉보기에 이상해 보이는 로딩 문제도 직접 파고들 수 있습니다.

Rails와 Bundler가 젬을 어떻게 처리하는지 더 알고 싶다면, 'Rails는 젬을 어떻게 다룰까?' 글을 참고해 보세요.