💎

DHH의 Rails 코딩 스타일

바닐라 Rails로 충분하다 — 37시그널스의 철학

DHH(David Heinemeier Hansson)는 Rails 창시자이자 37시그널스(Basecamp, HEY)의 CTO입니다. 수십 년간 프로덕션 Rails 코드를 작성하며 정립한 코딩 스타일은 "단순함"과 "프레임워크 신뢰"에 기반합니다.


1. 바닐라 Rails로 충분하다

습관적으로 설치하는 외부 라이브러리(Devise, Redis, Sidekiq 등)를 최소화하고, 프레임워크에 내장된 기본 기능을 최대한 활용하라는 원칙입니다.

실제로 37시그널스는 외부 의존성을 줄이고 데이터베이스만으로 백그라운드 작업과 캐시 등을 처리하여 배포와 운영을 단순화합니다.

# ❌ 습관적으로 추가하는 외부 gem
gem 'devise'        # → has_secure_password로 충분한 경우가 많음
gem 'sidekiq'       # → Rails 8의 Solid Queue로 대체 가능
gem 'redis'         # → Solid Cache, Solid Cable로 DB 기반 대체
gem 'pundit'        # → 간단한 before_action 콜백으로 충분할 수 있음

# ✅ Rails 내장 기능 먼저 검토
has_secure_password          # 인증
ActiveJob + Solid Queue      # 백그라운드 작업
Rails.cache + Solid Cache    # 캐싱
Action Cable + Solid Cable   # WebSocket

핵심: gem을 추가하기 전에 "Rails 내장 기능으로 해결할 수 없는가?"를 먼저 자문하세요.


2. 모든 것을 CRUD로 매핑하라

커스텀 액션을 만드는 대신, 새로운 리소스를 만들어 표준 CRUD(Create/Read/Update/Delete)에 매핑합니다.

# ❌ 커스텀 액션 — 컨트롤러가 비대해짐
resources :cards do
  member do
    patch :close      # CardsController#close
    patch :reopen     # CardsController#reopen
    patch :archive    # CardsController#archive
  end
end

# ✅ 새 리소스로 CRUD 매핑 — 일관된 패턴
resources :cards do
  resource :closure, only: [:create, :destroy]   # create=닫기, destroy=다시 열기
  resource :archival, only: [:create, :destroy]  # create=보관, destroy=복원
end

이렇게 하면:

  • ClosuresController#create = 카드 닫기

  • ClosuresController#destroy = 카드 다시 열기

  • 모든 컨트롤러가 index/show/new/create/edit/update/destroy 7개 액션만 사용

  • 라우팅이 RESTful하고 예측 가능


3. 상태를 불리언(Boolean)이 아닌 레코드로 관리하라

특정 상태를 true/false 컬럼으로 추가하는 대신, 별도의 레코드를 생성하여 관리합니다.

# ❌ 불리언 컬럼 — 히스토리 없음
class Card < ApplicationRecord
  # closed: boolean (컬럼)
  # 언제 닫혔는지? 누가 닫았는지? 알 수 없음
end

# ✅ 레코드로 관리 — 자연스러운 히스토리
class Closure < ApplicationRecord
  belongs_to :card
  belongs_to :user     # 누가 닫았는지
  # created_at          # 언제 닫았는지 (자동)
end

class Card < ApplicationRecord
  has_many :closures

  def closed?
    closures.exists?
  end

  def closed_by
    closures.last&.user
  end
end

장점:

  • 언제, 누가 닫았는지 자연스럽게 기록

  • 닫기/열기 이력 전체 조회 가능

  • 상태 변경에 대한 감사(audit) 로그 자동 생성

  • destroy로 상태 취소 가능 (불리언 토글보다 명확)


학습 방법

37시그널스의 오픈소스 코드를 GitHub에서 직접 살펴보세요:

  • Campfire — 37시그널스의 채팅 앱 (Rails 기본 기능만으로 구현)

  • ONCE — 한 번 사서 영원히 쓰는 앱 (외부 의존성 최소화 철학)

  • Basecamp — DHH의 핵심 프로덕트

실천 과제: 다음 프로젝트에서 습관적으로 쓰던 외부 라이브러리 하나를 빼고 직접 구현해 보세요. has_secure_password로 Devise 없이 인증을 만들어 보거나, Solid Queue로 Sidekiq을 대체해 보는 것에서 시작합니다.

핵심 포인트

1

새 기능 추가 전 → "Rails 내장으로 할 수 있나?" 먼저 확인

2

커스텀 액션이 필요할 때 → 새 리소스(Controller)로 분리하여 CRUD 매핑

3

상태 변경 기능 → 불리언 컬럼 대신 별도 모델(레코드) 생성

4

Gemfile에 gem 추가 전 → Rails 8 Solid 시리즈(Queue/Cache/Cable) 검토

5

37시그널스 오픈소스(Campfire, ONCE) 코드를 읽으며 패턴 학습

6

다음 프로젝트에서 외부 gem 하나를 빼고 직접 구현해보기

장점

  • 배포·운영이 단순해짐 (Redis, 별도 워커 불필요)
  • 의존성 줄어 보안 취약점 감소
  • 컨트롤러가 일관된 7개 액션 패턴 유지
  • 상태 변경 히스토리가 자동으로 남음
  • RESTful 라우팅으로 API 예측 가능
  • Rails 업그레이드 시 깨지는 부분 최소화

단점

  • 초기에는 직접 구현하는 게 느릴 수 있음 (학습 비용)
  • 리소스 분리가 과하면 컨트롤러 파일 수 증가
  • 팀원이 관습적 Rails(Devise 등)에 익숙하면 혼란 가능
  • 대규모 서비스에서는 Redis/Sidekiq이 성능상 필요할 수 있음

사용 사례

인증: Devise → has_secure_password (Rails 내장) 백그라운드 잡: Sidekiq → Solid Queue (Rails 8) 캐싱: Redis → Solid Cache (DB 기반) 카드 닫기: patch :close → ClosuresController#create 구독 취소: patch :unsubscribe → CancellationsController#create 게시글 공개: update(published: true) → PublicationsController#create 주문 완료: update(completed: true) → CompletionsController#create