Twelve-Factor App
Twelve-Factor App
SaaS 앱 개발 방법론. 설정, 의존성, 포트 바인딩 등 12가지 원칙.
Twelve-Factor App
SaaS 앱 개발 방법론. 설정, 의존성, 포트 바인딩 등 12가지 원칙.
Twelve-Factor App은 2011년 Heroku 개발자들이 제안한 현대적인 SaaS 애플리케이션 개발 방법론입니다. 클라우드 네이티브 환경에서 배포, 확장, 유지보수가 쉬운 앱을 만들기 위한 12가지 원칙을 정의합니다. 컨테이너, 서버리스, 마이크로서비스 시대에 더욱 중요해졌습니다.
핵심 원칙: 1) Codebase - 버전 관리되는 하나의 코드베이스, 2) Dependencies - 명시적 의존성 선언, 3) Config - 환경변수로 설정 분리, 4) Backing Services - DB/캐시 등을 교체 가능한 리소스로 취급, 5) Build/Release/Run - 빌드와 실행 단계 분리, 6) Processes - 상태 없는 프로세스로 실행.
추가 원칙: 7) Port Binding - 자체 포트 바인딩으로 서비스 노출, 8) Concurrency - 프로세스 복제로 수평 확장, 9) Disposability - 빠른 시작과 graceful shutdown, 10) Dev/Prod Parity - 개발/운영 환경 일치, 11) Logs - 이벤트 스트림으로 로그 처리, 12) Admin Processes - 관리 작업을 일회성 프로세스로 실행.
이 원칙들은 Docker, Kubernetes 환경과 자연스럽게 맞습니다. 환경변수 설정(ConfigMap/Secret), 상태 없는 컨테이너, 수평 확장(HPA), 헬스체크, 로그 수집(Fluentd) 등이 모두 Twelve-Factor 원칙을 따릅니다.
// Twelve-Factor App 원칙 적용 예제
// 3. Config - 환경변수로 설정 분리
// ❌ 안티패턴: 하드코딩
const dbUrl = 'postgres://user:pass@localhost:5432/mydb';
// ✅ 환경변수 사용
const config = {
database: {
url: process.env.DATABASE_URL,
pool: parseInt(process.env.DB_POOL_SIZE || '10')
},
redis: {
url: process.env.REDIS_URL
},
port: parseInt(process.env.PORT || '3000'),
logLevel: process.env.LOG_LEVEL || 'info'
};
// 필수 환경변수 검증 (시작 시점에)
const required = ['DATABASE_URL', 'REDIS_URL', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required env var: ${key}`);
process.exit(1);
}
}
// 4. Backing Services - 교체 가능한 리소스
// 환경변수만 바꾸면 로컬 Redis → AWS ElastiCache 전환 가능
const redis = new Redis(process.env.REDIS_URL);
// 6. Processes - Stateless 프로세스
// ❌ 메모리에 세션 저장 (스케일링 불가)
const sessions = new Map();
// ✅ 외부 저장소(Redis)에 세션 저장
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET
}));
// 9. Disposability - Graceful Shutdown
async function gracefulShutdown(signal: string) {
console.log(`Received ${signal}, shutting down gracefully...`);
// 1. 새 요청 거부
server.close();
// 2. 진행 중인 작업 완료 대기
await Promise.all([
redis.quit(),
prisma.$disconnect(),
closeDbPool()
]);
console.log('Cleanup complete, exiting');
process.exit(0);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 11. Logs - stdout으로 출력 (수집은 인프라가 담당)
// ❌ 파일에 직접 로깅
// fs.appendFileSync('/var/log/app.log', message);
// ✅ stdout으로 구조화된 로그
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// JSON 형식으로 stdout 출력 → Fluentd/Datadog이 수집
});
logger.info({ userId, action: 'login' }, 'User logged in');
// Kubernetes 배포 예시
/*
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: log-level
livenessProbe: # 9. Disposability
httpGet:
path: /health
port: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000
*/
클라우드 마이그레이션에서:
"온프레미스 앱을 K8s로 옮기려는데 문제가 많아요." - "Twelve-Factor 체크해보세요. 설정이 파일에 하드코딩되어 있거나, 로컬 파일시스템에 상태 저장하면 컨테이너화가 어려워요. 환경변수, 외부 스토리지로 바꿔야 합니다."
기술 면접에서:
"Twelve-Factor에서 가장 중요한 원칙은?" - "저는 Config와 Stateless Processes를 꼽겠어요. 환경변수로 설정을 분리하면 같은 코드로 어디든 배포 가능하고, 상태 없는 프로세스는 수평 확장과 장애 복구의 기본이 됩니다."
장애 대응에서:
"배포 후 Pod이 계속 재시작돼요." - "Graceful shutdown 구현했나요? SIGTERM 받으면 진행 중인 요청 마무리하고 DB 커넥션 정리해야 해요. 안 그러면 readiness 실패하고 무한 재시작될 수 있어요."