12 Factor App — 클라우드 네이티브 애플리케이션 설계 원칙
클라우드에 배포하면 "그냥 되는" 줄 알았는데, 왜 어떤 앱은 잘 돌아가고 어떤 앱은 문제투성이일까요?
이게 뭔가요?
12 Factor App은 Heroku의 공동 창업자가 정리한, 클라우드 환경에서 잘 동작하는 SaaS 애플리케이션을 만들기 위한 12가지 설계 원칙입니다.
2012년에 발표됐지만, 컨테이너와 쿠버네티스 시대인 지금도 거의 그대로 적용됩니다.
왜 필요한가요?
- 로컬에서 잘 돌아가는 앱이 클라우드에서 문제를 일으키는 이유를 체계적으로 설명
- 수평 확장, 무중단 배포, 환경 간 이동(dev → staging → prod)을 쉽게 만드는 패턴
- 컨테이너(Docker), 오케스트레이션(K8s) 환경의 기본 전제 조건
12가지 원칙
1. Codebase — 하나의 코드베이스, 여러 배포
하나의 Git 저장소 → 여러 환경에 배포
┌─────────────┐
│ Git Repo │──→ dev 환경
│ (하나) │──→ staging 환경
└─────────────┘──→ production 환경
✗ 환경별로 코드가 다르면 안 됨
✗ 여러 앱이 한 저장소에 있으면 분리 필요
2. Dependencies — 명시적 의존성 선언
✓ 좋은 예:
package.json, build.gradle, requirements.txt에 모든 의존성 명시
→ npm install, pip install -r requirements.txt로 재현 가능
✗ 나쁜 예:
"서버에 curl이 설치되어 있을 거야"라고 가정
→ 시스템 의존성도 Dockerfile에 명시해야 함
3. Config — 환경 변수로 설정 분리
# ✗ 코드에 하드코딩
db_url = "jdbc:mysql://prod-db:3306/myapp"
# ✓ 환경 변수로 분리
db_url = os.environ['DATABASE_URL']
환경별로 달라지는 값(DB 주소, API 키, 포트)은 코드가 아닌 환경 변수에 둡니다. 코드 변경 없이 설정만 바꿔서 배포할 수 있어야 합니다.
4. Backing Services — 부착된 리소스로 취급
DB, 메시지 큐, 캐시, 메일 서비스 등을 교체 가능한 리소스로 취급
로컬 MySQL → AWS RDS로 교체할 때
코드 변경 없이 DATABASE_URL 환경 변수만 변경
DATABASE_URL=mysql://localhost:3306/myapp (로컬)
DATABASE_URL=mysql://rds.amazonaws.com/myapp (프로덕션)
5. Build, Release, Run — 빌드와 실행의 엄격한 분리
Build: 코드 + 의존성 → 실행 가능한 아티팩트 (JAR, Docker 이미지)
Release: 아티팩트 + 설정 → 릴리스 (버전 태깅)
Run: 릴리스 실행
✗ 운영 서버에서 직접 코드 수정하면 안 됨
✓ 항상 빌드 → 릴리스 → 실행 순서를 지켜야 함
6. Processes — 무상태 프로세스
✗ 나쁜 예: 파일 시스템에 세션 저장
// 서버가 여러 대면 세션이 공유되지 않음
app.use(session({ store: new FileStore() }));
✓ 좋은 예: 외부 저장소에 상태 저장
// Redis에 세션 저장 → 어떤 서버가 처리해도 동일
app.use(session({ store: new RedisStore() }));
프로세스는 언제든 죽고 다시 시작될 수 있다고 가정합니다. 로컬 파일이나 메모리에 저장한 상태는 사라집니다.
7. Port Binding — 포트 바인딩으로 서비스 노출
앱 자체가 HTTP 서버 역할 (별도 웹 서버 불필요)
→ Spring Boot의 내장 Tomcat, Node.js의 Express 등
app.listen(process.env.PORT || 3000);
8. Concurrency — 프로세스 모델로 확장
수직 확장 (Scale Up): 서버 사양 올리기 → 한계 있음
수평 확장 (Scale Out): 프로세스 수 늘리기 → 12 Factor 방식
web=3 (웹 요청 처리 프로세스 3개)
worker=2 (백그라운드 작업 프로세스 2개)
scheduler=1 (스케줄러 프로세스 1개)
9. Disposability — 빠른 시작과 종료
빠른 시작: 수 초 내에 요청 처리 가능 상태
우아한 종료(Graceful Shutdown):
1. SIGTERM 수신
2. 새 요청 거부
3. 진행 중인 요청 완료 대기
4. 프로세스 종료
// Node.js 예시
process.on('SIGTERM', () => {
server.close(() => {
db.disconnect();
process.exit(0);
});
});
10. Dev/Prod Parity — 개발과 프로덕션의 차이 최소화
✗ 나쁜 예:
개발: SQLite + 로컬 파일 저장
프로덕션: PostgreSQL + S3
✓ 좋은 예:
개발: Docker Compose로 PostgreSQL + MinIO
프로덕션: RDS PostgreSQL + S3
→ 같은 종류의 서비스 사용
11. Logs — 로그를 이벤트 스트림으로 취급
✗ 나쁜 예: 앱이 직접 로그 파일 관리
logger.setOutput(new FileWriter("/var/log/app.log"));
✓ 좋은 예: stdout으로 출력, 수집은 인프라가 담당
console.log(JSON.stringify({ level: "info", msg: "요청 처리 완료" }));
→ Fluentd, Logstash 등이 수집하여 Elasticsearch로 전달
12. Admin Processes — 관리 작업도 일회성 프로세스로
DB 마이그레이션, 데이터 정리 등의 관리 작업도
앱과 동일한 환경에서 일회성 프로세스로 실행
# 좋은 예
kubectl exec -it app-pod -- python manage.py migrate
docker exec app python manage.py createsuperuser
# 나쁜 예: 프로덕션 서버에 SSH로 접속해서 직접 실행
한눈에 요약
| # | 원칙 | 핵심 |
|---|---|---|
| 1 | Codebase | 하나의 코드, 여러 배포 |
| 2 | Dependencies | 명시적 선언, 시스템 의존 금지 |
| 3 | Config | 환경 변수로 분리 |
| 4 | Backing Services | 교체 가능한 리소스 |
| 5 | Build/Release/Run | 단계 분리 |
| 6 | Processes | 무상태, 공유 없음 |
| 7 | Port Binding | 자체 포트 바인딩 |
| 8 | Concurrency | 프로세스 수로 확장 |
| 9 | Disposability | 빠른 시작, 우아한 종료 |
| 10 | Dev/Prod Parity | 환경 차이 최소화 |
| 11 | Logs | stdout 스트림 |
| 12 | Admin Processes | 일회성 프로세스 |
자주 헷갈리는 포인트
-
"12 Factor를 다 지켜야 하나" — 원칙이지 규칙이 아닙니다. 상황에 따라 타협할 수 있지만, 어기는 이유를 알고 어기는 것이 중요합니다.
-
"무상태면 상태를 어디에 저장하나" — 상태를 없애는 게 아니라, 프로세스 외부(DB, Redis, S3)로 옮기는 것입니다. 프로세스 자체는 언제든 교체 가능해야 합니다.
-
"Docker를 쓰면 자동으로 12 Factor가 되나" — 컨테이너는 도구일 뿐입니다. 컨테이너 안에서 파일에 상태를 저장하거나 설정을 하드코딩하면 여전히 문제가 됩니다.
정리
- 12 Factor App은 클라우드 환경에서 잘 동작하는 앱을 만들기 위한 12가지 원칙
- 핵심은 무상태, 환경 분리, 명시적 의존성, 로그 스트림
- 컨테이너와 쿠버네티스 환경의 기본 전제 조건
- 모든 원칙을 완벽히 지킬 필요는 없지만, 어기는 이유를 알고 판단하는 것이 중요
- Spring Boot, Next.js 같은 현대 프레임워크는 이미 많은 원칙을 기본 지원