coredot.today
12-Factor App: 클라우드 시대에 '잘 만든 앱'의 12가지 원칙
블로그로 돌아가기
12-Factor클라우드네이티브SaaSDevOps아키텍처Heroku

12-Factor App: 클라우드 시대에 '잘 만든 앱'의 12가지 원칙

로컬에서 잘 돌아가던 앱이 클라우드에만 올리면 왜 망가질까? 2011년 Heroku 엔지니어들이 수천 개의 앱 배포 경험에서 뽑아낸 12가지 원칙을 한국어로, 비유와 안티패턴 중심으로 풀어본다. Docker, Kubernetes, 서버리스 시대에도 여전히 유효한 클라우드 네이티브 앱의 기본기.

코어닷투데이2026-04-0150

들어가며: "왜 내 앱은 로컬에서는 되는데 클라우드에 올리면 안 될까?"

내 노트북에서는 완벽하게 돌아간다. 테스트도 통과하고, API도 잘 응답한다. 그런데 AWS에 올리면 환경변수 에러, 파일 업로드 소실, 로그 실종. 이것은 능력의 문제가 아니라 설계 원칙의 문제다.

2011년, Heroku의 엔지니어들이 수천 개의 앱 배포 경험에서 깨달았다. 클라우드에서 잘 돌아가는 앱에는 공통된 패턴이 있고, 망가지는 앱에도 공통된 안티패턴이 있다. 그것을 12가지 원칙으로 정리한 것이 12-Factor App이다.

💡
12-Factor App은 특정 언어나 프레임워크에 종속되지 않는 방법론(methodology)이다. Python이든, Java든, Node.js든 상관없다. 클라우드에서 동작하는 모든 소프트웨어에 적용할 수 있는 보편적 원칙이다.

이 글에서는 12가지 원칙을 한국어 비유, 안티패턴, 코드 예시와 함께 풀어본다.


1. 탄생 배경: Heroku 엔지니어들의 수천 번의 실패에서

만든 사람: Adam Wiggins

저자는 Adam Wiggins — Heroku 공동 창립자이자, 수천 개의 SaaS 앱 배포를 직접 지켜본 엔지니어다.

2011 최초 발표 연도
1,000+ 관찰한 앱 수 Heroku 플랫폼 배포 기준
12 핵심 원칙
15+ 지원 프로그래밍 언어 언어 무관 방법론

왜 이것이 필요했나

2010년대 초반 — 온프레미스에서 클라우드로, 모놀리식에서 마이크로서비스로 전환되는 과도기. Wiggins와 동료들은 반복되는 실패 패턴을 발견했다.

⚠️
문제
같은 코드인데 환경마다 다르게 동작한다. 개발자는 "내 컴퓨터에서는 돼요"라고 말한다.
🔧
원인 분석
설정이 코드에 하드코딩, 의존성이 암묵적, 빌드와 실행이 분리되지 않음, 로그가 파일에 저장 등 구조적 문제.
해결
환경 독립적이고, 이식 가능하며, 자동화 친화적인 앱을 만드는 12가지 원칙을 체계화.

12-Factor가 해결하려는 핵심 문제 세 가지: (1) 환경 간 차이 최소화, (2) 이식성 — 어디서든 실행, (3) 확장성 — 수평 확장이 자연스러운 구조.


12가지 원칙을 상징하는 신전의 12개 기둥과 그 앞에 선 개발자

2. Factor I ~ IV: 기반을 잡는 네 가지 원칙

Factor I: Codebase (코드베이스)

하나의 코드베이스, 여러 번의 배포

🏠
비유: 하나의 설계도(코드베이스)로 서울 지점, 부산 지점, 제주 지점(배포 환경)을 운영한다. 설계도는 하나인데, 각 지점의 인테리어(설정)만 다르다.

원칙: 앱은 하나의 Git 저장소에서 관리. 여러 환경에 배포되지만, 코드는 동일한 저장소에서 나온다.

Git Repository (1개)
dev 배포
staging 배포
production 배포

안티패턴: 환경별로 코드 브랜치를 따로 관리(main-dev, main-prod), 하나의 앱을 여러 저장소에 복사-붙여넣기로 동기화, 서로 다른 앱을 한 저장소에 합침.


Factor II: Dependencies (의존성)

명시적으로 선언하고, 격리한다

📦
비유: 요리 레시피에 "소금 적당히"라고 적으면 안 된다. "천일염 5g"처럼 정확히 적어야 누가 만들어도 같은 맛이 난다. 의존성도 마찬가지다.

원칙: 모든 라이브러리/도구는 명시적 선언 파일에 기록. 시스템 전역 패키지에 의존하면 안 된다.

언어별 의존성 선언 파일
Python
선언: requirements.txt 또는 pyproject.toml
격리: virtualenv 또는 venv
Node.js
선언: package.json + package-lock.json
격리: node_modules (프로젝트 로컬)
Java
선언: pom.xml (Maven) 또는 build.gradle (Gradle)
격리: 빌드 도구의 로컬 캐시

안티패턴: sudo apt-get install imagemagick으로 서버에 직접 설치하고, 코드에서는 "깔려 있겠지" 가정. 새 서버 배포 시마다 반복되는 "아, 설치 안 했네" 문제.

올바른 방법:

hljs language-bash
# Dockerfile로 시스템 의존성까지 명시
FROM python:3.12-slim
RUN apt-get update && apt-get install -y libmagickwand-dev
COPY requirements.txt .
RUN pip install -r requirements.txt

환경변수를 앱 외부의 설정 다이얼로 조절하는 모습

Factor III: Config (설정)

설정은 환경변수에 저장한다

🔑
비유: 자동차의 내비게이션 목적지는 차에 용접하지 않는다. 운전자가 갈 때마다 입력한다. 마찬가지로 데이터베이스 주소, API 키 같은 설정은 코드에 박아 넣지 않고, 환경변수로 주입한다.

원칙: DB 접속 정보, API 키, 외부 서비스 URL 등은 코드 밖, 환경변수(environment variable)에 저장한다.

안티패턴 vs 올바른 방법:

  • DB_HOST = "192.168.1.100" 하드코딩 → os.environ["DB_HOST"] 환경변수에서 읽기
  • config/production.yml 환경별 파일 → DATABASE_URL 환경변수 하나로 통일
  • API 키를 Git에 커밋 → .env.gitignore에 등록, 프로덕션에서는 시크릿 매니저
  • if env == "prod" 코드 분기 → 환경변수 값만 바꾸면 동작이 달라지도록 설계

리트머스 테스트: 지금 이 순간, 코드를 오픈소스로 공개해도 보안 문제가 없는가? "아니요"라면 코드에 비밀이 하드코딩되어 있다.


Factor IV: Backing Services (백엔드 서비스)

백엔드 서비스를 연결된 리소스로 취급한다

🔌
비유: 집의 전기를 한전에서 받든, 태양광 패널에서 받든 콘센트만 꽂으면 된다. 앱은 MySQL이든 PostgreSQL이든, 로컬이든 AWS RDS든, URL만 바꾸면 동작해야 한다.

원칙: DB, 메시지 큐, 캐시, SMTP 등 모든 외부 서비스는 연결된 리소스(attached resource)로 취급한다. URL만 바꾸면 다른 서비스로 교체할 수 있어야 한다.

Backing Services = 교체 가능한 리소스
핵심 비즈니스 로직
PostgreSQL DATABASE_URL
Redis REDIS_URL
S3 S3_BUCKET_URL
SendGrid SMTP_URL

안티패턴: db = mysql.connector.connect(host="localhost", user="root", password="1234") — 환경이 바뀌면 코드를 수정해야 한다.

올바른 방법: engine = create_engine(os.environ["DATABASE_URL"]) — URL만 바꾸면 로컬 MySQL에서 AWS RDS로 즉시 전환 가능. 프로덕션 DB 장애 시 코드 변경 없이 백업 DB로 전환할 수 있다면 이 원칙을 잘 따르고 있다.


3. Factor V ~ VIII: 빌드와 실행을 분리하고, 확장하기

Factor V: Build, Release, Run (빌드, 릴리스, 실행)

빌드 단계와 실행 단계를 엄격히 분리한다

🏭
비유: 공장에서 제품을 만드는 과정은 세 단계다. (1) 제조(Build) — 원자재로 제품 생산, (2) 포장(Release) — 라벨 붙이고 출하 준비, (3) 배송(Run) — 매장에서 판매. 매장에서 제품을 다시 조립하지 않는다.
BUILD 코드 + 의존성 → 실행 가능한 번들(아티팩트) 생성. npm run build, docker build
RELEASE 빌드 아티팩트 + 환경별 설정(Config) 결합. 고유한 릴리스 ID 부여 (예: v2.3.1-20260401)
RUN 릴리스를 실행 환경에서 프로세스로 구동. 이 단계에서 코드를 수정하면 안 된다

안티패턴: 프로덕션 서버에 SSH로 접속해서 vi app.py로 코드를 직접 수정 (Run 단계에서 Build를 하는 꼴). 배포할 때마다 서버에서 npm install 실행. 릴리스에 고유 버전이 없어서 롤백 불가.


Factor VI: Processes (프로세스)

앱을 하나 이상의 무상태(stateless) 프로세스로 실행한다

💭
비유: 편의점 직원은 교대한다. 낮 근무자가 퇴근해도 밤 근무자가 같은 일을 할 수 있어야 한다. "아까 그 손님이 뭘 샀는지"를 직원 머릿속(메모리)에만 저장하면 안 된다. 포스(POS) 시스템(외부 저장소)에 기록해야 한다.

원칙: 각 프로세스는 무상태(stateless). 영구 데이터는 DB나 캐시 같은 외부 서비스에 저장한다.

안티패턴: 세션을 서버 메모리에 저장 — 두 번째 서버로 요청이 가면 장바구니가 비어 있다.

올바른 방법: Redis에 세션 저장 — 어떤 서버가 처리해도 같은 결과. 이 원칙은 수평 확장의 전제 조건이다.


Factor VII: Port Binding (포트 바인딩)

포트 바인딩을 통해 서비스를 공개한다

🚪
비유: 가게는 문(포트)이 있어야 손님이 들어온다. 12-Factor 앱은 Apache나 Nginx 같은 외부 웹 서버에 의존하지 않고, 스스로 HTTP 포트를 열고 요청을 처리한다.

원칙: Apache/Tomcat에 앱을 주입하는 방식이 아니라, 앱 자체가 HTTP 포트를 열고 서비스를 제공한다.

hljs language-javascript
// Express.js — 앱 자체가 HTTP 서버
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

이 원칙 덕분에 앱이 다른 앱의 백엔드 서비스가 될 수 있다. 마이크로서비스 아키텍처의 기반 개념이다.


Factor VIII: Concurrency (동시성)

프로세스 모델을 통해 확장한다

📈
비유: 식당에 손님이 몰리면 셰프 한 명의 요리 속도를 올리는 것보다(수직 확장), 주방 보조를 더 고용하는 게(수평 확장) 효율적이다. 웹 요청은 웹 프로세스를, 백그라운드 작업은 워커 프로세스를 늘린다.

원칙: 서버 사양을 올리는 것(수직 확장)이 아니라 프로세스 수를 늘린다(수평 확장). 작업 유형별로 분류하고 독립 확장한다.

프로세스 유형별 수평 확장
web (x4) HTTP 요청 처리
worker (x2) 백그라운드 작업
scheduler (x1) 크론 작업
hljs language-yaml
# Kubernetes — 프로세스 유형별 독립 스케일링
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 4      # 트래픽에 따라 조절
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
spec:
  replicas: 2      # 큐 크기에 따라 조절

안티패턴: 하나의 프로세스에서 웹 요청 + 백그라운드 작업 + 스케줄러를 모두 처리. "CPU를 128코어로 올리자"는 수직 확장의 한계에 도달.


개발 환경과 프로덕션 환경이 동일한 쌍둥이 건물로 표현된 Dev/Prod 패리티

4. Factor IX ~ XII: 안정성과 운영을 위한 원칙

Factor IX: Disposability (폐기 가능성)

빠른 시작과 우아한 종료로 견고성을 극대화한다

비유: 택시 기사는 언제든 교대할 수 있다. 승객을 태우고 있으면 목적지까지 가고(graceful shutdown), 대기 중이면 바로 퇴근한다(fast startup). 클라우드 프로세스도 마찬가지다.

원칙: 프로세스는 빠르게 시작, 우아하게 종료. 언제든 새 인스턴스를 띄우거나 내릴 수 있어야 한다.

SIGTERM 종료 신호를 받는다
DRAIN 새 요청 수신을 중단한다. 처리 중인 요청은 완료까지 대기한다
CLEANUP 열려 있는 DB 연결, 파일 핸들 등을 정리한다
EXIT 프로세스가 종료된다. 데이터 손실 없음
hljs language-javascript
// Node.js graceful shutdown
process.on('SIGTERM', async () => {
  server.close();           // 1. 새 요청 받지 않기
  await flushQueue();       // 2. 진행 중 작업 완료
  await db.end();           // 3. DB 연결 종료
  process.exit(0);
});

쿠버네티스는 스케일링, 업데이트, 장애 복구 시 프로세스를 수시로 종료하고 시작한다. 시작은 수 초 이내, 종료는 진행 중 작업 완료 보장이 목표다.


Factor X: Dev/Prod Parity (개발/프로덕션 환경 일치)

개발, 스테이징, 프로덕션 환경을 최대한 비슷하게 유지한다

🌍
비유: 비행기 조종사는 실제 비행 전에 시뮬레이터로 연습한다. 시뮬레이터가 실제 조종석과 다르면 연습의 의미가 없다. 개발 환경과 프로덕션 환경도 마찬가지다.

원칙: 세 가지 간극을 최소화한다. 시간 간극(수 주 뒤 배포 → 수 시간 내 배포), 인적 간극(개발자/운영 분리 → 개발자가 직접 배포), 도구 간극(dev SQLite/prod PostgreSQL → 모든 환경에서 동일 도구).

특히 도구 간극이 위험하다. Docker Compose면 로컬에서도 프로덕션과 동일한 DB, 캐시, 큐를 띄울 수 있다.


Factor XI: Logs (로그)

로그를 이벤트 스트림으로 취급한다

📜
비유: 앱은 일기를 쓰되, 일기장을 직접 보관하지 않는다. 그냥 큰 소리로 말하면(stdout) 누군가(로그 수집기)가 받아 적고, 분류하고, 보관한다.

원칙: 로그는 그냥 stdout(표준 출력)으로 내보내면 된다. 저장, 라우팅, 보관은 실행 환경이 처리한다.

앱 → stdout
로그 수집기 (Fluentd, Logstash)
Elasticsearch
CloudWatch
Datadog

안티패턴: logging.FileHandler('/var/log/myapp/app.log') — 컨테이너가 죽으면 로그도 사라진다.

올바른 방법: logging.basicConfig(stream=sys.stdout) — 컨테이너(Docker, K8s)는 stdout을 자동 수집하고, 개발 중에는 터미널에서 바로 볼 수 있다.


Factor XII: Admin Processes (관리 프로세스)

관리/유지보수 작업을 일회성 프로세스로 실행한다

🧹
비유: 식당 청소는 매일 영업 후에 한다. 그런데 청소부가 주방 설비를 직접 만져서 설정을 바꾸면 안 된다. 같은 주방(환경)에서, 정해진 도구로, 정해진 절차대로 청소해야 한다.

원칙: DB 마이그레이션, 일회성 스크립트 등 관리 작업은 앱과 동일한 환경, 동일한 코드베이스와 설정으로 실행한다.

hljs language-bash
# 앱과 동일한 환경에서 관리 명령 실행
kubectl exec -it deploy/myapp -- python manage.py migrate  # K8s
heroku run python manage.py createsuperuser                 # Heroku

안티패턴: 프로덕션 DB에 직접 접속해서 SQL 수동 실행. 관리 스크립트를 별도 저장소에서 관리(코드베이스 불일치).


5. 각 Factor가 풀어주는 구체적 고통

현실의 고통 세 가지를 12-Factor로 해결해 보자.

🔥
"서버가 죽으니 세션이 다 날아갔어요"
세션을 서버 메모리에 저장. 서버 1대가 죽자 로그인 세션 전부 소실.
🔧
Factor VI + IX
무상태 프로세스 + 빠른 복구. 세션을 Redis로 이동하고, K8s가 자동으로 새 파드를 띄운다.
결과
서버 3대 중 1대가 죽어도 사용자는 아무것도 느끼지 못한다.
🐛
"프로덕션에서만 버그가 나요"
개발에서 SQLite, 프로덕션에서 PostgreSQL. 특정 SQL이 SQLite에서만 동작.
🔧
Factor X
Docker Compose로 로컬에서도 PostgreSQL 사용. 동일 DB 엔진, 동일 동작.
결과
"내 컴퓨터에서는 돼요" 문제가 사라진다.
🚨
"롤백할 수가 없어요"
프로덕션에 SSH로 코드를 직접 수정. "이전 상태"가 뭔지 모른다.
🔧
Factor I + V
모든 변경은 Git에, 모든 릴리스에 고유 버전. 이전 릴리스로 즉시 롤백.
결과
kubectl rollout undo 한 줄이면 30초 내 복구.

6. 12-Factor와 현대 기술의 관계

이후 등장한 기술들은 12-Factor를 더 쉽게 따를 수 있게 만들어주었다.

Docker, Kubernetes, 서버리스

Docker는 12-Factor의 최고 파트너다. Dockerfile이 의존성을 명시하고(II), docker build/docker run이 Build/Release/Run을 자연스럽게 분리하며(V), 동일한 이미지로 Dev/Prod Parity를 보장하고(X), stdout을 자동 수집한다(XI).

Kubernetes는 12-Factor의 실행 플랫폼이다. ConfigMap/Secret으로 설정을 주입하고(III), HPA로 자동 수평 확장하며(VIII), Rolling update로 무중단 배포하고(IX), Job/CronJob으로 관리 작업을 자동화한다(XII).

서버리스(AWS Lambda, Vercel Functions)는 12-Factor를 강제한다.

서버리스가 자동으로 강제하는 Factor 수준
VI. 무상태
강제됨
VII. 포트바인딩
강제됨
IX. 폐기가능
강제됨
VIII. 동시성
강제됨
XI. 로그
거의 강제
III. 설정
권장됨

12-Factor를 이해하면 서버리스가 왜 그렇게 설계되었는지 자연스럽게 이해된다.


7. Beyond 12-Factor: 2011년 이후 달라진 것

12가지 원칙은 여전히 유효하지만, 15년이 지나며 몇 가지 추가 고려사항이 생겼다.

Kevin Hoffman의 "Beyond the Twelve-Factor App" (15 Factors)

2016년, Pivotal의 Kevin Hoffman은 3가지 원칙을 추가했다: XIII. API First (설계 단계부터 API 계약 정의), XIV. Telemetry (로그뿐 아니라 메트릭+트레이싱 수집), XV. Auth/Security (제로 트러스트 환경에서 인증을 앱 설계에 포함).

2011년 vs 2026년: 무엇이 달라졌나

2011년에는 Docker(2013)도, 서버리스도 없었다. 2026년에는 Docker+K8s가 표준, OpenTelemetry로 관찰가능성 통합, ArgoCD로 GitOps, Terraform으로 IaC까지 갖추었다.

💡
핵심: 12-Factor의 원칙 자체는 변하지 않았다. 구현 도구가 비약적으로 발전했을 뿐이다. "환경변수로 설정을 관리하라"는 원칙이, K8s Secret + HashiCorp Vault로 더 안전하게 실현된다.

8. 실전 체크리스트: 내 앱은 몇 점?

각 항목에 "예/아니오"로 답해 보자.

12-Factor 자가 진단
기반 (I~IV)
☐ 앱 = Git 저장소 1개, 모든 환경 배포가 같은 코드에서 나온다
☐ 의존성이 선언 파일에 명시, npm install 한 줄로 환경 구성 완료
☐ DB 접속 정보/API 키가 환경변수에 있고, 코드에 하드코딩 없음
☐ 환경변수만 바꾸면 DB/캐시/큐 교체 가능
빌드와 실행 (V~VIII)
☐ 프로덕션에서 SSH로 코드 수정 안 함, 모든 릴리스에 고유 버전 존재
☐ 세션/캐시가 서버 메모리나 로컬 파일에 저장되지 않음
☐ 앱이 자체 포트 바인딩, 프로세스 수평 확장 가능
운영 (IX~XII)
☐ 수 초 내 시작, SIGTERM에 우아한 종료
☐ 개발/프로덕션에서 동일한 DB/캐시 사용
☐ 로그가 stdout, 관리 작업이 앱과 같은 환경에서 실행
점수 해석
10개 이상
클라우드 네이티브
7~9개
양호, 보완 필요
4~6개
리팩터링 권장
3개 이하
재설계 필요

처음부터 다 지키려 하지 말자. Factor III(Config), Factor VI(Processes), Factor X(Dev/Prod Parity)가 가장 즉각적인 효과를 준다.


9. 적용 로드맵: 어디서부터?

1~2주 즉시 적용 — Factor III: 하드코딩 설정을 환경변수로 이동. Factor XI: 파일 로깅을 stdout으로 전환.
2~4주 컨테이너화 — Factor II: Dockerfile로 의존성 명시. Factor V: CI/CD 구축. Factor X: Docker Compose로 Dev/Prod 통일.
2~4주 무상태화 — Factor VI: 세션/파일을 Redis/S3로 이동. Factor IV: 외부 서비스를 URL 기반으로 교체 가능하게.
4~8주 운영 고도화 — Factor VIII: 프로세스 유형 분리 + 오토스케일링. Factor IX: graceful shutdown. Factor XII: 관리 작업 자동화.

10. 자주 하는 오해들

"마이크로서비스 전용?" — 아니다. 모놀리식에도 적용된다. 마이크로서비스 전환 전에 먼저 적용하는 것이 올바른 순서.

"다 지켜야?" — 가이드라인이지 법칙이 아니다. Factor III(Config)과 II(Dependencies)는 어떤 프로젝트든 지키는 게 좋다.

"성능 저하?" — 오히려 수평 확장이 쉬워진다. Redis 세션 저장의 네트워크 왕복은 체감 불가 수준.

"2011년이라 구식?" — HTTP가 1991년에 나왔다고 구식이라 하지 않듯, 원칙은 시대를 초월한다.


마치며: "잘 만든 앱"의 기준

한 문장으로 요약하면 이렇다:

코드와 설정을 분리하고, 상태를 외부에 저장하고, 모든 것을 자동화하라.

이것만 기억해도 "로컬에서는 되는데 클라우드에서는 안 되는" 문제의 80%는 예방할 수 있다.

12-Factor App
이식 가능하고, 확장 가능하고, 운영 가능한 앱
Docker/K8s 친화적
서버리스 호환
CI/CD 자동화

체크리스트에서 가장 점수가 낮은 항목 하나부터 개선하자. 그것이 클라우드 네이티브로 가는 첫걸음이다.


더 읽어볼 자료: