Post

Ruby on Rails 8 MVC Architecture

Ruby on Rails의 MVC 아키텍처와 각 레이어의 역할, Convention over Configuration 원칙을 정리합니다.

출처: [심화] 인디해커를 위한 루비온레일즈 8 완벽 강의

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 요청 흐름

  1. 브라우저에서 요청 발생 (GET /posts)
  2. 라우터가 요청을 적절한 컨트롤러 액션으로 전달 (PostsController#index)
  3. 컨트롤러가 필요한 모델 데이터 로드

    1
    2
    
    @posts = Post.all  # Post.all은 클래스 레벨 메서드
                       # @posts는 인스턴스 변수로, 뷰에서 접근 가능
    
  4. 모델이 데이터베이스에서 데이터 조회
  5. 컨트롤러가 뷰에 데이터 전달
  6. 뷰가 HTML 생성
  7. 컨트롤러가 HTML 응답을 브라우저로 반환

Convention over Configuration

“설정보다 관례(CoC)”는 레일즈의 핵심 철학입니다. 명시적 설정 없이도 이름 규칙만 따르면 레일즈가 알아서 연결해줍니다.

요소명명 규칙예시
모델단수형, 카멜케이스Post
테이블복수형, 스네이크케이스posts
컨트롤러복수형, 카멜케이스PostsController
뷰 폴더컨트롤러명 복수형app/views/posts/
뷰 파일액션명.html.erbapp/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/postsposts#index목록
GET/posts/newposts#new작성 폼
POST/postsposts#create생성
GET/posts/:idposts#show상세
GET/posts/:id/editposts#edit수정 폼
PATCH/PUT/posts/:idposts#update수정
DELETE/posts/:idposts#destroy삭제

rake routes 명령으로 현재 정의된 모든 라우트를 확인할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.