본문 바로가기
Ruby on Ralis

Ruby의 구조: Ruby의 동작원리 (자구분석과 구문분석, 컴파일)

by sky1to 2024. 1. 10.

일본에 Rubyの仕組み(Ruby의 구조) 책을 읽고 Ruby가 어떻게 동작하는지에 대해 나 자신대로 간략하게 정리해 보았다.

 

Ruby라는 언어는 일본의 마츠모토 유키히로(matz)에 의해 만들어졌다. 때문에 일본에서는 아직 도 꽤 많은 스타트업과 회사들이 Ruby, Ruby On Rails를 사용하여 개발하는 곳이 많다. 정말 유명한 기업이라면 개발자들이 많이 사용하는 Github나 Shopify가 사용하고 있다. 일본 내에서도 유명한 스타트업 기업들이 Ruby On Rails를 사용하고 있다.

 

여담

일본의 유니콘 기업으로 올라선 SmartHR도 Ruby On Rails를 사용하고 있다. 국내에서는 당근마켓이 유명했지만 지금은 일부분만 사용하고 있는 것 같다

 

이런 Ruby라는 언어가 어떻게 만들어졌으며 우리가 Ruby코드를 작성했을 때 안에서는 어떤 동작이 이루어지고 있는지에 대해서 설명해보고자 한다. 

 

먼저 목차는 아래와 같습니다. 이 책 자체가 C언어에 대한 설명과 Ruby의 움직임에 대해 설명하고 있기에 이번 글에서는 1 ~ 2장까지의 내용을 정리해보고자합니다.

* 최대한 간략하게 설명하였으니 예제 코드같은것은 많이 생략되어 있습니다. 기회가 된다면 부분부분도 정리해 나가고 싶네요

 

1장: 자구분석과구문분석

2장: 컴파일

3장 Ruby는 어떤식으로 코드를 실행하는가

4장: 제어구조와 메소드 디스패치

5장: 오브젝트와 클래스

6장: 메소드검색과 정수 검색

7장: 해쉬 테이블: Ruby내부에서 움직이는 사람

8장: Lisp에서 차용한 아이디어

9장: 메타 프로그래밍

10장: JRuby: JVM위에 Ruby

11장: Rubinius:Ruby에 실행되어진 Ruby

12장: MRI ・ JRuby・Rubinius에 의한 GC

부록: 다른 Ruby가상 머신

 

본문에 앞서 한가지 질문!

프로그램을 실행했을 때 Ruby는 몇번 코드를 읽고 변환하고 있을까요???

이 질문에 답은 아래에 작성해놓겠습니다!

 

Ruby코드가 실행되는 흐름

먼저Ruby 코드가 실행됬을 때 전체적인 흐름을 확인해 보겠습니다.

자구분석이나 구문분석은 아래에서 설명할 예정이니 이런게 있다하고 넘어가시면 될 것 같습니다.   

책의 내용 번역

자구분석(字句分析)

먼저 별로 들어 본적없는 자구분석부터 확인해보도록 하겠습니다.

네이버에 검색해보니 국어사전에 등록되어있네요.

프로그램을 표현하는 문자의 열을, 프로그램을 표현하는 의미 최소 단위인 자구의 열로 변환하는 일.

Ruby는 코드를 맨 처음에 최소단위의 자구로 변경합니다

 

예시를 한번 봐볼까요. ruby를 한번 사용해본적이 있으신분에게는 아주 간단한 코드일 겁니다.

 

# 0부터 10까지 출력하는 반복문
10.times do |n|
  puts n
end

 

1 0 , t i m e s   d o   | n |  

 

위에 코드에 대해서 Ruby의 C코드에서는 문자 하나하나 읽어들여 그 내용에 대해서 처리하는 반복문이 포함되어 있습니다.

이걸 하나하나 읽어들여 확인한다. 10에 대한 확인이 끝나고 <.>를 만났을 때 반복문 처리는 10을 토큰으로 변경한다.

 

 

최종적으로는 아래와 같이 모든 글자가 토큰으로 변경된다.

 

실제로 코드로 확인해보면 아래와 같다

require 'ripper'
require 'pp'

code = <<STR
10.times do |n|
  puts n
end
STR
puts code

pp Ripper.lex(code)

 

 

 

=== === ===

자구분석에서 정리는 <코드 한글자 한글자를 분석해서 하나의 토큰으로 변경한다>를 알아가시면 될것 같습니다.

=== === ===

구문분석(構文分析)

여기서는 갑자기 난이도가 올라간다. 더 자세하게 정리하고 싶었지만 이 장에서는 간단하게 정리해 본다.

LALR서버를 사용하여 토큰의 입력 스트림을 추상구문나무(AST)라고 불리는 데이터 구조로 변경한다.

 

Ruby도 많은 프로그래밍 언어와 마찬가지로 parser generator를 이용한다. Ruby가 토큰열을 처리하기 위해 사용하는 서버는 Parser Generator에 의해 생성된다. Ruby는 Parser Generator로 Bison이라는 Yacc(이게 잘 알려저있다고 한다)에 새로운 버젼을 이용하고 있다.

 

Ruby는 Ruby자체를 빌드 하는 과정에서 Bison을 사용해 문법원칙규칙에서 파서 코드를 상성하고, 프로그램이 실행될 때 생선된 파서코드는 자구분석기에 의해 반환된 토큰을 분석한다.

그림으로 설명하면 아래와 같다.

 

C언어로 작성되어 있는 문법규칙(parse.y)

 

LALR(Look-Ahead, Left to right, Rightmost derivation) 알고리즘

위에서 LALR알고리즘이 나왔는데 다른 글들에 잘나와 있기 때문에 이 글에서의 설명은 생략한다. 구문분석기가 이 알고리즘을 사용하여 토큰을 분석하고 처리하구나 라는 점만 알아두면 좋을 것 같다.

이 알고리즘을 사용해 토큰 스트림을 추상구문나무(AST)라고 불리는 데이터 구조에 변환한다

추상구문나무(AST)는 아래와 같은 형식을 띈다

추상구문나무(AST)

 

추상구문나무(AST)란?

AST는 『Abstract Syntax Tree』 의 약어로서 추상구문나무라고 불리는 데이터 구조이다

RUby는 Ruby코드를 바이트코드에 변경하는 과정에 구문분석을 하고 AST에 변환하여 Ruby코드를 실행하고 있다

이 나무를 왜 만드는지 궁금하여 찾아봤는데 자세히 나온 글이 없어 나중에 다시 한번 찾아보도록 하겠습니다.

 

=== === ===

구문 분석에서의 정리는 <자구분석에서의 토큰열을 LALR서버를 사용하여 추상구문나무(AST)라고 불리는 데이터 구조로 변경한다>입니다

=== === ===

컴파일

Ruby는 잘알려진 동적 스크립트 언어이다. 하지만 실제로 Ruby는 컴파일러를 사용하고 있다. C, Java와 같은 프로그래밍언어다.

C와 Java와 다른점이라면 Ruby의 컴파일러의 실행은 뒤에서 움직이고 있는 것이다. 사용자는 이 컴파일러에 대해서 신경쓸 필요가 없다.

 

C는 바로 기계어로 변경하고, Java의 경우 JVM을 사용하고 있음으로 JVM이 이해 할 수 있는 코드로 변경한다. Ruby도 Ruby가상 머신이 이해할 수 있는 다른 언어로 컴파일한다. 그러나 위에 작성한 것과 같이 Ruby의 경우 사용자는 이 컴파일러에 대해서 전혀 신경쓸 필요가 없다라는 점이 차이점이다.

1.8 vs 1.9를 비교

Ruby 1.8에서는 컴파일러가 없었다. AST노드에서 바로 C와 기계어로 변경되었지만 1.9부터 YARV를 도입하였다. 도입하게 된 이유는 

훨씬 더 빠른 속도를 자랑하기 떄문이다.

 

약 1000번 정도 되는 적은 수의 반복문의 경우 YARV로 변경하지 않아도 되기 때문에 Ruby 1.8이 조금 빠른 경향이 있다. 

하지만 천만, 억이 되는 레코드의 경우 확연한 차이를 보인다. 자료가 많이 없어서 아래와 같은 차트를 첨부한다. 보면 두번째 바가 1.8, 3번째 바가 YARV인데 속도의 차이를 보더라도 약 5배정도의 차이를 보이는 것을 알 수 있다. 책에서는 약 4.25배정도 빠르다고 나오고 있다.

 

https://programmingzen.com/ruby-implementations-shootout-ruby-vs-yarv-vs-jruby-vs-gardens-point-ruby-net-vs-rubinius-vs-cardinal/

 

 

 

 YARV(Yet Another Ruby Virtual Machine)

위키백과에 따르면

> Ruby 프로그래밍 언어용으로 개발한 바이트코드 인터프리터입니다. 프로젝트의 목표는 Ruby 프로그램의 실행 시간을 크게 줄이는 것이었습니다

 

책에서 나온 자료를 추가하면 YARV는 Java에 JVM과 같은 아이디어 이다. AST를 YARV명령예시를 변환하여 컴파일 한다.

따라서 AST를 변환하는 바이트코드 인터프리터라고 보면 될 것 같다.

 

좀 어려운 말로 정리하면 구문분석에서 만든 추상구문나무를 바이트코드 명령예시를 생성합니다. Ruby코드는 Ruby언어에서 YARV가상 머신용 언어로 변경하고 프로그램중에 모든 범위나 섹션을 다른 명령예시 YARV명령예시의 세트에 컴파일한다입니다.

(모든 블록, 메소드, 람다식, 또는 프로그램중에 다른 범위는 대응하는 바이트 코드 명령예시에 세트를 갖고 있다. 지금은 굳이 몰라도 괜찮다)

 

YARV에 대해 더 자세한 YARV가 어떻게 동직하는지에 대한 자세한 내용도 있지만 이글에서는 생략하도록 하겠다.

 

=== === ===

결론적으로 컴파일에 대한 정리는 <AST를 YARV명령예시로 변환하여 컴파일 한다>이다.

=== === ===

 

 

먼가 이것저것 복잡한 내용을 설명하고, 많은 부분을 생략하였지만 이 글의 결론은 아래의 사진과 같다.

Ruby를 실행할 때는 내부에서 아래와 같은 플로우로 하나하나 분석하여 실행되고 있다.

책의 내용 번역

 

 

이책을 아직 반정도 밖에 못읽었지만 Ruby라는 언어가 내부에서 어떻게 동작하고 있고, 조금이라도 속도를 올리기 위해서는 어떻게 코드를 짜야하는지 조금 도움이 되는 것 같다. 지금까지 웹개발을 하면서 이런 책들을 많이 접하지 못하였는데, 이번에 이 책을 읽으면서 언어의 내부에 대해서 아는 것, 많은 천재들이 만들어논 내부에 대해서 공부하는 것은 많은 공부가 필요하고 다시한번 대단함을 느끼게 되었다.

 

이 글에서는 정말 간단하게 Ruby가 내부에서 동작하는 원리에대해서 정말 정말 정말 일부분만 작성하였지만 혹시 관심이 있으신분은 한 번 찾아봐서 읽으면 더 언어에 대해 깊게 알수 있지 않을 까 싶다.

 

정리하다 보니 아직 이해하지 못한 내용이 많아 시간이 오래 걸리기도 했고, 머리가 아파오기 시작하여 많이 정리를 못하였는데 시간이 조금씩 생길 때 마다 조금씩 정리해나가고 싶다.

 

긴 글 읽어주셔서 감사합니다! 틀린 내용에 대한 코멘트는 언제나 환영입니다!

 

> 프로그램을 실행했을 때 Ruby는 몇번 코드를 읽고 변환하고 있을까요???

가장 처음에 질문에 대한 답은 <3번>입니다!