모듈식 Rails SaaS 애플리케이션을 구조화하는 방법
Source: Dev.to

이전 글에서 저는 5년 전이라면 알았을 것 같은 Rails SaaS 아키텍처에 대해 이야기했습니다.
핵심 아이디어는 간단했습니다: Rails 애플리케이션이 실제 SaaS 제품으로 성장하면, 문제는 더 이상 기능을 빠르게 작성하는 것이 아니라는 것이었습니다.
진정한 도전 과제는 시스템을 이해하기 쉽게 유지하는 것이 됩니다.
그것은 자연스럽게 다음 질문을 낳습니다:
그 구조가 실제로 어떻게 보이는 걸까요?
이 글은 그 질문에 대한 제 답변 시도입니다. 이것이 Rails 앱을 조직하는 유일한 방법은 아니며, 여러 비즈니스 기능, 내부 도구, 장기적인 성장을 가진 제품에 현재 가장 적합하다고 생각되는 구조일 뿐입니다.
기본 성장 경로의 문제점
Most Rails applications begin with a very reasonable structure:
app/
config/
db/
lib/That works well at the beginning.
But as the product grows, many business capabilities start living side‑by‑side in the same application layer:
- 인증
- 역할 및 권한
- 알림
- 대시보드
- 감사
- 지원 티켓
- 파일 관리
- 청구 로직
- 관리자 도구
At that point, the issue is not that Rails is bad. The issue is that everything starts competing for space inside the same app boundaries.
- Models become aware of too much. → 모델이 너무 많은 것을 알게 됩니다.
- Controllers start coordinating unrelated concerns. → 컨트롤러가 관련 없는 관심사를 조정하기 시작합니다.
- Helpers grow in weird directions. → 헬퍼가 이상한 방향으로 성장합니다.
- Concerns multiply. → Concern가 늘어납니다.
Eventually the application feels large even when individual features are not that complex. → 결국 개별 기능이 그리 복잡하지 않음에도 애플리케이션이 크게 느껴집니다.
전환: 역량별 구조화
내게 더 효과적이었던 방법은 시스템을 비즈니스 역량에 따라 구조화하는 것이며, 기술 레이어만을 기준으로 하지 않는 것이다.
따라서 다음과 같이 생각하는 대신:
- models
- controllers
- views
다음과 같이 생각한다:
- support
- audit
- admin
- accounts
- users
- dashboards
- billing
각 역량마다 자체 경계를 가진다. Rails에서는 이를 구현하는 가장 깔끔한 방법이 engines를 사용하는 것이라고 나는 발견했다.
간단한 고수준 구조
A modular Rails SaaS application might look like this:
my_app/
├── app/
├── config/
├── db/
├── lib/
├── engines/
│ ├── lesli_core/
│ ├── lesli_admin/
│ ├── lesli_audit/
│ ├── lesli_billing/
│ ├── lesli_dashboard/
│ ├── lesli_shield/
│ └── lesli_support/
└── Gemfile메인 애플리케이션은 여전히 존재하지만, 이제 모든 기능이 쏟아지는 장소는 아닙니다.
대신, 메인 앱은 통합 레이어 역할을 하며, 엔진들은 실제 비즈니스 기능을 포함합니다.
메인 앱에 포함되어야 할 것은?
이 부분이 가장 중요합니다. 모듈식 구조는 메인 앱이 규율을 지킬 때만 작동합니다. 제 경우, 메인 Rails 앱은 보통 다음을 담당합니다:
- 환경 설정
- 배포 설정
- 부트 프로세스
- 엔진 마운트
- 앱‑특화 브랜딩 및 오버라이드
- 제품‑특화 커스텀 로직
- 시스템의 최종 구성
따라서 앱은 여전히 중요합니다—단지 모든 도메인을 직접 소유한다고 가장하지 않을 뿐입니다.
Source: …
엔진에 무엇이 포함되어야 할까?
각 엔진은 명확한 기능을 가집니다. 예를 들어, support 엔진은 다음과 같은 구조를 가질 수 있습니다:
engines/lesli_support/
├── app/
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── components/
├── config/
│ └── routes.rb
├── db/
│ └── migrate/
├── lib/
│ └── lesli_support/
└── lesli_support.gemspec그 엔진은 다음을 포함할 수 있습니다:
- 티켓
- 댓글 또는 토론
- 상태
- 우선순위
- 할당 흐름
- support‑specific 대시보드
- support와 관련된 알림
이렇게 하면 매우 가치 있는 것이 만들어집니다: 지역적 추론. support 작업을 해야 할 때, 다른 레이어에 섞여 있는 티켓 로직을 찾기 위해 거대한 애플리케이션을 뒤적이는 대신 support 엔진으로 바로 이동할 수 있습니다.
코어 레이어의 역할
나는 여전히 공유 코어 레이어를 갖는 것을 좋아하지만, 그것은 작고 의도적이어야 합니다.
내가 보기에 코어 엔진은 보통 다음과 같은 것들을 포함합니다:
- 진정으로 횡단하는 공유 관심사
- 공유 UI 프리미티브
- 기본 클래스
- 공통 헬퍼
- 플랫폼 구성 헬퍼
- 엔진 간 공통 인터페이스
내가 피하려고 하는 것은 코어 레이어를 두 번째 모놀리스로 만드는 것입니다.core가 모든 엔진이 공유 단축키를 던지는 장소가 된다면, 아키텍처는 서서히 같은 문제로 되돌아갑니다. 그래서 나는 계속해서 스스로에게 묻습니다:
이것이 정말 횡단적인가, 아니면 더 깔끔한 경계를 피하고 있는 것인가?
Source:
라우팅 및 구성
이 접근 방식의 장점 중 하나는 구성(composition)이 명시적으로 유지된다는 점입니다. 메인 애플리케이션이 무엇을 마운트할지 결정합니다. 간단한 예시:
# config/routes.rb
Rails.application.routes.draw do
mount LesliAdmin::Engine , at: "/admin"
mount LesliSupport::Engine , at: "/support"
mount LesliAudit::Engine , at: "/audit"
end이렇게 하면 최종 애플리케이션 구조를 이해하기 쉽습니다. 제품이 미스터리가 아니라, 기능들의 조합이라는 점이 명확해집니다.
경계가 재사용보다 더 중요합니다
엔진의 좋은 부수 효과는 재사용이지만, 그것이 제가 엔진을 좋아하는 주된 이유는 아닙니다. 더 큰 이점은 강제된 경계입니다.
- 경계가 없으면 모든 기능이 결국 서로에게 침투하게 됩니다.
- 경계가 있으면 통합을 의도적으로 해야 합니다.
이는 코드베이스가 성장하는 방식을 바꿉니다. 더 나은 질문을 스스로에게 하게 됩니다:
- 이 로직 조각이 기존 엔진에 속해야 할까요, 아니면 자체 기능을 가져야 할까요?
- 엔진 간에 어떻게 긴밀히 결합되지 않으면서 소통할 수 있을까요?
- 핵심 레이어에 진정으로 들어가야 할 것은 무엇이며, 특정 엔진에 포함되어야 할 것은 무엇일까요?
이러한 질문들을 앞에 두고 고민함으로써, 아키텍처는 모듈화되고 유지보수가 쉬우며 장기적인 성장에 대비할 수 있습니다.
Could This Dependency Exist?
- Does support really need to know about billing internals?
- Is this concern shared, or just misplaced?
- Should this logic live in the app, the engine, or the core layer?
Those questions improve architecture more than any naming convention ever will.
이것은 무료가 아니다
공정하게 말하자면, 이러한 구조는 약간의 오버헤드를 추가합니다. 다음과 같은 사항들을 더 많이 고민해야 합니다:
- 엔진 명명
- 도메인 경계
- 의존성 방향
- 엔진 간 마이그레이션
- 공유 컨벤션
- 로컬 개발 워크플로
따라서 저는 모든 프로젝트에 사용하지 않을 것입니다.
작은 내부 도구, MVP, 혹은 매우 빠르게 배포해야 하는 무언가를 구축하고 있다면, 이 구조가 너무 일찍 과도하게 느껴질 수 있습니다.
하지만 제품이 여러 기능에 걸쳐 성장하기 시작하면, 그 추가 구조가 무겁게 느껴지는 대신 유용하게 느껴지게 됩니다.
이 접근 방식에서 가장 마음에 드는 점
제가 가장 마음에 드는 점은 이것이 더 “고급”처럼 느껴진다는 것이 아니라, 시스템을 다루기 더 쉬워진다는 점입니다.
- 새 기능을 추가할 때, 그것이 어디에 들어가야 할지 더 명확히 알 수 있습니다.
- 무언가를 디버깅할 때, 애플리케이션의 무관한 부분을 오가며 헤매지 않게 됩니다.
- 리팩터링을 할 때, 주변을 모두 깨뜨리지 않을 것이라는 약간의 자신감을 가질 수 있습니다.
저에게는 이것이 좋은 아키텍처가 해야 할 일입니다.
- 추상화 점수를 얻으려는 것이 아니다.
- 다이어그램에서 인상적으로 보이려는 것이 아니다.
- 그저 앱이 성장함에 따라 이해하기 쉬워지도록 하는 것이다.
이것이 Lesli와 연결되는 방식
이는 제가 Lesli를 구축하면서 탐구해 온 동일한 일반적인 방향입니다.
Rails를 더 복잡하게 만들려는 것이 아닙니다.
주로 더 큰 SaaS‑스타일 애플리케이션이 성장할 수 있는 더 깔끔한 공간을 제공하려는 것입니다.
Lesli는 아직 진화 중이지만, 이 모듈식 접근 방식은 그 배경에 있는 아이디어 중 하나입니다.
