2025년에 다시 읽는 TwelveFactor App과 주해
SaaS 또는 웹앱개발 시 확장성과 유지보수성을 지키기 위한 방법론이다.
Heroku 에서 주도적으로 작성하여 배포하였다.
단일 코드베이스에서 모든 것이 코드로 한다는 원칙하에 더욱 견고한 프로그램을 구축 할 수 있다.
“견해” 부분은 본인의 해석을 단 것으로 지극히 주관적임을 밝힌다.
자세한 사항은 다음 링크에서 읽어 볼 수 있다. The Twelve Factros App - 한국어판
I. 코드베이스
버전 관리되는 하나의 코드베이스와 다양한 배포
요약
- Git, Subversion 등 버전 관리 시스템을 통해 관리되어야 함.
- 코드 베이스와 앱은 1:1 관계.
- 여러 앱이 동일한 코드를 공유하지 말아야 함. 동일한 코드를 여러 앱에서 이용 할 때는 라이브러리 화 해서 종속성 관리 해야 함.
- 하나의 코드 베이스로 다수의 배포를 할 수 있도록 구현 해야 함 - 프로덕션, 스테이징, 개발, 로컬 등
나의 견해
만약 하나의 소스로 한개의 서버만 운영 한다면 이러한 원칙이 필요 없을 것이다. 그러나 우리는 최소한 실제 운영 환경과 개발 환경이라는 2개의 환경을 가지게 될 것이다.
배포되는 환경에 따라 소스 코드의 시점 차이(git commit)가 있을 수 있겠지만, 단일 코드 베이스를 유지하지 못하면 코드 확장이 불가능 할 수 있다. 하나의 코드 베이스를 유지하면서 다양한 배포 환경을 지원 할 수 있어야 한다.
그러나 실무적으로 운영 브랜치와 개발 환경이 발생하고 코드베이스가 파편화되기 시작합니다. 결국 단일 코드베이스를 유지하지 못한다는 것은 코드 확장의 포기를 의미한다. 배포 환경에 따라 커밋 시점(Version)의 차이는 있을지언정, 코드의 뿌리는 하나여야만 어떤 환경에서도 동일한 동작을 보장할 수 있다. 이것이 12-Factor가 강조하는 '이식성’의 출발점이다.
실무적으로는 운영(Production, main) 브랜치와 피쳐 브랜치들이 있을 것이다. 피쳐 브랜치가 너무 오래 유지 된다면 메인 브랜치와 코드가 너무 달라져서 병합이 힘든 머지 지옥(Merge Hell) 이 발생 한다. 그리고 기능이 완료 되지 않았는데 Broken Version(DB 변경) 을 유발 할 수 있거나 다른 코드 동작을 깨트리는 것이 메인 브랜치에 들어가 장애를 유발 할 수 있다. 이를 극복하기 위한 몇가지 패턴을 제시한다.
Feature Flag 기능 플래그을 통한 지속적인 소스 병합
- 개발중인 것도 메인 브랜치에 병합하고, 모든 확인이 끝났을 때만 활성화 되게 하며 이를 Dark Launching 이라고 한다.
- 소스코드가 활성화되기 위한 정보를 외부에 관리한다. 서비스가 실행 될 때 플래그가 활성화 되어 있어야만 그 기능이 동작하게 합한다. Feature Flag 패턴 구현이 필요하여 복잡해지는 단점이 있다.
- 이렇게 하면 "코드의 병합"과 "기능의 출시"를 분리할 수 있어, 12-Factor가 지향하는 단일 코드베이스 유지와 **지속적 통합(CI)**을 완벽하게 실현할 수 있다.
Expand and Contract 패턴 : DB 스키마 변경 시
- 병렬 변경 (Parallel Changes)
- 1단계(Expand): 기존 컬럼은 두고 새로운 컬럼을 추가 (구버전과 신버전 코드 모두 동작 가능).
- 2단계(Migrate): 데이터를 새 컬럼으로 이관.
- 3단계(Contract): 구버전 컬럼 삭제.
CI, 자동화된 테스트의 중요성 Broken Version 관리
- 버그가 있는 코드 혹은 해당 변경으로 일부 혹은 전체 시스템이 영향을 받을 수 있다. 이를 위해서 자동화된 테스트와 CI는 필수이다.
- 빌드와 테스트가 통과되지 않으면 아예 병합 자체를 불가능하게 만드는 강제성이 필수다.
II. 종속성
명시적으로 선언되고 분리된 종속성
요약
- Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않습니다.
- 권장 : Ruby 의 Bundler, Python 의 pypi, C autoconf 등
- 비권장 : 암묵적인 시스템 도구. ImageMagick, Curl 등
견해
종속성을 명시적으로 관리하라는 지침이다. 결국 종속성도 코드 베이스에 포함 되어서 관리 되어야 한다는 얘기. 그렇다면 배포도 코드로 관리하여 코드 베이스에 포함 되어 있고, 배포를 통해 도구들이 자동으로 설치되고 유지하는 방식도 있을 것이다.
배포 환경 자체를 코드로 관리(IaC)하고, 애플리케이션이 실행되는 최소한의 단위(Container Image) 안에 모든 도구를 포함하는 방식이 각광받고 있다.
결국 이 원칙의 핵심은 **“애플리케이션이 자신의 생존에 필요한 모든 것을 스스로 명시하고 책임지는 것”**이다. 이를 통해 우리는 특정 서버의 상태에 의존하지 않는, 어디서든 즉시 실행 가능한 '이식성’을 얻게 된다.
Vagrant, Docker 등 컨테이너화를 통해서도 코드를 통해 명시적으로 선언될 수 있다. Immutable Infrastructure 라는 개념을 통해 배포 시점에 도구를 '설치’하는 것을 넘어, 필요한 모든 것이 갖춰진 '이미지’를 통째로 갈아 끼우는 방식으로 발전했다. 이는 '암묵적인 존재’에 의존할 여지를 아예 차단한다.
III. 설정
환경(environment)에 저장된 설정
요약
- 설정은 배포마다 달라질 수 있는 모든 것 : DB, S3 등 외부 서비스. 배포된 호스트의 정규화된 호스트 이름(canonical hostname)처럼 각 배포마다 달라지는 값
- 이러 한 값들은 코드에 상수로 저장하지 않아야 한다.
- "설정"의 정의는 애플리케이션 내부 설정을 포함하지 않는다.
- Rails의 config/database.yaml처럼 버전 관리 시스템에 등록되지 않은 설정 파일을 이용하는 방식
- Twelve-Factor App은 설정을 환경 변수 (envvars나 env라고도 불림)에 저장하며 배포시 쉽게 변경 가능해야 한다.
- 설정 관리의 그룹화 : production, dev, test, local 등. 하지만 설정이 각 그룹의 조합으로 폭발하게 되고 애플리케이션의 배포를 불안정 하게 할 수 있음.
- 각 배포마다 독립적으로 관리해서 확장성을 갖춰야 한다
견해
설정 관리의 핵심은 애플리케이션이 자신의 실행 환경을 스스로 판단하지 않게 하는 것에 있다. 흔히 사용하는 ‘development’, ‘production’ 같은 환경 그룹화는 환경이 늘어날수록 설정의 파편화를 초래한다. 따라서 프로그램은 현재 환경이 무엇인가에 따라 판단하는 것이 아니라 "DB 주소"에만 관심이 있다. 그리고 DB 주소는 주입되는 것이다.
진정한 12-Factor 앱은 자신이 ‘어디서’ 도는지 관심이 없어야 하며, 오직 실행에 필요한 개별 변수(Database URL, API Key 등) 가 외부에서 주입되기를 기다릴 뿐이다.
이것은 Infrastructue as Code 전략과 맞물린다. ID, 암호, VPC 상의 타 서비스 주소 등은 환경 변수 키나 논리적 식별자 형태로 소스상에 존재 해야 하며, 실제 값은 환경 변수를 통해 주입되어야 한다. 그리고 이 환경 변수는 배포시에 생성 될 수 있다. 배포시에는 ‘안전하게 보관된(AWS Secrets Manager, Sealed Secret 등)’ 곳에서 데이터를 읽어서 환경 변수를 주입한다.
1 | |
IV. 백엔드 서비스(Backing Service)
이용 하는 백엔드 서비스를 연결된 리소스로 취급
요약
- 애플리케이션 동작 중 네트워크를 통해 이용하는 모든 서비스. 예) DBMS, 메시지 큐, SMTP, 캐시 등등
- Twelve-Factor App의 코드는 로컬 서비스와 서드파티 서비스를 구별하지 않는다.
- 같은 기능을 하는 서비스는 애플리케이션 코드를 수정하지 않고 엔드포인트만 변경해서 전환 가능하도록 유지해야 한다.
- 각각의 다른 백엔드 서비스는 리소스이다. 예를 들어 샤딩을 위해 2개의 MySQL 이 있으면 이것은 구분되는 두 리소스로 간주한다. 이 데이터베이스는 부착된(Attached) 리소스로 다루며, 이는 서로 느슨하게 결합된다는 의미이다.
- 리소스는 배포시 자유롭게 연결되거나 분리될 수 있어야 한다. 예를 들어 DBMS가 이상이 발생 할 경우 코드 수정 없이 새 DBMS를 사용 할 수 있다.
견해
영어로는 Backing service라고 했는데 한국어 번역은 백엔드 서비스로 되어 있다. 서비스를 구성하는 다른 구성 요소로 이해 할 수 있다. 백 밴드(Backing Band), 백 코러스(Backing Vocals) 처럼 뒷받침 해주는 요소.
백킹 서비스는 애플리케이션이 정상적인 동작을 위해 네트워크를 통해 소비하는 모든 서비스(데이터베이스, 메시지 큐, 캐시 시스템 등)를 의미한다. 애플리케이션의 핵심 로직과 이러한 외부 서비스들을 엄격히 분리하여, 언제든지 설정을 통해 교체 가능한 ‘연결된 리소스’ 또는 앱을 지원하기 위한 외부자원 이라는 뉘앙스를 강조하기 위해 위해 Backend Service 가 아닌 Backing Service 라는 용어를 쓴것으로 보인다. 내가 벡엔드 서비스를 개발하고 있으니 내꺼가 아닌 다른 것을 구분하기 위해 Backing 이라고 한 측면도 있는 것 같다.
위의 "Attached Resource"는 물리적으로 두개라면 명확하게 구분된 2개의 리소스로 취급해야 한다는 의미이다. 그래야 나중에 샤드 1번만 다른 서버로 옮기거나 샤드 2번만 업그레이드 할 때 코드를 건드리지 않을 수 있다.
여기서 강조하고자 하는 것은 리소스들은 언제든지 코드 변경 없이 다른 것을 이용 할 수 있도록 애플리케이션을 개발해야 한다는 것을 의미한다. 이렇게 하려면 리소스 종류별로 추상화된 인터페이스를 구현해야 할 것이다. 예를 들면 django 에서는 사용하는 DBMS를 변경 할 때 DBMS 설정을 변경하지 각 DBMS 와 통신하기 위한 코드를 새로 구현하지 않는다. 왜냐하면 ORM 계층과 DBMS 계층이 잘 추상화되어 구현되어 있기 때문이다. 이를 통해 인프라 결합에 유연하게 대처하는 '느슨한 결합(Loose Coupling)'을 달성 할 수 있다.
- 애플리케이션이 의존하는 모든 외부 서비스(DB, 큐 등)를 코드 수정 없이 설정값 변경만으로 언제든 갈아 끼울 수 있는 '부착된 리소스’로 추상화해야 한다.
- Loose Coupling(느슨한 결합), Portability(이식성) 원칙
V. 빌드, 릴리즈, 실행
철저하게 분리된 빌드와 실행 단계
요약
코드 베이스는 3단계를 거쳐 프로덕션에 배포로 변환된다. 이 단계를 엄격히 구분한다.
- 빌드 단계 : 코드 베이스를 실행 가능한 번들로 변환. 명시적으로 선언된 버전, 종속성, 바이너리, 애셋을 컴파일
- 릴리즈 단계 : 빌드 결과와 현재 설정을 결합하여 실행 환경에서 바로 동작하도록 준비
- 실행 단계 : 애플리케이션을 시작
원칙
- 코드베이스는 3단계(빌드, 릴리즈, 실행)를 거쳐 배포된다. 각 단계는 엄격히 분리되어야 하며, 실행 중인 코드에 대한 변경은 불가능하다.
- 어떤 단계가 다른 단계를 변경 할 수 없다.
- 모든 릴리즈는 고유한 릴리즈 ID 를 부여한다. 모든 변경은 새로운 릴리즈를 생성해야 한다.
견해
현대적인 IaC 원칙으로 구성된 CI/CD 에서는 이러한 사항이 잘 구현되어 있다. 만약 처음 코드베이스를 구성한다면 이러한 원칙하에 [빌드, 릴리즈, 실행] 단계를 구현해야 한다.
이 원칙의 핵심은 불변성(Immutable) - 한 번 빌드된 결과물(Artifact)는 절대 변하지 않는다(Immutable) 에 있다. 예를 들어, 핫픽스를 한다고 서버에 직접 코드를 수정하면 안된다는 것이다. 변경은 오로지 배포 프로세스를 통해서만 이루어져야 한다.
이와 더불어 불변성의 혜택을 활용하는 것은 롤백 이다. 불변한 결과물이 있기 때문에 이것으로 바로 롤백 할 수 있도록 인프라를 구성하라.
| 단계 | 12-Factor | 정의현대적 도구/개념 (예시) |
|---|---|---|
| 빌드(Build) | 코드 + 종속성 → 바이너리 | docker build (이미지 생성), jar |
| 파일릴리즈(Release) | 빌드 + 설정 → 릴리즈 버전 | Docker Image + K8s ConfigMap/Secret |
| 실행(Run) | 릴리즈 → 프로세스 실행 | kubectl apply (Pod 실행), Container 시작 |
VI. 프로세스
애플리케이션을 하나 혹은 여러개의 무상태(stateless) 프로세스로 실행
요약
- Twelve-Factor 프로세스는 무상태(stateless) 이며, 아무 것도 공유하지 않습니다.**
- 유지될 필요가 있는 모든 데이터는 데이터베이스 같은 안정된 백킹 서비스에 저장되어야 한다.
- Twelve-Factor 앱에서 절대로 메모리나 디스크에 캐시된 내용이 다른 실행에서도 유효할 것이라 가정하지 않는다.
- 애셋 패키징 도구를 통한 정적 파일들은 런타임에 컴파일 하지 않고, 빌드 단계에서 수행한다.
- 웹 시스템 중에서는 "Sticky Session"에 의존하는 경우도 있는데 Twelve Factor 에서는 절대로 지양한다.
견해
여기에서는 API, 스크립트 등 어떤 실행을 할 때 Stateless 원칙 하에서 실행 되어야 한다고 강조한다. Stateless 는 간단해 보이지만, 클라우드 애플리케이션 수평 확장(Scale Out) 을 위한 가장 중요한 전제 조건이다. 클라우드 환경에서 서버(프로세스)는 언제나 생성되고, 재시작되고, 삭제 될 수 있다. 따라서 “세선, 파일, 데이터” 등은 외부(Backing Service)에 위임하고, 애플리케이션은 상태에 의해 문제를 유발하지 않아야 한다.
다르게 표현하면 프로세스가 언제든지 죽고 살아나도 문제가 없도록 구현해야 한다. 이는 확장성과 탄력성 관점에서 바라 볼 수 있다. Backing Service 에 데이터가 저장되므로 프로세스가 죽고 사는 것이 문제를 발생하지 않는 것이다. 물론, 실행 중에 죽으면 그 실행은 취소가 되니 문제가 있을 수는 있겠지만 재실행 하면 되므로 시스템이 회복 탄력성을 갖추고 있다고 볼 수 있다.
프로그래밍 패러다임에 빗대자면 상태가 없는(Stateless) 프로세스는 순수 함수, Stateful 서버는 전역 변수를 참조하고 수정하는 함수로 볼 수 있겠다. 순수 함수는 몇번을 실행해도 입력이 같으면 결과가 같다. 데이터가 필요하다면 함수에 인자를 넘기듯이, DB 같은 외부 리소스에서 가져와야 한다. Serverless(FaaS, Function as a Service) 아키텍쳐가 지향하는 바이다.
VII. 포트 바인딩
포트 바인딩을 사용해서 서비스를 공개함
- 애플리케이션은 독립적으로 포트를 통해 기능을 공개한다.
- 포트 바인딩을 위해 애플리케이션 종속성에 Tomcat, gunicorn 등 관련된 서비스를 포함해야 한다.
- 포트 바인딩을 사용한다는 것은 하나의 앱이 다른 앱을 위한 서비스가 될 수 있다는 의미.
견해
이 원칙의 핵심은 “애플리케이션이 실행 환경(Web Server)에 종속되지 않고 스스로 서비스를 제공할 수 있는가?” 에 있습니다.
과거 2000년대 초반만 해도 JSP는 Tomcat이라는 컨테이너 안에 배포되어야 했고, PHP는 Apache의 모듈로 기생해야만 동작했습니다. 즉, 앱은 서버의 일부였습니다. 하지만 현대의 프레임워크(Spring Boot, FastAPI 등)는 웹 서버를 자신의 종속성(Library) 으로 내장합니다. 이제 애플리케이션은 실행 환경의 도움 없이도 run 명령 하나로 스스로 포트를 바인딩하고 HTTP 요청을 처리하는 완전한 독립체가 되었습니다.
이러한 '실행, 애플리케이션의 독립성 이 전제되었기에, 쿠버네티스 같은 오케스트레이션 도구는 앱의 내부 구현(언어, 프레임워크)을 몰라도 포트만 연결하여 거대한 시스템을 조립할 수 있게 된 것입니다. 즉, 포트 바인딩은 단순한 네트워킹 방식이 아니라, 앱을 블랙박스화하여 이식성을 극대화하는 표준 인터페이스입니다.
VIII. 동시성(Concurrency)
프로세스 모델을 사용한 확장
- Twelve-Factor App에서 프로세스들은 일급 시민(First Class Citizen)입니다.
- 서비스 데몬들을 실행하기 위한 유닉스 프로세스 모델
- 애플리케이션의 작업을 적절한 프로세스 타입으로 할당하여 다양한 작업 부하를 처리 할 수 있도록 함 : 예를 들어 HTTP 는 웹 서비스가 처리, 오래 걸리는건 Worker 프로세스로 분리
- 애플플리케이션은 [여러개의 프로세스, 여러개의 물리적 머신]으로 확장 될 수 있어야 함
- 프로세스 포메이션 : 타입과 각 타입별 프로세스의 배치
- 프로세스는 절대 데몬화 하지 않고 PID 파일을 작성해서는 안되며 대신 다른 관리자(systemd, 클라우드 분산 프로세스 매니저, Foreman등)에 의해 [생성, 재시작, 종료] 되어야 한다.
견해
“거대한 하나의 프로그램을 만들지 말고, 작고 단순한 프로세스 군단을 만들어라.”
전통적인 방식에서는 트래픽이 몰리면 **“어떻게 하면 코드 안에서 쓰레드(Thread)를 더 효율적으로 쪼개서 이 요청들을 다 처리할까?”**를 고민했다. 하지만 이 방식은 코드를 엄청나게 복잡하게 만들고, 서버 한 대의 물리적 한계(CPU, RAM)를 넘을 수 없다는 단점이 있다.
**수평 확장(Scale-out)**은 12-Factor 가 제시하는 해법이다. “그냥 똑같은 프로세스를 옆에 하나 더 띄우세요.” 이것을 구현하기 위해서는 트래픽 분산 전략 및 기술이 필요하다.
프로세스는 일급 시민이라 한건 문제 해결의 주체를 프로세스로 이동한 것으로 표현하기 위한 것으로 보인다. 전통적인 관점에서 개발자는 쓰레드 관리, 락 등을 신경쓰며 동시성 프로그래밍을 했지만 "프로세스"를 하나 더 띄우는 것으로 해결하는 전략이다. Unix 에서 Process 를 Fork 하는 것과 다를 바 없다.
다른 하나는 애플리케이션 내에서 역할별로 프로세스를 다양하게 구현하는 것이다. 예를 들면 고객을 응대하는 직원(Web Process)과 뒤에서 물건을 나르는 직원(Worker Process)을 구분하여 프로세스가 실행 되도록 한다. 서로 섞이지 않으니 관리도 쉽다.
"프로세스를 절대 데몬화하지 마라"는 말은 “앱이 스스로 백그라운드로 숨어들지 말게 하라” 는 것이며 애플리케이션의 내부 관점에서의 이야기로 보인다.
예를 들어 docker run -d 명령어를 쓰면 컨테이너가 백그라운드에서 돈다. 하지만 이는 **관리자(Docker)**가 컨테이너를 백그라운드에 배치한 것이지, 앱이 스스로 숨은 것이 아니다. 컨테이너 내부를 들여다보면 앱은 여전히 포그라운드(PID 1)에서 실행되고 있어야 한다.
만약 애플리케이션이 스스로 데몬화(fork & exit)를 시도하면, Docker는 메인 프로세스가 종료된 것으로 간주하여 컨테이너를 즉시 꺼버리게 됩니다. 즉, **“앱은 실행만 하고(Foreground), 위치 선정(Background)과 생명 주기 관리는 관리자(Docker/K8s)가 전담한다”**는 원칙으로 이해된다.
IX. 폐기 가능(Disposability)
빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화
요약
- 프로세스는 언제든 시작되거나 종료 가능해야 한다
- 이를 통해 배포가 쉽게 되고, 배포의 안정성도 담보한다
- 프로세스 시작 시간의 최소화하여 확장이 더 민첩하게 이루어 질 수 있게 한다
- 프로세스 매니저로부터 종료 신호를 받으면 그레이스풀 셧다운을 해야 한다.
- HTTP 의 경우 요청이 짧다는 가정하에 있고, 뒤집어 얘기하면 단일 HTTP(API) 요청은 가급적 짧게 구현해야 한다.
- worker 의 경우 현재 요청을 취소하고 재실행한다
- 그러나 우아하지 않은 종료도 처리될 수 있게 설계 되야 한다.
- Crash-only design에서는 논리적인 결론
의견
장애 복구에서 배포까지 아우르는 원칙으로 생각한다.
Graceful Shutdown 은 프로세스가 어떤 동작 수행 중 강제로 종료 될 때, 시스템이나 서비스에 문제를 일으키지 않고 종료 하는 것을 의미한다.
이 원칙은 무중단 배포와 오토스케일링의 성패를 가르는 중요한 요소이다. 프로세스가 빠르게 시작되지 못하면 트래픽이 폭주해서 스케일아웃을 했는데도 불구하고 실제 트래픽을 처리할 때까지 계속 장애 상태가 될 것이다.
배포시 장시간 동작하는 프로세스가 시스템이나 데이터를 망가뜨릴까봐 장시간 대기를 하면서 배포 하지 못하게 되면 어떻게 될까? 결제가 발생하는 도중 시스템에 장애가 발생하여 중단되고 시스템이 다시 작동 할 때 어떻게 되어야 할까? 이러한 상황들을 극복하기 위한 원칙을 제시한 것이라 할 수 있다. 멱등성을 지켜야 한다.
멱등성(Idempotent) : 작업을 하다가 말았으면(우아하지 않은 종료), 처음부터 다시 해도 결과가 똑같도록 하라.
Crash-only Design은 아주 흥미로운 개념인데, 쉽게 말해 **“정상 종료와 비정상 종료를 구분하지 말라”**는 것이다.
- “정상 종료할 때만 캐시를 파일에 저장한다” -> (X) 틀린 설계 (전원 나가면 데이터 날아감)
- “평소에 저널링(Journaling)을 잘해서, 언제 전원이 나가도 재부팅하면 복구된다” -> (O) Crash-only 설계
즉, **“시작(Startup)은 곧 복구(Recovery)다”**라는 철학을 가지라는 뜻이다.
X. 개발/프로덕션환경 일치
개발, 스테이징, 프로덕션 환경을 최대한 비슷하게 유지
- 역사적으로 개발환경과 프로덕션 환경 사이에는 큰 차이가 있어 왔다.
- 시간, 담당자, 툴
- Twelve Factor App 은 개발 환경과 프로덕션 환경 차이를 최소화 한다
- 시간 : 작성한 코드가 배포까지 며칠, 몇주, 수개월 이상 => 개발한 코드는 몇 시간 심지어 몇 분 후 배포
- 담당자 : 시스템 엔지니어 배포 => 코드 개발한 사람이 배포하고 모니터링에 깊이 관여
- 툴 : 프로덕션과 개발자가 서로 다른 툴을 사용 => 개발과 프로덕션 환경 일치
- 특히 프로덕션에는 강력한 백킹 서비스를 사용하고 로컬에서는 가벼운 것을 사용 하는것을 선호 할 수 있다. 예를 들어 PostgreSQL 을 프로덕션에 사용하지만 로컬에서는 SQLite 로 개발하는 것. 하지만 Twelve Factor App 에서는 이것을 지양한다.
- 예전보다 백킹 서비스 설치가 쉬워짐
- Vagrants, Docker 등 가벼운 가상 환경을 통해 쉽게 일치화
견해
로컬 환경에 개발을 위해서 여러 서비스를 띄우는 것이 번거로울 수 있다. Django 는 ORM 추상화가 매우 잘 되어 있다. 그럼에도 불구하고 SQLite 와 Postgresql은 차이가 있다(Null ordering). 이러한 프로덕션과 개발환경의 사소한 차이가 결국에는 프로덕션에 배포 됐을 때 문제를 사전에 알 수 없게 만든다. 사소한 버전 차이도 문제를 야기 시킬 수 있으므로, 코드 베이스를 통해 명시적으로 관리하고 동일 환경이 되게 해야한다.
한편, 프로덕션 환경과 개발 환경을 다르게 하고 싶은 이유 중 하나는 같은 환경을 꾸미는 것이 고역이기 때문이기도 하다. 따라서 환경을 같게 만드는 도구를 사용하던지(Vagrants, Docker 등) 그런 환경을 쉽게 만들고 설치 할 수 있는 도구를 코드 베이스에 추가하는 것이 좋다.
그런 견지에서 요즘 대부분의 서버는 리눅스에서 운영되므로 개발자의 환경도 리눅스인게 좋다. 하지만, 맥과 윈도가 데스크탑 환경이 훨씬 좋은 문제가 있다. 그러니 최소한 가상환경에서 실행해보거나 배포 전 프로덕션과 같은 환경에서 검증을 마쳐야 해야 한다.
혹자는 프로덕션과 닮지 않은 개발 환경은, 지뢰가 묻힌 놀이터와 같다. 고 했다.
| 구분 | 과거 (Legacy) | 12-Factor (Modern) |
|---|---|---|
| 시간의 차이 | 개발 후 배포까지 한 달 걸림 | CI/CD를 통해 몇 시간, 몇 분 안에 배포 |
| 담당자의 차이 | 개발은 개발자가, 배포는 운영팀이 | DevOps 문화: 개발자가 직접 배포하고 모니터링 |
| 툴의 차이 | 로컬은 SQLite, 운영은 Oracle | Container: 로컬과 운영 모두 동일한 Docker 이미지 사용 |
XI. 로그
로그를 이벤트 스트림으로 취급
- 로그는 실행중인 App 의 동작을 확인 할 수 있는 수단.
- 모든 실행중인 프로세스의 출력 스트림으로 기록된 이벤트가 시간 순서로 저열된 스트림.
- 앱은 출력 스트림의 전달이나 저장에 절대 관여하지 않아야 함. 대신 버퍼링 없이
stdout에 출력. - 스테이징이나 프로덕션에서는 각 프로세스의 스트림은 모든 프로세스의 출력 스트림과 병합되어 최종 목적지로 전달.
- 앱은 열람하거나 설정 할 수 없지만 이를 대신해주는 로그 라우터 사용. 예) Logplex, Fluentd
- 가장 중요한점은 로그는 로그 분석 시스템이나 범용 데이터 보관소에 저장하여 장시간의 앱의 동작을 분석 할 수 있어야 함
- 과거의 특정 이벤트 찾기
- 트렌드에 대한 대규모 그래프
- 알람
견해
부연설명
stdout에 스트림으로 출력하는 것을 강조하는 것은 예전에는 애플리케이션이 고유의 형식으로 로그 파일에 로그를 출력했기 때문. 현재는 stdout 에 출력하면 수집기가 로그 저장소에 기록하는 방식으로 변경됨. 즉, 더이상 앱은 로그 기능에 신경쓰지 말고 로그 전문 서비스를 이용.
로그 파일에서 관측 가능성(Observability)으로
과거 단일 서버 환경에서는 로그 파일을 직접 열어보는 것만으로 충분했다. 하지만 마이크로서비스와 클라우드 환경에서는 수십, 수백 개의 컨테이너가 수시로 생성되고 사라지기 때문에, 개별 인스턴스의 파일에 접근하여 상태를 확인하는 것은 불가능에 가깝다.
따라서 애플리케이션은 로그 저장 방식에 대한 고민을 내려놓고, 모든 이벤트를 표준 출력(stdout)이라는 **‘이벤트 스트림’**으로 흘려보낸다. 이렇게 단일화된 스트림으로 로그 라우터가 로그를 수집하여 중앙에서 통합 분석 할 수 있게 한다.
최근에는 단순한 로그 수집을 넘어 **로그(Logs), 메트릭(Metrics), 트레이스(Traces)**를 통합하여 시스템 전체를 조망하는 관측 가능성(Observability) 확보가 필수적인 트렌드로 자리 잡았다. 이를 위해 벤더 종속성을 피하고 표준화된 데이터 수집을 지원하는 OpenTelemetry가 사실상의 표준으로 자리 잡고 있다.
Log, Metric, Trace 관련 서비스/스택
- 관측 가능성(Observability): 시스템의 속성. “우리 시스템은 내부 상태를 얼마나 잘 보여주는가?” (개념/데이터)
- 3대 요소 (Logs, Metrics, Traces): 관측 가능성을 구현하기 위한 구체적인 데이터 타입
- APM: 이 데이터들을 자동으로 수집하고 분석해서 대시보드로 보여주는 소프트웨어 제품
| 구분 | APM (전통적 관점) | Observability (현대적 관점) |
|---|---|---|
| 핵심 질문 | “시스템이 건강한가?” | “왜 그런 일이 벌어졌는가?” |
| 대상 | 알려진 문제 예: CPU 과부하, 응답 속도 저하 | 알려지지 않은 문제 예: 특정 사용자 그룹만 결제가 5초 지연됨 |
| 주 데이터 | 트레이스(Trace) + 메트릭(Metric) 중심(코드 레벨 프로파일링에 강함) | 로그(Log) + 고차원 메트릭 + 트레이스 통합(맥락 파악에 강함) |
| 방식 | Agent 방식: 설치하면 알아서 다 해줌(Magic) | Instrumentation 방식: 개발자가 코드에 심거나 설정을 주입함 (Explicit) |
| 비유 | 건강검진표 (혈압 높음, 간 수치 높음) | MRI 및 정밀 수술 (정확히 어느 혈관이 막혔는지 탐색) |
APM은 ‘성능 모니터링’ 이라는 목적을 달성하기 위해 ‘메트릭과 트레이스’ 를 주로 사용하는 도구이며, 최근에는 ‘로그’ 까지 통합하여 **‘Observability Platform’**으로 진화하고 있다.
Metric : 시스템의 지표
메트릭은 ‘수치로 표현된 데이터의 집합’ 으로, 특정 시점의 시스템 상태를 숫자로 요약해서 보여준다. 시계열 데이터(Time-series). 숫자와 시간의 조합. CPU 사용량, 램 사용량, 디스크 IO, 네트워크, http status 별 통계 등
예: cpu_usage: 45%, memory_free: 2GB, http_requests_per_second: 500
개별 사용자가 누구인지는 관심 없고, "전체 평균 응답 속도"나 “총 에러 횟수” 같은 통계가 중요한다. 이에 따라 “CPU가 90% 넘으면 경고 보내라” 같은 규칙을 걸기에 가장 좋다. Prometheus, Grafana(시각화), Datadog(Metric) 등
Trace : 요청 추적 지도
트레이스는 ‘하나의 요청(Request)이 시스템을 통과하는 전체 경로’ 를 파악하는 것이다. 특히 마이크로서비스(MSA) 환경에서 중요하다. 이를 통해 특정 시나리오를 수행하는 애플리케이션들의 병목과 성능을 관찰 할 수 있다. 또한, 오류가 발생한 지점과 실행 경로를 확인 할 수 있다.
하나의 요청이 A서버 -> B서버 -> C서버를 거쳐 갈 때, 각 구간(Span)마다 시간이 얼마나 걸렸는지 보여준다. 사용자의 요청이 들어올 때 고유 ID를 부여해서, 여러 서버를 거쳐도 계속 유지된다. trace_id, correlation-id
대표 도구: Jaeger, Zipkin, AWS X-Ray, Datadog(APM), Sentry
상용 SaaS (돈으로 편리함을 사는 솔루션)
가장 강력하지만 비싸다. 하지만 총 소유비용은 직접 운영 개발하는 것보다 쌀지 모른다.
- Datadog: 업계 1위. 통합 대시보드가 가장 강력함.
- New Relic: 애플리케이션 성능 모니터링(APM)의 원조. 깊이 있는 코드 레벨 분석에 강점.
- Dynatrace: AI 기반의 자동 문제 원인 분석이 특징.
- Splunk: 로그 분석의 전통 강자. 보안 관제(SIEM) 쪽으로도 많이 쓰임.
② 오픈소스 / 구축형 (Self-Hosted)
ELK(OpenSearch)는 정말 강력함. 알람, 분석에서 LLM 과의 통합도 지원하고 있어서 더 강력해짐. 다만 토큰 = 돈
(1) ELK Stack (전통의 강자)
- Elasticsearch (검색/저장) + Logstash (수집) + Kibana (시각화)
- 장점: 강력한 검색 기능.
- 단점: 리소스를 많이 먹고 운영 난이도가 높음.
- OpenSearch 가 포크되어 나오면서 이 영역도 같이 뛰어들고 있음.
- 개인적인 경험으로 ARM 서버 클라우드가 x86 x64 서버보다 싸기 때문에 설치해 보려 했으나 관련 스택들이 ARM 친화적이지 못한 것들이 있었음. OpenSource니 기여하면 되잖아?!?
(2) Grafana ‘LGTM’ Stack (최근 대세) 가볍고, Grafana 하나로 모든 것을 봅니다.
- Loki (로그): “로그를 위한 Prometheus”. ELK보다 훨씬 가볍고 저렴함.
- Grafana (시각화): 모든 데이터의 시각화 허브.
- Tempo (트레이스): 분산 트레이싱 저장소 (Jaeger 대체).
- Mimir (메트릭): Prometheus의 장기 저장소 확장판.
(3) 개별 특화 도구
- Metrics: Prometheus (사실상 표준), InfluxDB, Thanos (Prometheus 확장).
- Trace: Jaeger (가장 유명함), Zipkin. Sentry
- Collection: OpenTelemetry Collector (모든 데이터를 수집해서 위 도구들로 보내주는 표준 에이전트).
효과적인 관측을 위해서는 이 세 가지 데이터의 역할 분담이 필수적이다. 메트릭(Metric) 을 통해 시스템의 전반적인 건강 상태와 트렌드를 파악하여 이상 징후를 감지하고, 트레이스(Trace) 를 통해 분산된 마이크로서비스 간의 요청 흐름을 추적하여 병목 지점을 찾아내며, 로그(Log) 를 통해 해당 지점의 구체적인 에러 내용과 맥락을 확인해야 한다.
12-Factor App이 로그를 스트림으로 취급하라는 것은, 결국 이 세 가지 데이터가 물 흐르듯 수집되어 통합 분석 시스템(Observability Platform)에서 유기적으로 연결되게 하기 위함이다. 소흘히 할 수 있는 영역이고 비용(자원 소모)도 많이 발생한다. 그렇다고 해서 없다면 계기판 없이 자동차를 운전하는 것과 마찬가지이다.
XII. Admin 프로세스
admin/maintenance 작업을 일회성 프로세스로 실행
요약
- 일회성 작업의 정의: 데이터베이스 마이그레이션(
migrate), 콘솔 실행(REPL), 일회성 스크립트 실행 등 운영/유지보수를 위한 작업들. - 환경의 동일성:관리 프로세스는 웹 프로세스와 완전히 동일한 환경에서 실행되어야 한다.
- 동일한 코드베이스와 **설정(Config)**을 공유해야 한다.
- 동일한 릴리즈(Release) 버전을 기반으로 실행되어야 한다.
- 종속성 분리: 관리 프로세스 역시 시스템에 설치된 도구가 아닌, 앱이 선언한 종속성(Dependency) 격리 환경(virtualenv, bundle exec 등) 내에서 실행되어야 한다.
- 실행 방식: 로컬에서는 쉘 명령어로, 프로덕션에서는 SSH 등을 이용하되, 실행 환경 자체는 일반 앱 프로세스와 차이가 없어야 한다.
견해
과거에는 운영자가 SSH로 서버에 접속해서, 수기로 작성한 스크립트를 돌리거나 root 권한으로 패키지를 즉석에서 설치해 작업을 처리하곤 했다. 12-Factor는 이러한 '변칙적인 관리 작업’을 엄격히 금지한다.
이 원칙의 핵심은 “관리 작업이라고 해서 특별 대우를 하지 마라” 는 것입니다. DB 마이그레이션을 하든, 데이터를 수정하는 스크립트를 돌리든, 그것은 웹 서버가 돌아가는 것과 똑같은 도커 이미지(Docker Image), 똑같은 환경 변수 위에서 실행되어야 한다는 것이다. 이 관리 작업까지도 코드베이스로 관리되거나 Log 수집을 통해 관리 되어야 한다.
예를 들어 쿠버네티스(K8s) 환경에서 이 원칙은 다음과 같이 적용된다.
- SSH 접속 금지:
ssh로 서버에 들어가는 대신,kubectl exec를 통해 실행 중인 컨테이너 환경 안으로 들어가거나,Job리소스를 생성하여 동일한 이미지로 일회성 작업을 수행한다. - 재현 가능성: 이렇게 해야만 관리자가 수행한 작업이 로그에 남고, 누가 언제 실행해도(로컬이든 운영이든) 동일한 결과를 보장할 수 있어야 한다.
결국 이 원칙은 운영 중 발생하는 ‘구성 표류(Configuration Drift)’—서버의 상태가 알게 모르게 조금씩 달라지는 현상—를 막는 마지막 안전장치이다.
개인적인 경험으로는 구성원끼리의 지식공유를 통한 관리가 목표였던 조직에서 일회성 관리 업무는 꼭 2명 이상이 함께 해야 했고, 가능하다면 해야하는 관리 업무를 코드로 작성해서 코드베이스에 포함시키고 따로 작업 기록도 남기게 했었다.