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

코드 읽기 모험: Awesome Print 젬의 내부 동작 원리 파헤치기

awesome_printirbpry에서 출력 결과를 훨씬 읽기 쉽게 포맷해 주는 유용한 Ruby 젬입니다.

예를 들어…

다음은 awesome_print로 해시(Hash)를 화면에 표시했을 때의 모습입니다:

코드 읽기 모험: Awesome Print 젬의 내부 동작 원리 파헤치기

그런데 이건 대체 어떻게 작동하는 걸까요?

"진실은 오직 한 곳에서만 찾을 수 있다. 바로 코드 안에서다." ― 로버트 C. 마틴(Robert C. Martin)

그럼 소스 코드를 직접 들여다보며 그 비밀을 밝혀봅시다!

멋진 출력의 시작

저는 코드 리딩 세션을 시작할 때 프로젝트 구조(파일과 폴더)를 빠르게 훑어본 뒤, 이후 탐색 방향을 잡아줄 질문 하나를 던지는 것부터 시작합니다.

그래서 제가 처음 던진 질문은 이것입니다:

awesome_print는 어떻게 pry의 출력을 바꿔낼 수 있을까?

이제 탐정 모자를 쓰고 가설을 세워봅니다:

"이 젬은 아마 $stdout을 교체해서 pry의 출력을 가로채고, 그걸 예쁘게 다듬는 게 아닐까?"

하지만 이런 방식이라면 Ruby 표준 라이브러리의 pretty_print처럼 출력을 자유롭게 커스터마이징하기 어렵습니다.

게다가 출력을 파싱하거나 심지어 eval로 실행해야 할 수도 있는데, 이건 별로 좋은 생각이 아니죠!

다음 단계:

이 젬이 어떻게 로드되는지 살펴봤습니다. 이렇게 하면 코드로 들어가는 출발점을 찾을 수 있습니다.

Awesome Print 불러오기

pry에서 awesome_print를 사용하려면 다음과 같이 입력합니다:

require 'awesome_print'
AwesomePrint.pry!

이제 pry!가 어디서 정의되어 있는지 찾아야 합니다.

저는 Atom 에디터의 "디렉터리 내 검색" 기능을 사용해 찾았습니다.

찾은 코드는 다음과 같습니다:

def pry!
  Pry.print = proc { |output, value| output.puts value.ai } if defined?(Pry)
end

Pry는 print 값만 설정하면 자신의 출력 방식을 변경할 수 있도록 허용하는 것 같습니다.

이것으로 "어디서부터 시작해야 할까?"라는 질문에 대한 답을 얻었습니다 🙂

이 "proc"이 무엇인지 더 알고 싶으신가요? 제 책 『Ruby Deep Dive』의 샘플 챕터인 "블록, 프록 & 람다 완벽 가이드"를 읽어보세요.

이 proc은 두 개의 인자를 받습니다:

  • output (출력 스트림)
  • value (출력할 값)

그리고 이 객체들에 대해 두 개의 메서드를 호출합니다.

다음 질문:

ai 메서드는 대체 무엇일까요?

aiKernel 모듈에 정의된 메서드입니다:

def ai(options = {})
  ap      = AwesomePrint::Inspector.new(options)
  awesome = ap.awesome(self)

  if options[:html]
    awesome = "</pre>#{awesome}</pre>"
    awesome = awesome.html_safe if defined? ActiveSupport
  end

  awesome
end

alias :awesome_inspect :ai

Ruby의 모든 객체는 기본적으로 Kernel을 포함하기 때문에, 어떤 객체든 이 ai 메서드를 호출할 수 있습니다.

이제 조금 더 깊이 파고들어 Inspector 클래스가 어떻게 작동하는지 살펴보겠습니다.

Inspector 클래스

inspector.rb 파일을 열자마자 initialize 메서드 안에서 거대한 옵션 해시를 발견할 수 있습니다.

일부분만 보면 다음과 같습니다:

@options = {
  indent:        4,      # 들여쓰기에 사용할 공백 개수.
  index:         true,   # 배열 인덱스 표시 여부.
  html:          false,  # HTML 대신 ANSI 색상 코드 사용.
  multiline:     true,   # 여러 줄로 표시.
  # ...
}

그다음에는 이런 코드가 등장합니다:

@formatter  = AwesomePrint::Formatter.new(self)
@indentator = AwesomePrint::Indentator.new(@options[:indent].abs)

Thread.current[AP] ||= []

포매팅 자체와 코드 들여쓰기를 담당하는 두 객체를 설정하는 부분입니다.

그런데 이 Thread.current는 뭘까요?

Thread.current는 현재 실행 중인 스레드에 접근할 수 있게 해줍니다. 애플리케이션에서 스레드를 명시적으로 사용하지 않더라도 항상 "메인(main)" 스레드 하나는 존재합니다.

AP 상수는 inspector.rb 파일 맨 위에 정의된 단순한 상수입니다:

AP = :__awesome_print__

그래서 여기서 무슨 일이 일어나고 있는 걸까요?

Awesome print는 Thread.current__awesome_print__ 키를 활용해 현재 스레드에서만 접근 가능한 데이터를 저장합니다.

이는 멀티스레딩 환경에서 발생할 수 있는 문제를 피하기 위한 장치입니다.

멋진 포매팅의 핵심

이제 실제 출력 포매팅이 일어나는 AwesomePrint::Formatter 클래스 내부를 살펴보겠습니다.

작동 방식은 이렇습니다. inspector(ai 메서드가 생성한 객체)가 format 메서드를 호출합니다.

def unnested(object)
  @formatter.format(object, printable(object))
end

그러면 Formatter 클래스의 format 메서드가 해당 객체 타입에 맞는 최선의 처리 방법을 찾아, 약간의 메타프로그래밍 기법으로 다른 메서드를 호출합니다.

해당 메서드는 다음과 같습니다:

def format(object, type = nil)
  core_class = cast(object, type)

  awesome = if core_class != :self
    send(:"awesome_#{core_class}", object) # 핵심 포매터.
  else
    awesome_self(object, type) # 해당하지 않으면 object.inspect로 폴백.
  end

  awesome
end

Formatter 클래스를 제대로 이해하려면 cast 메서드도 함께 봐야 합니다:

def cast(object, type)
  CORE.grep(type)[0] || :self
end

CORE 상수는 Ruby의 핵심 클래스들을 나타내는 심볼 배열이며, :self는 "못 찾음"을 의미하는 심볼입니다(이 예제에서만 그런 의미이지, Ruby 전반에서 그런 건 아닙니다).

CORE = [:array, :bigdecimal, :class, :dir, :file, :hash, :method, :rational, :set, :struct, :unboundmethod]

정리하면 이런 흐름입니다:

포매팅 대상 객체가 "핵심 클래스" 목록에 있다면 특화된 포맷이 적용됩니다.

그렇지 않다면 일반적인 포맷이 적용됩니다.

특화 포매터들은 lib/awesome_print/formatters/ 디렉터리에 정의되어 있으며, Array, Hash, Class 등이 포함됩니다.

예를 들어, 클래스를 위한 포매터 메서드는 다음과 같습니다:

def format
  superclass = klass.superclass
  if superclass
    colorize("#{klass.inspect} < #{superclass}", :class)
  else
    colorize(klass.inspect, :class)
  end
end

원한다면 직접 나만의 포매터를 작성할 수도 있습니다.

마무리

이번 글에서는 배열이나 해시 같은 객체를 보기 좋게 출력해 주는 Awesome Print 젬에 대해 알아봤습니다.

즐겁게 읽으셨고 새로운 것을 배워가셨기를 바랍니다!

더 많은 사람들이 배울 수 있도록 지금 바로 좋아하는 SNS에 이 글을 공유해 주세요 🙂