Ruby on Rails 8 MVC Architecture
Ruby on Rails의 MVC 아키텍처와 각 레이어의 역할, Convention over Configuration 원칙을 정리합니다.
Model
데이터와 비즈니스 로직을 담당합니다.
- 레일즈의 모델은
ActiveRecord라는 ORM의 구현체입니다. - DB에서 하나의 row(데이터)를 가져오면 그게 객체가 되어, 해당 객체에서 attribute에 바로 접근할 수 있습니다.
- 객체를 통해 수정도 가능하기 때문에 직접 쿼리를 작성할 필요가 거의 없습니다.
ActiveRecord 주요 기능
Associations (연관 관계)
모델 간의 관계를 선언적으로 표현합니다. Spring의 @OneToMany, @ManyToOne 같은 역할을 단 한 줄로 처리합니다.
1
2
3
4
5
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :tags, through: :post_tags
end
Validations (유효성 검증)
DB에 데이터를 저장하기 전 유효성을 검증합니다. Spring의 @Valid, @NotNull 어노테이션과 같은 역할입니다.
1
2
3
4
5
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :name, length: { minimum: 2, maximum: 50 }
validates :age, numericality: { greater_than_or_equal_to: 0 }
end
Scopes (재사용 가능한 쿼리)
자주 사용하는 쿼리 조건을 메서드처럼 체이닝해서 사용할 수 있습니다. JPA의 @Query나 QueryDSL과 유사하지만, 체이닝이 훨씬 자연스럽습니다.
1
2
3
4
5
6
7
8
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_author, ->(user) { where(user: user) }
end
# 체이닝 사용
Post.published.recent.by_author(current_user)
Callbacks
레코드의 생명주기(저장 전, 저장 후, 삭제 전 등)에 훅을 걸 수 있습니다.
1
2
3
4
5
6
7
8
9
10
class Order < ApplicationRecord
before_create :set_order_number
after_update :notify_status_change, if: :saved_change_to_status?
private
def set_order_number
self.order_number = SecureRandom.hex(8)
end
end
Fat Model
레일즈는 Fat Model 패턴을 지향합니다. 비즈니스 로직을 모델에 집중시키는 방식입니다.
- 처음에는 모델에 로직이 많지 않지만, 시간이 지날수록 여러 모델이 추가되고 복잡도가 높아집니다.
- 복잡한 쿼리, 유효성 검증, 재사용 가능한 메서드를 모델에 정의합니다.
실무에서는 모델이 비대해지는 것을 방지하기 위해 Service Object, Concern, Query Object 같은 패턴을 함께 활용하기도 합니다.
Service Object 패턴
모델이 너무 비대해지면 Service Object로 분리합니다. Spring의 @Service 레이어와 같은 역할입니다. 여러 모델에 걸친 복잡한 트랜잭션 처리나 외부 API 호출이 들어갈 때 특히 유용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# app/services/order_completion_service.rb
class OrderCompletionService
def initialize(order, user)
@order = order
@user = user
end
def call
ActiveRecord::Base.transaction do
@order.update!(status: :completed)
@user.increment!(:points, @order.points_earned)
OrderMailer.completion_email(@order).deliver_later
end
end
end
# 컨트롤러에서 호출
OrderCompletionService.new(@order, current_user).call
View
사용자 인터페이스를 담당합니다.
- 데이터를 어떻게 표현할지를 결정합니다.
- 레일즈의 기본 템플릿 언어는 ERB(Embedded Ruby)로, HTML 안에 Ruby 코드를 삽입할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
<%# app/views/posts/index.html.erb %>
<h1>게시글 목록</h1>
<% @posts.each do |post| %>
<div class="post">
<h2><%= post.title %></h2>
<p>작성자: <%= post.user.name %></p>
<%= link_to '상세보기', post_path(post) %>
</div>
<% end %>
<% %> 는 Ruby 코드 실행, <%= %> 는 실행 결과를 출력합니다.
Partial (파셜)
반복되는 뷰 컴포넌트를 별도 파일로 분리해 재사용하는 방식입니다. React의 컴포넌트 개념과 유사하며, 파일명은 반드시 언더스코어(_)로 시작합니다.
1
2
3
4
5
6
7
8
9
10
11
<%# app/views/posts/_post_card.html.erb %>
<div class="post-card">
<h2><%= post.title %></h2>
<p><%= truncate(post.body, length: 100) %></p>
</div>
<%# 사용하는 곳에서 %>
<%= render 'post_card', post: @post %>
<%# 컬렉션 렌더링 (N+1 주의) %>
<%= render partial: 'post_card', collection: @posts %>
- 레일즈의 뷰 레이어를 사용하지 않고 React, Vue 같은 별도 프론트엔드 프레임워크와 API 형태로 연동하는 경우도 많습니다.
- Rails 7 이후에는 Hotwire(Turbo + Stimulus)를 내장하여, JavaScript 없이도 동적인 UX를 구현할 수 있는 방향을 권장하고 있습니다.
Controller
Model과 View 사이의 중개자 역할을 합니다.
Thin Controller
레일즈는 Thin Controller 패턴을 지향합니다. 컨트롤러는 간결하게 유지하고, 복잡한 로직은 모델로 위임합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.published.recent
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: '게시글이 작성되었습니다.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
before_action: 특정 액션 전에 공통 로직을 실행합니다. Spring의 HandlerInterceptor나 AOP와 유사한 역할입니다.
Strong Parameters: 허용된 파라미터만 통과시켜 Mass Assignment 취약점을 방지합니다. 허용하지 않은 필드는 자동으로 걸러지기 때문에, 실수로 민감한 컬럼이 수정되는 상황을 막을 수 있습니다.
Rails MVC 요청 흐름
- 브라우저에서 요청 발생 (
GET /posts) - 라우터가 요청을 적절한 컨트롤러 액션으로 전달 (
PostsController#index) 컨트롤러가 필요한 모델 데이터 로드
1 2
@posts = Post.all # Post.all은 클래스 레벨 메서드 # @posts는 인스턴스 변수로, 뷰에서 접근 가능
- 모델이 데이터베이스에서 데이터 조회
- 컨트롤러가 뷰에 데이터 전달
- 뷰가 HTML 생성
- 컨트롤러가 HTML 응답을 브라우저로 반환
Convention over Configuration
“설정보다 관례(CoC)”는 레일즈의 핵심 철학입니다. 명시적 설정 없이도 이름 규칙만 따르면 레일즈가 알아서 연결해줍니다.
| 요소 | 명명 규칙 | 예시 |
|---|---|---|
| 모델 | 단수형, 카멜케이스 | Post |
| 테이블 | 복수형, 스네이크케이스 | posts |
| 컨트롤러 | 복수형, 카멜케이스 | PostsController |
| 뷰 폴더 | 컨트롤러명 복수형 | app/views/posts/ |
| 뷰 파일 | 액션명.html.erb | app/views/posts/index.html.erb |
Spring에서는 @Controller, @Repository, @RequestMapping 등을 명시적으로 설정해야 하지만, 레일즈는 이름 규칙만 맞추면 별도 설정 없이 자동으로 연결됩니다.
RESTful 라우팅 관례
routes.rb에서 resources :posts 한 줄이면 7개의 RESTful 라우트가 자동 생성됩니다.
1
2
3
Rails.application.routes.draw do
resources :posts
end
| HTTP 메서드 | 경로 | 컨트롤러#액션 | 용도 |
|---|---|---|---|
| GET | /posts | posts#index | 목록 |
| GET | /posts/new | posts#new | 작성 폼 |
| POST | /posts | posts#create | 생성 |
| GET | /posts/:id | posts#show | 상세 |
| GET | /posts/:id/edit | posts#edit | 수정 폼 |
| PATCH/PUT | /posts/:id | posts#update | 수정 |
| DELETE | /posts/:id | posts#destroy | 삭제 |
rake routes 명령으로 현재 정의된 모든 라우트를 확인할 수 있습니다.