옛글/Ruby on the Rails

Rails autoloader :zeitwerk 방식 *업데이트 중

ShakeJ 2019. 5. 27. 10:31

지난번 Rails 5.2에서 6.0으로 업그레이드를 진행하면서, 기존에 사용했던 방식에 에러가 발생!

[Controller]

A

B
C

 

C는 B를 상속, B는 A를 상속하는 상황에서, B에 before_action 내에서 A의 함수를 호출 시, 5.2에서는 문제 없이 진행되나, 

6.0에서는 에러를 발생(A의 함수를 가지고 오지 못하는 케이스)가 있었음. 

결국 C의 입장에서 before_action을 실행했을 때 A의 함수를 가지고 오지 못하는 문제점을 살펴보다, 

6.0부터는 autoloader가 default로 true이고, 방식이 :classic이 아닌, :zeitwerk을 사용하는 부분에서 해당 에러가 발생하는 것을 확인함. 

(application.rb에 config.autoloader = :classic 방식을 사용하겠다고 명시하면 에러는 발생하지 않지만, 과연 autoloader와 zeitwerk방식이 무엇인지 확인해보는 걸로)

 

1. autoloader는 무엇인가? 무슨 이점이 있는가?

 

https://guides.rubyonrails.org/autoloading_and_reloading_constants.html

 

Autoloading and Reloading Constants — Ruby on Rails Guides

Ruby on Rails allows applications to be written as if their code was preloaded. In a normal Ruby program classes need to load their dependencies: require 'application_controller' require 'post' class PostsController < ApplicationController def index @posts

guides.rubyonrails.org

Rails는 코드를 preload하고 사용할 수 있음.

일반적으로 ruby 프로그램은 아래와 같이 require로 명시를 해준 뒤 사용을 함.

 

require 'application_controller'
require 'post'
 
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Rails의 Rubyist들은, 좀 더 빠르게 이 방식을 바꾸고자 했음. 만약 class가 이름으로 정의되어 있다면, 자동으로 해당 클래스를 import하면 되지 않을까? 자동으로 의존성을 찾기 위해 미리 파일을 스캔할 수도 있지만, 이 방식을 에러가 나기 쉬울 것이다.

 

나아가 Kernel#require는 파일을 한번만 읽어오지만, 읽어온뒤 파일 변경 시 변경사항을 반영할 수 있다면, 서버를 재시작하지 않고 개발을 좀 더 편리하게 할 수 있을 것이다. 그래서 Rails는, 따로 require을 하지 않고 사용이 가능하다.

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end
class Project < ActiveRecord::Base
end


Project = Class.new(ActiveRecord::Base)


Project.name # => "Project"

우리가 자주 사용하는 class A < B 를 사용 시 A = Class.new(B)와 같다고 보면 되며, .name의 경우 side effect로 저장되어 A로 나오게 됨.

 

우리가 자주 세팅하는 eagar_loading (미리 모두 읽어오는 방식)의 경우 개발환경처럼 파일들이 '자주' 변하지 않기 때문에, 일반적으로 eagar_loading = true로 사용하며, 자동으로 읽어오더라도

 

class BeachHouse < House
end

와 같은 코드 실행 시 미리 가져온 클래스들 중 House가 발견되지 않았다면, Rails는 자동으로 로딩함. 

****** 자동 로딩 알고리즘 ****** 

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

Ruby는 class나 moudle 키워드 뒤에 오는 상수를 탐색함.

클래스나 모듈이 사용된 부분에서 처음 생성한 것인지, 혹은 이전에 생성한 것인지 확인하기 위해서임. 

 

해당 시점에 정의되어 있지 않다면, 자동 로딩을 하지 않음. (당연한 이야기처럼 들리지만)

PostsController내의 메소드 실행 시 PostsController가 정의되어 있지 않았다면, 자동로딩이 아니라 새로운 컨트롤러를 생성함. 

 

반대로 뒤에 붙는 ApplicationController가 지금까지 생성되지 않았다면, Rails는 자동 로딩이 실행 됨 (상수이므로)

가장 처음 app/assets/application_controller.rb를 탐색하고, 다음 경로로 app/controllers/application_controller.rb를 탐색함. 

module Admin
  class BaseController < ApplicationController
    @@all_roles = Role.all
  end
end

Role을 로딩하려는 순간, 이루어지는 일은, 

Role을 자동로딩하려고 부모의 네임스페이스나 자기자신에게서 Role이 정의되어 있는지를 체크함. 
1. Admin::BaseController::Role을 찾고, 
2. Admin::Role을 찾고, 
3. Role을 찾게 됨

 

실패할 경우, autoload_paths에 경로의 폴더내에 해당 파일들을 찾게 됨 

이후 더 자세한 내용은 문서참조.

 

Autoloader라는 것은, 기존 ruby에서는 require로 명시된 클래스를 가져오지만, ruby on rails는 이를 조금 더 편리하게 사용하기 위해, 

require로 명시를 하지 않고, 클래스나 모듈 등 상수가 정의될 때, 자동으로 우선순위에 따라 이름 등의 규칙을 통해 자동으로 가져오는 방식을 의미함. 

 

2. zeitwerk방식과 classic 방식의 차이는?

 

현재 Ruby on rails 6 Guide는 나오지 않은 상태라, 따로 정보를 찾아야 하는 상황.

 

5월 16일 Rails Core team member Xavier Noria presents Zeitwerk 이벤트가 있었음. 
https://www.meetup.com/ko-KR/Barcelona-on-Rails/events/nrfkgqyzhbvb/

 

Rails Core team member Xavier Noria presents Zeitwerk

2019년 5월 16일 (목) 오후 7:00: Our local Rails Core team member Xavier Noria (@fxn) wrote the new gem Zeitwerk to replace the autoloading mechanisms used in Rails. It will be shipped as part of Rails 6.Bef

www.meetup.com

내용은, Rails 6에서 변경되는 autoloading mechanism에 사용되는 Zeitwerk 방식에 대한 내용.

찾았다!

https://www.youtube.com/watch?v=YSc_GNQP6ts

https://speakerdeck.com/fxn/zeitwerk-a-new-code-loader?slide=3

 

Zeitwerk: A new code loader

These are the slides of my talk at RubyKaigi 2019, where I present Zeitwerk, a new code loader for Ruby projects.

speakerdeck.com

Zeitwerk는 autoloading, eager loading, reloading을 하며, 의존성을 가지지 않고 어떤 ruby project에도 사용 할수 있다더라. 

파일의 이름을 상수의 Path로 사용하며 (기존 Rails가 사용하던 방식과 동일하게 User 모델일 경우 user.rb로, hotel/pricing.rb인 경우 Hotel::Pricing 사용하는 것처럼)

(Coreteam 멤버인 Noria 씨가 기존 Rails의 자동로딩을 개선시키면서 다른 루비프로젝트도 사용할 수 있게 Zeitwerk로 나온듯)

 

호출될 때 hash table에서 찾아서 맵핑을 하게 되는데, 
첫번째 클래스는, (relative constants)는 호출될 때 먼저 Hotel을 호출하고, 그 다음 Pricing module을 호출한다. 
두번째는 모듈을 호출할 때 특정 모듈 안에 포함된다고 호출한다. 
...[업데이트 중]



기존 Rails의 classic 방식의 제한사항 * 기존 방식의 제한사항에 대한 이해가 쉬운 예제나 이슈 찾아보기
* The nesting is unknown

* relative/qualified is unknown

* const_missing is the last step 

 

* Non thread-safe 
(기존 방식의 non thread-safe한 예시 : https://stackoverflow.com/questions/38587612/example-of-something-that-is-not-thread-safe-in-rails)

In order for reloading to be thread-safe, you need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible RW lock. When a request comes in, the framework acquires the lock for reading at the beginning, and the code in the framework that calls loader.reload needs to acquire the lock for writing.

On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.


발견된 문제 사항

 

bin/rails zeitwerk:check 을 통해 현재 zeitwerk방식에 맞지 않는 케이스를 찾아준다. 

* knock gem은 아직 지원을 하지 않는다 :( 

zeitwerk방식을 사용 시, 

UserTokenController <- Knock::AuthTokenController 

  def auth_params

  end 

end 

 

인 경우 당연스럽게 UserTokenController의 auth_params 메서드가 오버라이드 되어 실행되어야 하는데, AuthTokenController의 auth_params 메서드가 불린다. (autoload의 순서를 찍어보니, UserTokenController를 먼저로드하고, 이후에 authTokenController가 로드된다 / load의 순서가 바뀌더라도 override가 안된다는 건 기본 문법이 파괴되는 부분인데 뭔가 이상)

 

대부분의 gem들이 현재 zeitwerk방식을 지원하지 않고 있고, 만약 zeitwerk를 호환한다고 하면 레일즈 버전 호환성 문제도 생길텐데... 내가 뭘 잘못 이해하고 있나싶어, 관련 내용을 zeitwerks를 개발한 Noria에게 메일을 보내보니, 오히려 문제로 볼만한 좋은 description이란 내용의 메일을 받아 rails github에 bug report로 올려놓은 상황. Gem들을 미리 로딩해놓고 이후 application 내 controller들을 autoload하면 zeitwerk방식을 지원하지 않는 gem들과도 호환성문제가 없어지지 않을까란 생각. (https://github.com/rails/rails/issues/36381