최근에 질문을 받았다.

Active Record는 다른 ORM 과 달리 어떻게 알아서 데이터를 매핑하나요?

너무 당연히 쓰고 있어서 대답을 하지 못했다.
그냥 schema 파일을 알아서 읽어서 하는 게 아닐까?라고 어렴풋이 생각하고 검색해보고 넘어갔기에 설명을 해줄 수 있을 만큼 자세히 알고 있지 못했다.
그리고 이 글에서도 아주 자세히 쓰진 못할 것 같다.
어렴풋이 생각한 게 대충 맞았지만 자세히 설명하기 위해서는 많은 부분을 설명해야 하기 때문이다.

ActiveRecord는 어떻게 알아서 DB의 컬럼을 매핑해서 ActiveRecord 패턴 을 만들어줄까?

Convention over configuration

Ruby on Rails를 접하면 많이 들어 본 문장 Convention over configuration.(줄여서 CoC)
Rails 프레임워크는 컨벤션(관례)을 통해 암묵적으로 설정을 정의하지 않아도 된다는 특징이 있다.
이 점이 매핑을 알아서 해주는 맹점이었다.

공식 Rails Guide를 보면 Model은 단수형, 데이터베이스 컬럼은 복수형 네이밍을 하게끔 되어있고, 관계 테이블의 네이밍 또한 제시해준다.

예를 들면 모델/클래스와 테이블/스키마 간의 컨벤션은 아래와 같다.

Model / Class Table / Schema
Article articles
LineItem line_items
Deer deers
Mouse mice
Person people

외래키(Foreign key)의 경우에도 단수형 이름으로 생성한다. (예: user_id, order_id)<br/> 주 키(Primary Key) 또한 기본적으로 id` 를 사용한다.

이외에도 다양한 규칙들이 있는데 Rails Guide를 참고하면 된다.

만약 이 규칙을 지키지 않을 경우에는 직접 명시해서 사용하는 방법도 있다. self.table_name, self.inheritance_column 의 값을 변경함으로 테이블 명이나 STI 컬럼을 변경할 수 있다.

ActiveRecord::Base

Rails에서 모델에 상속받는 ActiveModelActiveRecord::Base를 상속받는다.

그래서 모든 이유는 여기에 있지 않을까 하여 뜯어보았다.

ActiveRecord::Base에서 발견한 ActiveRecord::Attributes 모듈에서 의심이 되는 코드들을 발견하였다.

class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal:

...

def define_attribute(
  name,
  cast_type,
  default: NO_DEFAULT_PROVIDED,
  user_provided_default: true
)
  attribute_types[name] = cast_type
  define_default_attribute(name, default, cast_type, from_user: user_provided_default)
end

이 코드가 결국 동작하는게 클래스에 Attribute를 세팅해주는 것 같았다.
확인을 해보기 위해 아래와 같이 콘솔에서 실행했다.

Class::class_attribute :test, default: [] # =>  [:test]
Class.test # => []
Class.test = 1 # => 1
Class.test # => 1

해당 코드는 아주 단순히 찾았지만 블로그에 포스팅을 하기 위해 조금 더 찾아보니 정말 복잡하게 구현되어 있었다.

결론

컨벤션(불가피 할 경우 명시적인 설정)에 의하여 schema에서 모델 클래스의 DB 이름을 찾아 attribute를 세팅한다.

세팅을 할 때 default 값 등이 있을 경우 이에 대한 내용도 함께 세팅한다.

예를 들면

# Schema.rb
create_table "users", force: :cascade do |t|
  t.string "name", null: false
  t.string "nickname", default: '익명유저', null: false
  t.string "email", null: false
end

다음과 같은 스키마가 있다면

class User < ApplicationRecord
  attribute :name, :string
  attribute :nickname, :string, default: '익명유저'
  attribute :email, :string,
end

와 같이 만들어준다고 생각하면 될 듯 하다. (사실은 더욱 복잡하지만..)

내부적으로 호출하는 ActiveModel::Naming ActiveModel::Atribute, ActiveRecord::ModelSchema ActiveRecord::Attributes등 각각 기능과 이름이 명확한 모듈들이 나눠서 역할을 분배하고 있었다.

종종 코어 부분을 분석해봐야겠다는 생각이 들었다.

느낀 점

루비를 왜 메타 프로그래밍 언어라고 부르는 지 정말 잘 알 수 있었다.
아직도 자세히 알고 말했다고 할 수 없을 정도로 복잡하고 어렵다.

그리고 너무 돌아가는 코드만 짜기에 급급했던 것은 아니었나 반성해보며 조금 더 코어의 내용을 학습해야겠다는 생각이 들었다.