클라우드에 배포하면 "그냥 되는" 줄 알았는데, 왜 어떤 앱은 잘 돌아가고 어떤 앱은 문제투성이일까요?

이게 뭔가요?

12 Factor App은 Heroku의 공동 창업자가 정리한, 클라우드 환경에서 잘 동작하는 SaaS 애플리케이션을 만들기 위한 12가지 설계 원칙입니다.

2012년에 발표됐지만, 컨테이너와 쿠버네티스 시대인 지금도 거의 그대로 적용됩니다.

왜 필요한가요?

  • 로컬에서 잘 돌아가는 앱이 클라우드에서 문제를 일으키는 이유를 체계적으로 설명
  • 수평 확장, 무중단 배포, 환경 간 이동(dev → staging → prod)을 쉽게 만드는 패턴
  • 컨테이너(Docker), 오케스트레이션(K8s) 환경의 기본 전제 조건

12가지 원칙

1. Codebase — 하나의 코드베이스, 여러 배포

PLAINTEXT
하나의 Git 저장소 → 여러 환경에 배포

┌─────────────┐
│  Git Repo   │──→ dev 환경
│  (하나)     │──→ staging 환경
└─────────────┘──→ production 환경

✗ 환경별로 코드가 다르면 안 됨
✗ 여러 앱이 한 저장소에 있으면 분리 필요

2. Dependencies — 명시적 의존성 선언

PLAINTEXT
✓ 좋은 예:
  package.json, build.gradle, requirements.txt에 모든 의존성 명시
  → npm install, pip install -r requirements.txt로 재현 가능

✗ 나쁜 예:
  "서버에 curl이 설치되어 있을 거야"라고 가정
  → 시스템 의존성도 Dockerfile에 명시해야 함

3. Config — 환경 변수로 설정 분리

YAML
# ✗ 코드에 하드코딩
db_url = "jdbc:mysql://prod-db:3306/myapp"

# ✓ 환경 변수로 분리
db_url = os.environ['DATABASE_URL']

환경별로 달라지는 값(DB 주소, API 키, 포트)은 코드가 아닌 환경 변수에 둡니다. 코드 변경 없이 설정만 바꿔서 배포할 수 있어야 합니다.

4. Backing Services — 부착된 리소스로 취급

PLAINTEXT
DB, 메시지 큐, 캐시, 메일 서비스 등을 교체 가능한 리소스로 취급

로컬 MySQL → AWS RDS로 교체할 때
코드 변경 없이 DATABASE_URL 환경 변수만 변경

DATABASE_URL=mysql://localhost:3306/myapp    (로컬)
DATABASE_URL=mysql://rds.amazonaws.com/myapp (프로덕션)

5. Build, Release, Run — 빌드와 실행의 엄격한 분리

PLAINTEXT
Build:   코드 + 의존성 → 실행 가능한 아티팩트 (JAR, Docker 이미지)
Release: 아티팩트 + 설정 → 릴리스 (버전 태깅)
Run:     릴리스 실행

✗ 운영 서버에서 직접 코드 수정하면 안 됨
✓ 항상 빌드 → 릴리스 → 실행 순서를 지켜야 함

6. Processes — 무상태 프로세스

PLAINTEXT
✗ 나쁜 예: 파일 시스템에 세션 저장
  // 서버가 여러 대면 세션이 공유되지 않음
  app.use(session({ store: new FileStore() }));

✓ 좋은 예: 외부 저장소에 상태 저장
  // Redis에 세션 저장 → 어떤 서버가 처리해도 동일
  app.use(session({ store: new RedisStore() }));

프로세스는 언제든 죽고 다시 시작될 수 있다고 가정합니다. 로컬 파일이나 메모리에 저장한 상태는 사라집니다.

7. Port Binding — 포트 바인딩으로 서비스 노출

PLAINTEXT
앱 자체가 HTTP 서버 역할 (별도 웹 서버 불필요)
→ Spring Boot의 내장 Tomcat, Node.js의 Express 등

app.listen(process.env.PORT || 3000);

8. Concurrency — 프로세스 모델로 확장

PLAINTEXT
수직 확장 (Scale Up): 서버 사양 올리기 → 한계 있음
수평 확장 (Scale Out): 프로세스 수 늘리기 → 12 Factor 방식

web=3       (웹 요청 처리 프로세스 3개)
worker=2    (백그라운드 작업 프로세스 2개)
scheduler=1 (스케줄러 프로세스 1개)

9. Disposability — 빠른 시작과 종료

PLAINTEXT
빠른 시작: 수 초 내에 요청 처리 가능 상태
우아한 종료(Graceful Shutdown):
  1. SIGTERM 수신
  2. 새 요청 거부
  3. 진행 중인 요청 완료 대기
  4. 프로세스 종료

// Node.js 예시
process.on('SIGTERM', () => {
  server.close(() => {
    db.disconnect();
    process.exit(0);
  });
});

10. Dev/Prod Parity — 개발과 프로덕션의 차이 최소화

PLAINTEXT
✗ 나쁜 예:
  개발: SQLite + 로컬 파일 저장
  프로덕션: PostgreSQL + S3

✓ 좋은 예:
  개발: Docker Compose로 PostgreSQL + MinIO
  프로덕션: RDS PostgreSQL + S3
  → 같은 종류의 서비스 사용

11. Logs — 로그를 이벤트 스트림으로 취급

PLAINTEXT
✗ 나쁜 예: 앱이 직접 로그 파일 관리
  logger.setOutput(new FileWriter("/var/log/app.log"));

✓ 좋은 예: stdout으로 출력, 수집은 인프라가 담당
  console.log(JSON.stringify({ level: "info", msg: "요청 처리 완료" }));
  → Fluentd, Logstash 등이 수집하여 Elasticsearch로 전달

12. Admin Processes — 관리 작업도 일회성 프로세스로

PLAINTEXT
DB 마이그레이션, 데이터 정리 등의 관리 작업도
앱과 동일한 환경에서 일회성 프로세스로 실행

# 좋은 예
kubectl exec -it app-pod -- python manage.py migrate
docker exec app python manage.py createsuperuser

# 나쁜 예: 프로덕션 서버에 SSH로 접속해서 직접 실행

한눈에 요약

#원칙핵심
1Codebase하나의 코드, 여러 배포
2Dependencies명시적 선언, 시스템 의존 금지
3Config환경 변수로 분리
4Backing Services교체 가능한 리소스
5Build/Release/Run단계 분리
6Processes무상태, 공유 없음
7Port Binding자체 포트 바인딩
8Concurrency프로세스 수로 확장
9Disposability빠른 시작, 우아한 종료
10Dev/Prod Parity환경 차이 최소화
11Logsstdout 스트림
12Admin Processes일회성 프로세스

자주 헷갈리는 포인트

  1. "12 Factor를 다 지켜야 하나" — 원칙이지 규칙이 아닙니다. 상황에 따라 타협할 수 있지만, 어기는 이유를 알고 어기는 것이 중요합니다.

  2. "무상태면 상태를 어디에 저장하나" — 상태를 없애는 게 아니라, 프로세스 외부(DB, Redis, S3)로 옮기는 것입니다. 프로세스 자체는 언제든 교체 가능해야 합니다.

  3. "Docker를 쓰면 자동으로 12 Factor가 되나" — 컨테이너는 도구일 뿐입니다. 컨테이너 안에서 파일에 상태를 저장하거나 설정을 하드코딩하면 여전히 문제가 됩니다.

정리

  • 12 Factor App은 클라우드 환경에서 잘 동작하는 앱을 만들기 위한 12가지 원칙
  • 핵심은 무상태, 환경 분리, 명시적 의존성, 로그 스트림
  • 컨테이너와 쿠버네티스 환경의 기본 전제 조건
  • 모든 원칙을 완벽히 지킬 필요는 없지만, 어기는 이유를 알고 판단하는 것이 중요
  • Spring Boot, Next.js 같은 현대 프레임워크는 이미 많은 원칙을 기본 지원
댓글 로딩 중...