Rails
Rails 는 루비로 웹 애플리케이션을 만드는 프레임워크입니다. 데이터베이스를 다루는 자리, 화면을 그리는 자리, 요청을 받는 자리를 미리 한 벌로 갖춰 둡니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 루비로 웹 애플리케이션을 만들 때 쓰는 프레임워크입니다. 데이터베이스를 다루는 자리, 화면을 그리는 자리, 요청을 받는 자리를 한 벌로 묶어 줍니다. 뼈대를 찍는 명령 한 줄이면 이 세 자리가 갖춰진 파일이 한꺼번에 생깁니다.
왜 이렇게 하나 — 이름을 무엇으로 지을지를 매번 새로 정해야 한다면 정말 중요한 결정에 쓸 시간이 그만큼 줄어듭니다. 되풀이되는 결정을 줄이려는 것입니다.
어떻게 도나
- 이름과 구조를 관습으로 미리 정해 둡니다 —
Person클래스가 자동으로people테이블에 연결되는 식입니다 - 이름 규칙과는 별도로, 프로젝트에 쓸 도구(프레임워크) 묶음도 미리 골라 둡니다
- 명령 한 줄로 이 세 자리에 해당하는 파일을 한꺼번에 만들어 줍니다
대가 — 다른 언어의 옛 습관이나 낯선 패턴을 고집하며 관습을 받아들이지 않는 개발자에게는 즐겁지 않은 경험이 될 수 있습니다. 위험한 기능을 막는 장치도 따로 없습니다. 큰 판(버전)이 올라갈 때는 하위 호환이 깨져 애플리케이션이 부서질 수 있습니다.
상세
공식 가이드는 Rails 를 루비 프로그래밍 언어로 쓰인 웹 애플리케이션 개발 프레임워크라고 적습니다. 개발자가 시작할 때 무엇이 필요한지를 미리 가정해 두고, 그 가정 위에서 웹 애플리케이션 짜기를 더 쉽게 만드는 것이 설계 목표입니다. 데이터베이스를 다루는 자리, 화면을 그리는 자리, 요청을 받는 자리를 미리 한 벌로 갖춰 두는 것이 그 가정의 실체입니다.
Rails 는 스스로를 의견이 있는 소프트웨어라고 부릅니다. 일을 하는 "최선"의 방법이 하나 있다고 가정합니다. 그 길을 권하도록 설계했다고 적습니다. 어떤 경우에는 대안을 권하지 않도록 설계했다고도 밝힙니다. 다른 언어에서 들고 온 옛 습관을 고집하거나 딴 데서 배운 패턴을 그대로 쓰려 들면 덜 즐거운 경험을 하게 될 수도 있다는 말이 같은 자리에 붙어 있습니다. 가이드는 두 개의 큰 지침 원칙을 듭니다.
반복하지 않기
DRY(Don't Repeat Yourself, 반복하지 않기)는 소프트웨어 개발 원칙입니다. 모든 지식 조각은 시스템 안에서 하나의, 모호하지 않은, 권위 있는 표현을 가져야 한다는 것입니다. 같은 정보를 몇 번이고 다시 적지 않으면 코드가 더 유지보수하기 쉬워지고, 더 확장하기 쉬워지고, 버그가 줄어든다고 가이드가 적습니다.
설정보다 관습
Rails 는 웹 애플리케이션에서 많은 것을 하는 최선의 방법에 대해 의견을 갖고 있습니다. 끝없는 설정 파일로 개발자가 그것을 직접 정의하게 하는 대신, 그 관습 묶음을 기본값으로 삼습니다.
포기한 것
이름과 구조를 고를 자유
Rails 초기의 생산성 표어 하나는 "당신은 아름답고 유일한 눈송이가 아니다" 였다고
독트린이 적습니다. 헛된 개별성을 포기하면 시시한 결정의 수고를 건너뛰고 정말 중요한
자리에서 더 앞으로 나아갈 수 있다는 주장입니다. 데이터베이스 기본 키를 어떤 형식으로
적든 누가 신경 쓰나, id 인지 postId 인지 posts_id 인지 pid 인지가 정말 중요한가,
이것이 되풀이해서 심사숙고할 값어치가 있는 결정인가 — 아니라고 독트린은 답합니다. 웹용
정보 시스템을 만드는 개발자 앞에 놓인 두껍고 계속 자라는 반복 결정의 정글에 마체테를
휘두르는 것이 Rails 임무의 일부라고 밝힙니다.
설정을 관습으로 옮기면 되풀이되는 심사숙고에서 풀려납니다. 정해 둔 관습을 기본값으로 받는 동안은 끝없는 설정 파일에 그것을 직접 적을 일이 없습니다.
포기한 값은 이름입니다. Active Record 는 루비 객체로 나타낸 모델을 데이터베이스 테이블에 이어 주는 Rails 의 부품입니다. 모델 클래스 이름을 복수형으로 바꿔 대응하는 데이터베이스 테이블을 찾습니다. 여러 낱말로 된 클래스 이름은 루비 관습대로 UpperCamelCase 를 따르고, 그때 테이블 이름은 snake_case 가 됩니다.
| 모델 클래스 | 테이블 |
|---|---|
Article |
articles |
LineItem |
line_items |
Product |
products |
Person |
people |
얻은 것은 그 위에 얹히는 추상입니다. Person 클래스가 people 테이블에 대응한다고
믿을 수 있으면, has_many :people 로 선언된 연관을 찾을 때도 같은 규칙을 써서 Person
클래스를 찾을 수 있다고 독트린이 적습니다. 전문가의 생산성만이 아니라 초보자의 진입
장벽도 낮아집니다. Rails 에는 초보자가 알 필요조차 없이 모르는 채로 이득만 보는 관습이
아주 많다고 밝힙니다.
구성요소를 고를 권리
독트린은 이 자리를 오마카세에 빗댑니다. 무엇이 맛있는지 모르는 손님이 셰프에게 맡기면, "맛있다" 가 무엇인지 알기 전에도 맛있는 식사를 기대할 수 있습니다. Rails 자신도 하나의 틀 안에 여러 프레임워크를 묶어 담은 것입니다. 이렇게 프로젝트가 쓸 여러 프레임워크와 도구를 켜켜이 쌓아 놓은 조합, 곧 스택을 남이 조립하게 맡기는 이득도 설정보다 관습에서 얻는 것과 비슷하되 층이 더 높습니다. 설정보다 관습은 개별 프레임워크를 어떻게 가장 잘 쓰나에 매여 있고, 오마카세는 어느 프레임워크들을 쓰나와 그것들이 어떻게 맞물리나에 매여 있습니다.
이것은 쓸 수 있는 도구를 개별 선택지로 늘어놓고 고르는 특권과 부담을 프로그래머 개인에게 주는 오랜 전통과 어긋난다고 독트린이 밝힙니다. "작업에 맞는 최선의 도구를 써라" 는 말은 논쟁의 여지가 없어 보이지만, "최선" 을 자신 있게 가리려면 그것을 가릴 토대가 있어야 합니다. 그것이 보기보다 훨씬 어렵다는 것입니다. 그래서 Rails 는 프로그래머 개인이 자기 연장통의 도구를 하나하나 고를 특권을 줄이고, 대신 모두를 위한 더 나은 연장통을 택했다고 적습니다.
안전 난간
루비는 서랍에 날카로운 칼을 여럿 두고 있습니다. 사고가 아니라 설계라고 독트린이 적습니다.
가장 유명한 것이 몽키 패칭 — 이미 있는 클래스와 메서드를 바꾸는 힘입니다. String#capitalize
를 덮어써서 원래 구현에 기대던 온갖 주변 코드를 깨뜨리는 것을 프로그램 차원에서 막는
장치가 루비에는 없습니다. 무엇이 막아 주느냐는 물음에 독트린이 내놓는 답은 "아무것도
없다" 입니다.
그런 분별은 관습으로, 넌지시 미는 것으로, 교육으로 지킬 뿐이라고 적습니다. 부엌에서
날카로운 칼을 치우고 모두에게 숟가락으로 토마토를 썰라고 하지는 않는다는 것입니다. 얻은
것은 몽키 패칭의 반대쪽 얼굴입니다. 이미 있는 클래스를 덮어쓰는 것만이 아니라 거기에
새 메서드를 얹는 것도 같은 힘입니다. 2.days.ago 처럼 오늘로부터 이틀 전 날짜를 돌려주는
표현이 이렇게 기존 클래스에 메서드를 얹어 만든 것입니다. 이 맞바꿈이 손해라고 볼 수도
있다는 말을 독트린 스스로 덧붙입니다.
안정성
십 년 넘게 굴러온 시스템은 자연히 굳어지는 쪽으로 간다고 독트린이 적습니다. 모든 변경이 어딘가의 누군가에게 문제일 이유가 백만 가지 있고, 개인에게는 그것도 정당한 이유입니다. 그러나 보수의 목소리를 너무 가까이 들으면 건너편에 무엇이 있는지 영영 못 봅니다. 그래서 가끔은 감히 깨뜨리고 바꿔야 한다는 것이 이 값입니다.
대가는 큰 판이 올라갈 때 하위 호환이 깨져 내 애플리케이션이 부서지는 것입니다. 2.x 에서 3 으로 넘어가던 대이주의 아픈 기억이 그 자리에 있던 많은 사람에게 아직 흉터로 남아 있다고 독트린이 적습니다. 힘든 이주였다고 밝힙니다. 여럿을 오래 2.x 땅에 남겼습니다. 더러는 설득할 수 없을 만큼 마음이 상했다고 적습니다. 다만 이것이 필요 없거나 지나친 아픔을 아무렇게나 주는 면허는 아니라고 못 박습니다.
포기한 값은 판의 수명입니다. Rails 는 시맨틱 버저닝을 옮겨 놓은 판을 따르며, 판 번호를 x.y.z(메이저.마이너.패치) 형식으로 적습니다. 패치 z 는 버그 픽스만 담고 API(Application Programming Interface, 응용 프로그램 인터페이스) 변경도 새 기능도 담지 않습니다. 보안 픽스에 필요한 경우가 예외입니다. 마이너 y 는 새 기능을 담고 API 변경을 담을 수 있습니다. 메이저 x 는 새 기능을 담고 API 변경을 담을 공산이 큽니다. 새 기능은 main 브랜치에만 들어가고 패치 판으로는 오지 않습니다.
| 무엇 | 언제까지 |
|---|---|
| 버그 픽스 | 그 계열의 첫 판이 나온 뒤 1년 |
| 보안 픽스 | 그 계열의 첫 판이 나온 뒤 2년 |
유지보수 정책 문서는 예를 듭니다. 가상의 1.1.0 이 2023년 1월 1일에 나왔다면 버그 픽스는 2024년 1월 1일까지 받고, 그 뒤로는 지원되지 않는 것으로 봅니다. 보안 픽스는 2025년 1월 1일까지 받고, 그 뒤로는 수명이 끝납니다.
예시
프로젝트 하나 만들기
$ rails new store
$ cd store
$ bin/rails server
=> Booting Puma
=> Rails 8.1.0 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 6.4.3 (ruby 3.3.5-p100) ("The Eagle of Durango")
* Min threads: 3
* Max threads: 3
* Environment: development
* PID: 12345
* Listening on http://127.0.0.1:3000
rails new 가 만드는 애플리케이션은 플래그로 바꿀 수 있습니다. 어떤 선택지가 있는지는
rails new --help 가 보여 줍니다. 애플리케이션 디렉터리 안에서 명령을 돌릴 때는
bin/rails 를 씁니다. 그래야 그 애플리케이션이 가진 판의 Rails 가 쓰입니다. 이 명령은
Puma 라는 웹 서버를 띄워 정적 파일과 애플리케이션을 서빙합니다.
관습이 코드에 드러난 자리
라우트는 애플리케이션이 어떤 URI(Uniform Resource Identifier, 통합 자원 식별자) 경로에 응답하는지를 정해 두는 대응표입니다. 이 대응표에 다음 한 줄을 적습니다.
resources :products
bin/rails routes 를 돌리면 그 대응표가 전부 나옵니다. 위 한 줄이 만든 것은 이렇습니다.
| Prefix | Verb | URI Pattern | Controller#Action |
|---|---|---|---|
| products | GET | /products(.:format) |
products#index |
| POST | /products(.:format) |
products#create |
|
| new_product | GET | /products/new(.:format) |
products#new |
| edit_product | GET | /products/:id/edit(.:format) |
products#edit |
표의 Controller#Action 열에서 Controller 가 앞서 말한 요청을 받는 자리, 곧
컨트롤러입니다. 데이터베이스를 다루는 자리는 모델이, 화면을 그리는 자리는 뷰가 채웁니다.
이 CRUD(Create-Read-Update-Delete, 생성·조회·수정·삭제) 액션을 다 원하지 않으면 필요한 것만 짚어서 적을 수 있습니다.
모델 클래스는 이렇게 생겼습니다.
class Product < ApplicationRecord
end
Product 가 상속하는 ApplicationRecord 는 각 모델에 Active Record 의 동작을 실어 주는
Rails 의 기반 클래스입니다. 이 클래스에 코드가 하나도 없다는 것이 놀라울 수 있다고 가이드가
적습니다. Product 모델이 쓰일 때 Rails 가 데이터베이스 테이블에 컬럼 이름과 타입을
물어보고, 그 속성들에 대한 코드를 자동으로 만들어 냅니다.
뼈대를 찍는 명령
$ bin/rails generate scaffold Post title:string body:text
invoke active_record
create db/migrate/20250919150748_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
invoke resource_route
route resources :posts
invoke scaffold_controller
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
create app/views/posts/index.html.erb
create app/views/posts/edit.html.erb
create app/views/posts/show.html.erb
create app/views/posts/new.html.erb
create app/views/posts/_form.html.erb
create app/views/posts/_post.html.erb
명령 한 줄이 마이그레이션 파일, 모델, 테스트, 픽스처(테스트에서 쓸 미리 준비한 샘플 데이터), 라우트 한 줄, 컨트롤러, 그리고 뷰 여섯 개를 한꺼번에 만듭니다.
이걸 채택해 굴리는 시스템
GitHub 는 자사 엔지니어링 블로그에서 github.com 이 처음부터 Ruby on Rails 모놀리스(여러 서비스로 쪼개지 않고 기능 전부를 한 코드베이스 한 애플리케이션에 담은 구조)였다고 밝힙니다. 그 글을 쓸 당시 애플리케이션은 2백만 줄에 가까웠습니다. 1,000명이 넘는 엔지니어가 매일 함께 작업했습니다. 하루에 20번까지 배포했습니다. 거의 매주 그 배포 중 하나가 Rails 업그레이드였다고 적습니다.
운영
프로세스 수와 스레드 수
Puma 설정에서 가장 중요한 둘은 워커당 스레드 수와 워커 수입니다. Puma 는 프로세스를 워커라고 부릅니다.
| 설정 | 무엇을 정하나 | 기본값 |
|---|---|---|
threads 지시자 · RAILS_MAX_THREADS |
워커당 스레드 수 | 기본 생성 설정에서 3 |
workers 지시자 · WEB_CONCURRENCY |
워커 수 | 가이드가 값을 적지 않음 |
이 둘은 층이 다른 설정입니다. Puma 는 워커 프로세스를 여럿 띄우고, 워커 프로세스마다
다시 스레드를 여럿 둡니다. workers 가 바깥 층의 개수를, threads 가 그 안쪽 층의
개수를 정합니다. Puma 는 들어오는 요청을 이 워커 프로세스들에 나눠 보냅니다.
flowchart TD
R["요청"]
M["Puma"]
R --> M
subgraph W1["워커 프로세스 1"]
direction TD
T1["스레드 1"]
T2["스레드 2"]
T3["스레드 3"]
end
subgraph W2["워커 프로세스 2"]
direction TD
T4["스레드 1"]
T5["스레드 2"]
T6["스레드 3"]
end
subgraph WN["워커 프로세스 N"]
direction TD
T7["스레드 1"]
T8["스레드 2"]
T9["스레드 3"]
end
M --> W1
M --> W2
M --> WN
워커마다 그려 둔 스레드 셋은 threads 의 기본값 3을 그대로 씁니다. 워커 수 N 은
가이드가 값을 정해 두지 않은 자리라 개수 대신 자리만 비워 둡니다.
워커당 스레드를 하나 넘게 쓴다면, 워커 수는 서버에서 쓸 수 있는 CPU(Central Processing Unit, 중앙처리장치) 코어 수로 잡아야 합니다. 한 서버에서 여러 애플리케이션을 굴린다면 이 애플리케이션에 쓰게 하고 싶은 코어 수로 잡습니다. 워커당 스레드를 하나만 쓴다면, 워커 수를 코어 수보다 많게 잡을 수 있습니다. 워커가 입출력을 기다리며 노는 시간을 메우는 몫입니다.
WEB_CONCURRENCY=auto 로 두면 Puma 워커 수가 쓸 수 있는 CPU 수에 맞춰 자동으로
조정됩니다. 다만 CPU 를 나눠 쓰는 클라우드 호스트나 CPU 개수를 부정확하게 보고하는
플랫폼에서는 이 설정이 부정확할 수 있다고 가이드가 적습니다.
YJIT
최근 루비 판에는 YJIT(Yet Another Ruby JIT, 또 다른 루비 JIT라는 뜻)라는 JIT(Just-In-Time, 실시간) 컴파일러가 함께 옵니다. JIT 컴파일러는 메모리를 좀 더 쓰는 대가로 코드를 더 빨리 실행하게 해 줍니다. 가이드는 이 추가 메모리를 정말 감당할 수 없는 경우가 아니라면 YJIT 를 켜기를 강력히 권한다고 적습니다.
Rails 7.2 기준으로, 애플리케이션이 루비 3.3 이상에서 돈다면 YJIT 가 Rails 에 의해 기본으로
켜집니다. 그보다 옛 Rails 나 루비 판은 직접 켜야 합니다. 추가 메모리 사용이 문제라면
YJIT 를 아예 끄기 전에 --yjit-exec-mem-size 설정으로 메모리를 덜 쓰도록 조정해 볼 수
있습니다.
flowchart TD
A{"Rails 7.2 기준, 루비 3.3 이상에서 도나"}
A -->|"그렇다"| B["YJIT 가 기본으로 켜짐"]
A -->|"아니다(옛 Rails·옛 루비)"| C["직접 켜야 함"]
B --> D{"추가 메모리 사용이 문제인가"}
C --> D
D -->|"아니다"| E["그대로 켜 둠"]
D -->|"그렇다"| F["--yjit-exec-mem-size 로 먼저 메모리 사용량 조정"]
F --> G["그래도 문제면 YJIT 를 아예 끔"]
캐시 스위치와 만료 순간
캐싱을 둘러싼 운영 손잡이는 자리가 둘입니다 — 캐싱 자체를 켜고 끄는 스위치 하나와, 캐시 항목이 만료되는 순간을 다루는 손잡이 하나입니다.
config.action_controller.perform_caching 값을 바꾸면 요청을 받는 컨트롤러 부품인
Action Controller 가 제공하는 캐싱에만 영향이 갑니다. 코드에서 직접 캐시를 읽고 쓰는
저수준 캐싱에는 영향을 주지 않습니다. 개발 환경에서 켜려면 config/environments/development.rb
에서 perform_caching 을 true 로 둡니다.
ActiveSupport::Cache::Store#fetch 는 저수준 캐싱 쪽 메서드입니다. 키가 있으면 그 값을
읽고, 없거나 만료됐으면 블록을 실행해 값을 만들고 캐시에 써 둔 뒤 그 값을 돌려줍니다. 이
메서드의 :race_condition_ttl 은 만료된 값이 새 값을 만드는 동안 다시 쓰일 수 있는 초
수를 정합니다. 캐시 항목이 만료될 때 여러 프로세스가 같은 항목을 동시에 다시 만들어 내는
것을 막는 데 쓸 수 있습니다. 도그 파일 효과라고도 부르는 자리입니다.
어떤 프로세스가 :race_condition_ttl 초보다 덜 지나 만료된 항목을 만나면, 새 값을
만들기 전에 만료 시각을 :race_condition_ttl 초만큼 밉니다. 늘어난 그 시간 창 동안
다른 프로세스들은 옛 값을 계속 씁니다. 첫 프로세스가 새 값을 쓰고 나면 다른 프로세스들이
그 값을 씁니다. 첫 프로세스가 새 값을 만들다 오류로 죽으면, 늘어난 시간 창이 지난 뒤
다른 프로세스가 새 값 만들기를 시도할 수 있습니다.
sequenceDiagram
participant A as 프로세스 A
participant 캐시
participant B as 프로세스 B
A->>캐시: 방금 만료된 항목을 만남
Note over 캐시: 만료 시각을 race_condition_ttl 만큼 밀어 둠
A->>A: 새 값 생성
B->>캐시: 같은 키 읽기
캐시-->>B: 옛 값
alt A가 새 값 만들기에 성공
A->>캐시: 새 값 쓰기
B->>캐시: 같은 키 다시 읽기
캐시-->>B: 새 값
else A가 새 값을 만들다 오류로 죽음
Note over 캐시: 늘어난 시간 창 동안 옛 값 유지
Note over B: race_condition_ttl 이 지난 뒤 다른 프로세스가 새 값 만들기를 시도
end
관련 항목
Rails 를 이루는 부품
Active Record · Active Support · Action Mailer · Action Mailbox · Action Text · Active Job · Active Storage · Action Cable
Rails가 딛고 선 도구와 관습
루비 · Puma · 마이그레이션 · 시맨틱 버저닝 · 몽키 패칭 · UpperCamelCase · snake_case
Rails가 구현하는 패턴과 경계하는 문제
MVC(Model-View-Controller, 모델-뷰-컨트롤러) · ORM(Object-Relational Mapping, 객체-관계 매핑) · 크로스 사이트 요청 위조 · N+1 · 캐시 스탬피드 · 도그 파일
다른 이름: Ruby on Rails · rails