infra
Platform

모듈 맵

[SW Eng] 12-Factor App — 환경에 흔들리지 않는 앱의 원칙

0 / 38 완료

펼치기
0 / 38 완료0%

PM·SRE를 위한 소프트웨어 엔지니어링 · 20 / 38

[SW Eng] 12-Factor App — 환경에 흔들리지 않는 앱의 원칙

설정 분리·무상태·로그 스트림·일회성 프로세스 등 클라우드 네이티브 앱의 12가지 원칙을 PM·인프라 관점에서 핵심만 정리합니다

🚨INCIDENT ALERT
HIGH

새 서비스를 K8s에 올렸더니 문제가 줄줄이 터집니다. 인스턴스를 2개로 늘렸더니 로그인이 자꾸 풀립니다(세션이 한 인스턴스에만). 설정을 바꾸려면 코드를 다시 빌드해야 하고, 로그는 컨테이너 안 파일에 쌓여 사라집니다. 옆 팀의 서비스는 같은 환경에서 인스턴스를 10개로 늘려도 멀쩡하고, 설정은 환경변수로 즉시 바뀝니다. 차이는 '12-Factor 원칙'을 따랐는가입니다. 이 원칙은 '클라우드·컨테이너에서 잘 굴러가는 앱'의 체크리스트입니다.

이번 챕터에서 배울 것
  • 1설정을 코드에서 분리해 환경에 두는 원칙과 그 이유를 설명할 수 있다
  • 2무상태 프로세스가 수평 확장·무중단 배포를 가능케 함을 설명할 수 있다
  • 3로그를 stdout 스트림으로 흘리는 원칙이 컨테이너와 맞는 이유를 설명할 수 있다
  • 412-Factor를 새 서비스의 클라우드 준비도 체크리스트로 활용할 수 있다

핵심 원칙 — 환경 의존을 끊는다

💡개념

12-Factor 중 인프라가 가장 챙기는 4가지

12-Factor는 클라우드 네이티브 앱의 12가지 원칙입니다. PM·인프라에게 가장 중요한 핵심부터 봅니다.

원칙한 줄안 지키면중요도
Config(설정)환경마다 다른 값은 코드가 아니라 환경(env)에환경별 빌드·비밀 유출 → 용어사전★★★
Stateless(무상태)프로세스는 로컬에 상태를 안 둠(외부 저장소로)수평 확장 불가·세션 풀림★★★
Logs(로그)stdout 스트림으로 흘리고 수집은 플랫폼이컨테이너에서 로그 소실 → 용어사전★★
Disposability(일회성)빠른 기동·우아한 종료(graceful shutdown)배포·스케일 시 요청 유실★★
Backing servicesDB·캐시·큐를 교체 가능한 '연결된 자원'으로환경 이전 어려움★★
Build/Release/Run빌드·릴리스·실행 단계 분리롤백·추적 어려움 → CI/CD 파이프라인★★

나머지(Codebase, Dependencies, Processes, Port binding, Concurrency, Dev/prod parity)도 같은 정신 — "환경에 흔들리지 않고, 교체·확장·재현이 쉬운 앱" 을 지향합니다.

12-Factor App 12가지 원칙 전체 맵 — 인프라 핵심 4가지 강조확대

위 그림처럼 12개 원칙 중 설정·프로세스·일회성·로그 4가지(★)가 클라우드·컨테이너 환경에서 특히 중요합니다.

무상태 — 확장의 전제

💡개념

상태를 밖으로 빼야 인스턴스를 자유롭게 늘린다

오프닝의 "인스턴스 늘렸더니 로그인 풀림"은 상태(세션)를 프로세스 로컬에 둔 탓입니다.

TEXT
상태를 로컬에 둠(나쁨):
  요청1 → 인스턴스A (세션 저장)
  요청2 → 인스턴스B (A의 세션 모름!) → 로그인 풀림
  → 어쩔 수 없이 sticky session(같은 사람=같은 인스턴스) 강제 → 확장 경직

무상태(좋음):
  세션을 Redis 등 외부에 저장
  요청1 → 인스턴스A → Redis
  요청2 → 인스턴스B → Redis(같은 세션 조회) → OK
  → 어느 인스턴스가 받든 동일 → 자유롭게 scale-out, 무중단 배포

무상태는 모놀리식 vs 마이크로서비스의 확장성과 릴리스 전략의 롤링/블루그린 배포의 전제입니다. 상태(세션·캐시·업로드 파일)를 외부 저장소(Redis·DB·오브젝트 스토리지)로 빼는 것이 클라우드 앱의 기본입니다.

설정 분리 vs 무상태 프로세스 — 잘못된 방식과 올바른 방식 비교확대

위 그림처럼 설정을 코드에 하드코딩하거나 세션을 로컬에 두면 확장과 이식이 불가능하고, 환경변수 분리와 외부 세션 저장소로 이를 해결합니다.

로그 스트림과 일회성 프로세스 — 앱은 로그를 파일이 아닌 stdout 스트림으로 내보내고 수집은 플랫폼이 담당(로그 라우팅 분리). 프로세스는 일회성(disposable)이라 빠른 기동·graceful shutdown(SIGTERM 처리)·언제든 교체 가능해야 함 — 컨테이너·오토스케일 환경에서 무중단 배포·탄력 확장의 토대확대

위 그림처럼 앱은 로그를 stdout으로 흘리기만 하고 플랫폼이 수집하며, 일회성 프로세스는 빠른 시작·우아한 종료·무상태 재시작으로 K8s와 자연스럽게 맞물립니다.

클라우드 준비도 판단 — 어디서 12-Factor를 어기고 있나
설정(DB주소·키)을 어디에코드/이미지 하드코딩 금지 → 환경변수. 그래야 같은 이미지가 dev/staging/prod에 그대로. '환경마다 다른 이미지가 필요하면 이미 어긴 것'
세션·업로드 파일을 어디에로컬 디스크/메모리 금지 → 외부(Redis·S3·DB). 무상태라야 인스턴스를 자유롭게 늘리고 죽여도 됨. '상태가 로컬에 있으면 확장 불가'
로그를 파일로 vs stdout앱은 stdout으로 흘리기만, 수집·회전은 플랫폼이. 앱이 파일·회전을 관리하면 컨테이너와 안 맞음. '로그는 스트림이지 파일이 아니다'
비밀번호를 설정에 평문환경변수에도 평문 비밀은 위험 — 비밀 저장소(Vault·Secrets Manager) 참조. 깃·이미지·로그에 새지 않게. '설정과 비밀은 다르게 다룬다'
앱이 종료 신호를 받으면SIGTERM에 진행 중 요청 마무리 후 우아하게 종료(graceful shutdown). 빠른 시작·우아한 종료라야 K8s 롤링·오토스케일과 맞물림. '죽일 수 있게 만들어야 자동화된다'
어디까지 12-Factor를 지킬까전부 교조적으로가 아니라 '확장·이식·자동화'에 직결되는 것(설정·상태·로그·종료) 먼저. 레거시는 한 번에 못 바꾸니 우선순위. '원칙은 목적의 수단'

클라우드 준비도 점검 — 직접 확인

112-Factor 체크리스트로 새 서비스 준비도 점검

새 서비스를 컨테이너/K8s에 올리기 전, 인프라가 이 체크리스트로 '클라우드에서 잘 굴러갈지'를 미리 판단합니다.

TEXT
□ 설정: DB접속·키가 코드/이미지에 하드코딩됐나, 환경변수/Secret으로 빠졌나?
   → grep -r "password\|api_key" src/  로 하드코딩 흔적 확인
□ 무상태: 세션·캐시·업로드를 로컬 메모리/디스크에 두나, 외부(Redis/S3)에 두나?
   → 인스턴스 2개로 늘려 로그인·업로드가 유지되는지 테스트
□ 로그: 파일에 직접 쓰나, stdout으로 흘리나?
   → 컨테이너 docker logs로 앱 로그가 보이는지 확인
□ 종료: SIGTERM 받으면 처리 중 요청을 마치고 우아하게 종료하나?
   → graceful shutdown 구현 여부([[glossary-server-was-linux]])
OUTPUT
점검 결과 예:
  설정  ✗ application.yml에 운영 DB 비밀번호 하드코딩 → Secret으로 분리 필요
  무상태 ✗ 세션을 톰캣 로컬에 저장 → Redis 세션으로 외부화 필요
  로그  ✓ stdout 출력
  종료  ✗ SIGTERM 즉시 종료(처리 중 요청 유실) → graceful shutdown 추가
→ 3개 미흡. 이대로 scale-out하면 세션 풀림·비밀 유출·배포 시 요청 유실
echo '설정·상태·로그·종료 4축 점검'
🔍실행 후 확인할 것
  • 설정 점검: 코드/이미지에 비밀번호·키가 박혀 있으면 즉시 위험(유출 + 환경별 빌드) → Secret/환경변수로 분리. grep으로 하드코딩 흔적부터 찾는다
  • 무상태 점검: 인스턴스를 2개로 늘렸을 때 로그인·장바구니·업로드가 유지되면 무상태(OK). 풀리면 로컬 상태 보유 → 외부 저장소로 빼기 전엔 scale-out 금지
  • 로그 점검: docker logs로 앱 로그가 보이면 stdout(OK). 컨테이너 안 파일에만 쌓이면 컨테이너 재시작 시 소실 → 수집 불가(용어사전)
  • 종료 점검: 배포(롤링) 중 처리 중이던 요청이 에러로 끊기면 graceful shutdown 미구현 → SIGTERM 후 유예시간 동안 마무리하도록 수정

상황: 개발 편의로 application.yml에 운영 DB 비밀번호·API 키를 넣어 이미지에 빌드했습니다. 그 결과 dev/stg/prod마다 다른 이미지를 빌드해야 하고, 더 심각하게는 운영 비밀이 git 저장소와 이미지 레이어에 그대로 남아 유출됩니다.

원인: 12-Factor의 Config 원칙 위반 — 환경마다 다른 값(설정)을 코드/이미지에 박았습니다. "같은 산출물을 모든 환경에 배포"하는 재현성(시맨틱 버저닝)과 보안이 함께 깨집니다.

진단:

로컬 터미널
grep -rnE "password|secret|api[_-]?key" src/ config/ | grep -v "ENV\|getenv\|\\${"
# 실제 값이 박혀 있으면(환경변수 참조가 아니라) 하드코딩 확정
git log -p -- application.yml | grep -i password   # 이력에도 남았는지

해결: (1) 설정을 환경변수/Secret Manager(Vault·K8s Secret)로 분리하고 코드는 동일 유지. (2) 이미 노출된 비밀은 즉시 교체(rotate) — 저장소에서 지워도 git 이력에 남으므로 키 자체를 갱신해야 함. (3) CI에 시크릿 스캐너를 넣어 하드코딩을 머지 게이트에서 차단(CI/CD 파이프라인). 설정 분리는 편의가 아니라 보안·재현성의 기본입니다. 환경별 설정 운영은 용어사전에서 더 다룹니다.

심화 — 원칙을 지켰는데도 남는 문제들

💡개념

심화: 무상태의 청구서 — 상태는 사라지지 않고 이동한다

12-Factor 체크리스트를 통과했다면, 다음은 그 원칙들이 만들어 내는 새 비용을 보는 단계입니다. 원칙은 문제를 없애는 것이 아니라 더 다루기 쉬운 곳으로 옮기는 것이고, 옮겨 간 자리에서 새 숙제가 시작됩니다.

  • 무상태화는 상태의 이동입니다: 세션·캐시를 Redis로 빼면 앱은 자유로워지지만, 이제 모든 인스턴스가 그 Redis 하나에 매달립니다. 그 Redis가 죽으면 열 개로 늘린 인스턴스가 동시에 무너지므로, 무상태 앱의 확장은 백킹 서비스의 고가용성이 전제입니다. 상태를 옮겼으면 옮겨 간 곳의 이중화·용량 산정이 새 숙제가 됩니다.
  • 네트워크 왕복이라는 상시 비용: 프로세스 메모리에서 꺼내던 것을 외부 저장소에서 가져오면 매번 네트워크 왕복이 붙습니다. 요청 하나가 세션·설정·캐시를 수십 번 조회하는 구조라면 이 왕복이 응답시간의 주성분이 됩니다. 그래서 실무에선 무상태 원칙을 지키면서도, 자주 읽고 잃어도 되는 값은 수명이 짧은 인스턴스 내 캐시로 왕복을 줄이는 절충을 씁니다 — 원칙의 목적(어느 인스턴스든 동일)을 해치지 않는 선에서.
  • 환경변수의 한계: Config 원칙의 표준 수단인 환경변수는 값을 바꾸려면 프로세스 재시작이 필요하고, 프로세스 정보 조회나 오류 덤프에 노출될 수 있으며, 유효기간을 두고 자동 교체(rotation)하는 것도 못 합니다. 그래서 규모가 커지면 비밀은 환경변수 평문이 아니라 비밀 저장소 참조·파일 마운트로 진화합니다 — 원칙의 정신(코드와 설정의 분리)은 유지하되 수단을 발전시키는 것입니다.

12-Factor는 2011년, 지금과는 다른 플랫폼 시절에 쓰인 문서입니다. 조항을 문자 그대로 외우기보다 각 조항이 지키려던 것(재현성·확장성·자동화)을 이해하면, 조항끼리 부딪히는 상황에서도 스스로 판단할 수 있습니다.

상황: 체크리스트대로 SIGTERM을 받으면 처리 중인 요청을 마치고 종료하도록 구현을 마쳤습니다. 그런데도 롤링 배포 때마다 수십 건의 502가 찍힙니다. 에러 시각은 배포 시각과 정확히 겹치는데, 앱 로그엔 정상 종료 기록만 남아 있습니다.

원인: 종료 신호와 트래픽 차단이 서로 다른 경로로 동시에 진행되기 때문입니다. 플랫폼이 인스턴스에 SIGTERM을 보내는 것과 로드밸런서의 대상 목록에서 그 인스턴스를 빼는 것은 별개 작업이라 순서가 보장되지 않습니다. 앱이 SIGTERM을 받고 새 요청 수신을 끊은 뒤에도, 목록 갱신이 전파되기 전 몇 초 동안 로드밸런서는 그 인스턴스로 계속 요청을 보냅니다 — 그 틈에 도착한 요청이 502가 됩니다. graceful shutdown은 '받은 요청의 마무리'를 책임질 뿐, '죽기 시작한 곳으로 새 요청이 오는' 이 경합은 앱 혼자 못 막습니다.

진단: 502의 타임스탬프를 배포 이벤트·인스턴스 종료 시각과 겹쳐 봅니다. 에러가 항상 각 인스턴스 종료 직후 몇 초 구간에만 몰려 있고, 로드밸런서 접근 로그에서 이미 종료 중인 인스턴스로 향한 요청이 확인되면 이 경합이 원인입니다.

해결: SIGTERM을 받아도 즉시 수신을 끊지 말고 몇 초 기다린 뒤 종료 절차를 시작하게 합니다 — 그 몇 초가 로드밸런서 목록 전파를 기다려 주는 시간입니다. 순서를 만들면 더 확실합니다: 먼저 헬스체크(readiness)를 실패시켜 새 트래픽을 끊고, 남은 요청을 드레인한 뒤, 마지막에 프로세스를 종료합니다. 이 타이밍 조율까지 갖추면 롤링 배포(릴리스 전략) 중 에러가 0에 수렴합니다. 우아한 종료의 완성은 앱 코드가 아니라 플랫폼과의 합의된 순서에 있습니다.

💼
실무 맥락
현업 패턴

인프라/SRE로서 12-Factor는 설계 리뷰의 체크리스트입니다 — 새 서비스가 컨테이너/K8s에 올라오기 전, 설정 분리·무상태·stdout 로그·graceful shutdown이 됐는지 점검해 "이대로 올리면 scale-out에서 세션 풀리고, 배포 때 요청 유실되고, 로그가 사라진다"를 미리 경고합니다. 이 점검 한 번이 출시 후의 반복 장애를 막습니다. PM은 12-Factor를 '클라우드 준비도'의 공통 언어로 삼아, 비기능 요구(요구사항 정의)에 "무상태·설정 외부화·관측성"을 명시하고 인프라와 개발이 같은 기준으로 설계하도록 정렬합니다.

다음 모듈에서는 서비스 간 통신을 느슨하게 만드는 또 하나의 핵심 — 동기 vs 비동기와 메시지 큐를 다룹니다.

실전 랩으로 손에 익히기: 12-Factor App 실습 — 설정 분리·무상태·배포 일관성을 항목별로 진단합니다.

지식 확인

퀴즈 — 8문제

Q1

12-Factor의 '설정(Config)은 환경에 저장' 원칙이 막으려는 문제는?

Q2

'무상태(Stateless) 프로세스' 원칙이 수평 확장(scale-out)에 중요한 이유는?

Q3

12-Factor의 '로그는 이벤트 스트림으로' 원칙이 권장하는 것은?

Q4

PM·인프라가 12-Factor를 '체크리스트'로 쓰면 좋은 점은?

Q5

같은 빌드 산출물(이미지)을 dev·staging·prod에 그대로 쓰면서 환경별 설정만 다르게 하려면 12-Factor가 권하는 방법은?

Q6

무상태(Stateless) 원칙을 지키려면 세션·업로드 파일 같은 상태를 어디에 둬야 하나?

Q7

[심화] 세션·캐시를 Redis로 빼 앱을 무상태로 만들었다. 이 무상태화가 새로 만드는 숙제는?

Q8

[심화] graceful shutdown(SIGTERM에 요청 마무리 후 종료)을 구현했는데도 롤링 배포 때마다 502가 몇 초씩 난다. 앱 로그엔 정상 종료 기록만 있다. 원인과 처방은?

0 / 8 답변

🧪 실습으로 확인하기

12-Factor App — 환경에 흔들리지 않는 앱의 원칙

중급

설정을 코드에서 분리하고, 프로세스를 무상태로 만들며, 빌드·릴리스·실행 분리와 dev/prod 패리티로 배포를 일관되게 만들어, 어느 환경에 놔도 똑같이 도는 앱으로 12-Factor 관점에서 항목별 진단·정비한다.

50📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요