
12-Factor App: 클라우드 시대의 생존 법칙
당신의 앱이 AWS나 Docker 환경에서 자꾸 죽는다면? Heroku 개발자들이 만든 '현대적인 앱을 위한 12가지 헌법'. 로컬호스트에서는 잘 되는데 배포만 하면 터지는 이유와 해결책.

당신의 앱이 AWS나 Docker 환경에서 자꾸 죽는다면? Heroku 개발자들이 만든 '현대적인 앱을 위한 12가지 헌법'. 로컬호스트에서는 잘 되는데 배포만 하면 터지는 이유와 해결책.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

제 첫 서비스를 AWS에 배포했을 때 일입니다.
로컬 개발 환경(내 맥북)에서는 완벽하게 돌아가던 Node.js 앱이 EC2 인스턴스에 올리자마자 크래시 났습니다.
로그를 보니 Cannot find module 'dotenv'라는 에러가 떴습니다.
"어? 분명 로컬에서는 설치했는데? 잘 돌아갔는데?"
알고 보니 저는 npm install dotenv를 로컬에서만 했고, 정작 가장 중요한 package.json의 dependencies에는 추가하지 않았던 겁니다.
로컬에선 우연히 node_modules/dotenv가 있어서 돌아갔던 것뿐이었죠. 하지만 클린 설치를 하는 배포 환경에서는 당연히 모듈이 없으니 죽어버린 겁니다.
이 사건 이후로 "아, 배포 환경과 개발 환경은 완전히 다른 세상이구나"라는 걸 뼈저리게 배웠습니다.
그때 선배가 던져준 링크 하나가 제 개발 인생을 바꿨습니다. 바로 12-Factor App 방법론입니다.
12-Factor App은 클라우드 플랫폼의 조상님 격인 Heroku(헤로쿠)의 개발자들이 만든 "클라우드 친화적인 애플리케이션을 짓기 위한 12가지 헌법"입니다. 그들은 수십만 개의 고객 애플리케이션을 호스팅하고 운영하면서 데이터를 모았습니다. "어떤 앱은 트래픽이 폭주해도 잘 버티는데, 어떤 앱은 조금만 건드려도 죽어버리더라." 그 "잘 살아남는 앱들의 공통점"을 정리한 게 바로 이 12가지 원칙입니다.
이 원칙은 2011년에 나왔지만, 지금의 Docker, Kubernetes, AWS ECS, Serverless 등 모든 현대적 인프라는 12-Factor를 전제로 설계되었습니다. 즉, 이 원칙을 모르면 "왜 쿠버네티스가 이따위로 복잡하게 생겼지?"라고 불평만 하게 됩니다. 하지만 이 원칙을 이해하면 "아, 그래서 쿠버네티스에 ConfigMap이 있고, 그래서 Pod는 맘대로 죽었다 살아나는구나"가 보입니다. 클라우드의 문법을 이해하게 되는 거죠.
12개를 전부 외울 필요는 없습니다. 실제로는 다음 5가지만 챙겨도 "기본은 된 개발자" 소리를 듣습니다.
"설정은 환경변수(Environment Variables)에 저장한다."
초창기 제 코드는 이랬습니다:
// ❌ 최악의 예: 코드 안에 박제된 비밀번호
const DB_PASSWORD = 'mySecretPassword123';
const API_KEY = 'sk-1234567890';
const supabase = createClient(url, DB_PASSWORD);
이걸 GitHub에 올렸습니다. Public Repository에 말이죠. 다행히 봇이 긁어가기 전에 친구가 발견해서 내렸지만, 만약 그때 털렸다면 AWS 요금 폭탄을 맞았을 겁니다. 더 큰 문제는, 개발 서버 DB랑 운영 서버 DB가 다를 때마다 코드를 수정해야 한다는 점이었습니다.
코드는 정적(Static)입니다. 한 번 짜면 모든 환경(Dev, Stage, Prod)에서 똑같아야 합니다. 하지만 설정(Config)은 동적(Dynamic)입니다. 개발할 땐 로컬 DB를 쓰고, 운영에선 AWS RDS를 써야 합니다.
그래서 등장한 게 환경변수(.env)입니다:
# .env (절대 Git에 올리지 마세요!)
DATABASE_URL=postgres://localhost:5432/dev_db
SUPABASE_KEY=eyJhbGc...
// ✅ 올바른 예: 환경변수 사용
require('dotenv').config();
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
이제 코드는 그대로 둔 채, 서버의 환경변수만 바꾸면 DB를 마음대로 갈아끼울 수 있습니다. 보안과 유연성 두 마리 토끼를 다 잡은 거죠.
"DB, 큐, 캐시는 URL만 바꾸면 교체 가능해야 한다."
처음에 로컬 개발할 때는 간편한 SQLite를 썼습니다. 파일 하나로 DB가 해결되니까 정말 편했죠.
그런데 서비스가 커져서 운영 환경에서는 PostgreSQL을 써야 했습니다.
문제는 제 코드가 SQLite에 강하게 결합(Coupling)되어 있었다는 겁니다.
// ❌ DB 종류에 종속된 코드
import sqlite3
conn = sqlite3.connect('dev.db')
cursor = conn.cursor()
PostgreSQL로 바꾸려면 코드 전체를 뜯어고쳐야 했습니다. "아, 처음부터 생각하고 짤 걸..."
12-Factor 앱은 모든 외부 자원(DB, S3, Redis, RabbitMQ 등)을 "URL로 접근하는 추상적인 리소스"로 취급합니다.
// ✅ URL 기반 추상화 (ORM 사용)
import os
from sqlalchemy import create_engine
// 환경변수에 따라 DB가 바뀜 (sqlite:/// 또는 postgresql://)
db_url = os.getenv('DATABASE_URL')
engine = create_engine(db_url)
이제 .env에서 DATABASE_URL 한 줄만 바꾸면 SQLite에서 PostgreSQL로, 혹은 AWS Aurora로 순식간에 이사 갈 수 있습니다.
코드 변경? "0줄"입니다.
"앱은 아무것도 기억해서는 안 된다. 기억은 DB나 Redis가 하는 것이다."
제 Express 서버 코드는 로그인 유저 정보를 메모리에 저장했습니다.
// ❌ 서버 메모리에 유저 정보 저장 (Stateful)
const sessions = {}; // 전역 변수
app.post('/login', (req, res) => {
const userId = authenticate(req.body);
sessions[userId] = { loggedIn: true }; // 메모리에 저장
res.send('로그인 성공');
});
로컬에서는 완벽했습니다. 서버가 1대니까요. 하지만 AWS ECS에 배포하고 트래픽이 늘어 오토 스케일링(Auto Scaling)이 발동하자 재앙이 닥쳤습니다. "로그인했는데 페이지 이동하니까 로그아웃돼요!"
이유는 간단합니다. 사용자가 1번 서버에서 로그인했는데, 다음 요청은 로드 밸런서가 2번 서버로 보냈거든요.
2번 서버의 sessions 변수는 텅 비어있으니 "누구세요?"가 되는 겁니다.
클라우드에서 서버는 언제든 생기고 사라집니다. 서버의 메모리는 믿을 수 없습니다. 모든 상태(State)는 외부 저장소(Redis, Memcached, DB)에 맡겨야 합니다.
// ✅ Redis에 세션 저장 (Stateless)
const redis = new Redis(process.env.REDIS_URL);
app.post('/login', async (req, res) => {
// ...
await redis.set(`session:${userId}`, JSON.stringify({ loggedIn: true }));
res.send('로그인 성공');
});
이제 서버가 100대로 늘어나도, 모든 서버가 같은 Redis를 바라보므로 문제가 없습니다. 이것이 수평 확장(Scale-out)의 기본입니다.
"로그 파일(access.log)을 관리하려고 하지 마라. 그냥 표준 출력(stdout)으로 뱉어라."
// ❌ 파일에 로그 저장
fs.appendFileSync('/var/log/myapp/error.log', errorMsg);
이 방식은 서버가 1대일 땐 tail -f로 보기 좋습니다.
하지만 서버가 10대가 되면? "어느 서버에 에러 로그가 남았지?"라며 10대 서버에 일일이 SSH로 들어가서 찾아야 합니다. 끔찍하죠.
게다가 디스크 용량이 꽉 차서 서버가 죽는 경우도 허다합니다.
앱은 그냥 단순하게 console.log만 찍으세요.
// ✅ stdout으로 출력
console.log(JSON.stringify({ level: 'info', msg: 'User login' }));
그러면 Docker나 Kubernetes가 그 출력을 낚아채서 CloudWatch, Datadog, ELK Stack 같은 중앙 로그 저장소로 보내줍니다. 우리는 멋진 대시보드에서 100대 서버의 로그를 통합해서 검색하면 됩니다. "로그 파일 관리"라는 짐을 앱에서 덜어내는 겁니다.
"빠르게 시작하고, 우아하게 종료해라."
제 서버는 켜지는 데 30초가 걸렸습니다. (온갖 데이터를 미리 캐싱하느라).
그리고 종료 신호(SIGTERM)를 무시하고 뚝 끊겼습니다.
Kubernetes에서 롤링 업데이트(Rolling Update)를 할 때마다 헬게이트가 열렸습니다. 새 버전 배포하는 데 한참 걸리고, 그 와중에 사용자 요청들은 "연결 끊김" 에러를 내뱉었죠.
클라우드 서버는 애완동물(Pet)이 아닙니다. 아프면 치료해주는 게 아니라, 아프면 총 쏴서 죽이고 새 걸로 갈아치우는 가축(Cattle)입니다. 그래서 언제든 죽고 태어날 준비가 되어 있어야 합니다.
npm install처럼 의존성을 파일에 명시하세요. "내 컴퓨터엔 깔려있는데?"는 통하지 않습니다. (Docker가 이걸 완벽하게 해줍니다).이 12가지 원칙을 읽다 보면 드는 생각이 있습니다. "어? 이거 그냥 Docker 쓰면 해결되는 거 아니야?"
맞습니다. Docker와 Kubernetes는 12-Factor App 철학을 기술적으로 구현한 도구들입니다.
Dockerfile -> Dependencies, Port Binding 해결Docker Image -> Build/Release/Run 분리 해결Docker Container -> Disposability, Stateless 강제Docker Compose -> Dev/Prod Parity 해결그러니 "12-Factor가 뭐야?"라고 묻는다면, "그냥 Docker스럽게 짜는 것"이라고 답해도 80점은 됩니다. 하지만 그 이면에 깔린 "이식성(Portability)"과 "확장성(Scalability)"의 철학을 이해한다면, 여러분은 100점짜리 클라우드 엔지니어가 될 수 있습니다.
오늘 여러분의 코드를 한번 열어보세요. 혹시 config.js에 비밀번호가 박혀있진 않은가요?