[BACKEND] The Twelve-Factor Application

Block Odyssey Tech Blog
13 min readApr 26, 2023

--

12Factor 란?

The Twelve Factors(이하 12Factor)란 간단히 말하면 SaaS(Software As A Service)를 위한 방법론으로 12가지 요소를 원칙대로 Application을 구성하면 자연스럽게 독립된 환경으로부터 운영이 가능해 집니다.
즉 많은 장점을 가진 Cloud Native Application 으로 만들기 위한 필수 원칙으로 알려져 있습니다.

SaaS 특징

  • 설정 자동화를 위한 절차(declarative) 를 체계화 하여 새로운 개발자가 프로젝트에 참여하는데 드는 시간과 비용을 최소화 한다.
  • OS에 따라 달라지는 부분을 명확히 하고, 서로 다른 실행 환경 간의 이식성을 최대화 한다.
  • 최근 등장한 Cloud Platform 배포에 적합하고, 서버와 시스템의 관리가 필요 없게 된다.
  • 개발 환경과 운영 환경의 차이점을 최소화함으로써 지속적인 배포신속성을 극대화 한다.
  • 소프트웨어(툴, 아키텍처, 개발 방식)의 변화를 최소화 하면서 확장 한다.

12Factor는 Back-end 영역에서의 필수 원칙으로 알려져 있지만,
Front-end 개발자도 서버에 대한 업무 영역(React-ReactDOMServer, NextJS-API Routes)이 넓혀지고 있어서 모든 원칙을 필요로 하진 않겠지만 어느 정도는 중요한 가이드가 될 꺼라 생각 합니다.

그럼 12가지 항목에 대해 알아보면서 실제 서비스엔 어떻게 적용되어야 하는지에 대해 얘기해보겠습니다.

1. 코드베이스 (CodeBase)

  • 버전 관리되는 하나의 코드 베이스와 다양한 배포

코드 베이스는 가장 중요하고 기본이라 할 수 있는 부분 입니다.
하나의 코드 베이스는 하나의 앱을 가지면서 서비스 간 의존성이 낮아지며 독립된 커뮤니케이션 구조를 유지하지만, 하나의 코드 베이스만으로 서로 다른 환경(dev, stage, production)에 배포가 가능합니다.

이는 곧 팀의 작업 속도 향상을 가져오고 서비스의 성장과 속도에도 영향을 끼치게 됩니다.

2. 종속성 (Depedencies)

  • 명시적으로 선언되고 분리된 종속성

대부분의 프로그래밍 언어는 라이브러리 배포를 위한 패키징 시스템을 제공하고 있습니다.
Application 실행에 있어 필요한 모든 라이브러리와 버전을 리스트업하고 설치 및 배포는 관리툴(npm, yarn)을 통해 손쉽게 진행 가능합니다.

12Factor에서는 더 넓은 의미를 포함 하고 있습니다.

Twelve-Factor App은 전체 시스템에 특정 패키지가 암묵적으로 존재하는 것에 절대 의존하지 않습니다.
종속선 선언 mainifest를 이용하여 모든 종속성을 완전하고 엄격하게 선언합니다.
더 나아가, 종속성 분리 툴을 사용하여 실행되는 동안 둘러싼 시스템으로 암묵적인 종속성 “유출”이 발생하지 않는 것을 보장합니다.
이런 완전하고 명시적인 종속성의 명시는 개발과 서비스 모두에게 동일하게 적용됩니다.

예를 들어서 라이브러리 버전 및 package.json 에 정확한 버전을 사용(‘~,^’ 제거, lock파일 사용)하거나 실행 환경에서는 도커 컨테이너를 통해 종속성을 관리할 수 있습니다.

3. 설정 (Config)

  • 환경(environment)에 저장된 설정

여기서 말하는 설정의 정의는 데이터베이스 연결 정보, 서비스 인증 정보, 외부 연결을 위한 호스트 정보 등 배포 환경(dev, stage, production)에 따라 달라질 수 있는 모든 것을 말합니다.
이러한 환경별 변수는 코드 내부에 두지 않고 분리된 공간에 저장되어야 하고, 런타임에서 코드에 의해 읽혀야 합니다.

예를 들어 아래와 같은 기술이 있습니다.

  • Rails — config/database.yaml, Next.js — Runtime Configuration, Spring Cloud Config

4. 백엔드 서비스 (Backing Services)

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

여기서의 백엔드 서비스란 서드파티 서비스로 DB, Cache, SMTP, Messaging/Queueing system, Map 등 Application이 정상 동작 하는 동안 네트워크를 통해 이용하는 모든 서비스를 말합니다.

백엔드 서비스 원칙을 지키기 위해선 각 리소스별로 자유롭게 배포에 연결되거나 분리될 수 있어야 합니다.
또한 환경별로도 자유롭게 선택할 수 있어야 하는데 예를 들면 DB를 MySQL에서 Amazon RDS로 전환할 때 코드 수정 없이 가능해야 합니다.

5. 빌드, 릴리즈, 실행 (Build, release, run)

  • 철저하게 분리된 빌드와 실행 단계

코드 베이스는 build > release > run의 단계를 거쳐 배포로 변환되며, 각 단계는 엄격하게 분리되어야 합니다.

  • Build : 코드를 빌드하여 실행 가능한 번들 파일로 변환시키는 단계로, 커밋된 코드 중 배포 프로세스에서 지정된 버전을 사용하며, 종속성을 가져와 바이너리와 에셋들을 컴파일 합니다.
  • release : 빌드 단계에서 만들어진 빌드와 배포의 현재 설정을 결합합니다. 완성된 릴리즈는 빌드 설정을 모두 포함하며, 실행 환경에서 바로 실행될 수 있도록 합니다.
  • run : 선택된 릴리즈에 대한 애플리케이션을 실행 환경에서 돌아가도록 합니다.

빌드는 새로운 코드가 배포될 때마다 개발자에 의해 시작되지만 실행 단계는 서버가 재부팅되거나 충돌이 발생하여 자동으로 재실행 될 수 있습니다.
이 경우 배포툴(도커)를 이용하여 문제 발생시 빠른시간안에 대응이 가능하도록 하고 실행 단계는 최대한 변화가 적어야 합니다.
그러므로써 개발 — 인프라, 개발 — 운영 관계에서 의존성이 낮아져 결합도를 느슨하게 유지할 수 있다는 원칙입니다.

6. 프로세스 (Processes)

  • 애플리케이션을 하나 혹은 여러 개의 무상태(stateless) 프로세스로 실행

실행 환경에서 Application은 하나 이상의 프로세스로 실행되며, 각 프로세스는 stateless로 아무것도 공유하지 않아야 합니다.
SaaS는 여러개의 인스턴스로 Scale-out이 가능하기 때문에 인스턴스가 재실행될 때 local file, session 등과 같은 상태 정보는 모두 초기화 됩니다.
그러므로 인스턴스의 메모리와 파일을 공유하지 않고 유지가 필요한 데이터들은 백엔드 서비스(DB, Redis)에 저장되어야 합니다.

웹 시스템 중 유저의 세션 데이터를 앱의 프로세스 메모리에 캐싱하고, 이후 같은 유저의 요청도 같은 프로세스로 전달될 것 가정하는 방식을 ‘Sticky’ 방식이라 하는데, 이러한 방식은 12Factor에 위반됩니다.
준수 방법으론 위에 말한 백엔드 서비스를 이용하거나 세션 상태 데이터의 경우에는 범용 분산 캐시 시스템인 Memcached 또는 Redis와 같은 데이터 저장소를 사용합니다.

7. 포드 바인딩 (Port binding)

  • 포트 바인딩을 사용해서 서비스를 공개함

일반적으로 프론트엔드 부분에서는 브라우저를 통해 포트 바인딩된 서비스에 접근하기 때문에 대부분 지켜진다고 생각됩니다.
포트 바인딩을 사용한다는 것은 프론트엔드뿐만 아니라 하나의 Application이 다른 Application을 위한 백엔드 서비스가 될 수 있다는 것을 의미합니다.

예를 들자면 각각의 마이크로 서비스 A가 B의 서비스를 사용해야 할 때, B의 DB계정과 권한을 A에게 부여해서 B의 DB에 접속 하게 하는 경우가 바로 이 규칙을 위반하는 것입니다.
이를 해결하기 위해선 A에서 B 서비스로의 HTTP 요청을 통해 원하는 DB 데이터를 확인할 수 있도록 구성해야 합니다.

  • PHP — Apache HTTPD, Java — Tomcat, Python — Tornado, Ruby — Thin

8. 동시성 (Concurrency)

  • 프로세스 모델을 통한 확장

여섯번째 원칙 ‘프로세스(Processes)’에서도 잠시 언급했듯이 각 프로세스는 stateless로 아무것도 공유해야 할 상태가 없어 수평적으로 확장(Scale-out)되어야 동시성도 높일 수 있습니다.
프로세스의 타입과 각 타입별 프로세스의 갯수의 배치를 ‘프로세스 포메이션’이라고 합니다.

예를 들어 싱글 스레드 기반으로 돌아가는 Node.js에 2개 이상의 core가 존재한다면, cluster 기능을 활용해서 프로세스 매니저인 pm2로 손쉽게 수평으로의 확장이 가능하도록 구현할수 있습니다.

9. 폐기 가능 (Disposability)

  • 빠른 시작과 그레이스풀 셧다운(graceful shutdown)을 통한 안정성 극대화

12factor에서는 프로세스의 시작, 종료, 배포가 빈번하기 때문에 각 단계 별 시간 최소화가 중요합니다.
특히 종료를 안전하게 하는 것을 ‘graceful shutdown’ 이라 합니다.

서버에서 Kill 명령을 날렸을 때 기본적(-s 없이)으로 시그널을 지정하지 않으면, 기본 시그널 값은 정상 종료(15, SIGTERM)가 됩니다.

$ kill -l

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

강제종료 호출인 ‘kill -9 pid’, ’kill -SIGKILL pid’ 형태의 호출은 데이터가 유실되거나 리소스가 제대로 안 닫히는 등의 큰 문제가 발생할 수 있기 때문에, 프로세스를 안전하게 종료하기 위해 권장하지 않습니다.

10. 개발/프로덕션 환경 일치 (Dev/Prod parity)

  • development, staging, production 환경을 최대한 비슷하게 유지

개발, 스테이징, 프로덕션 환경을 최대한 비슷하게 유지하는 것을 의미합니다.
보통 이 3단계에는 큰 차이점 세가지가 있는데 이는 아래와 같습니다.

  • 시간의 차이 : 개발자가 작업한 코드는 프로덕션에 반영되기까지 시간이 소요됩니다.
  • 담당자의 차이 : 대부분의 경우 개발자가 작성한 코드를 시스템 엔지니어가 배포합니다.
  • 툴의 차이 : 배포 환경과 개발자의 환경이 OS, Tools 등이 다를 수 있습니다.

DB, Queuing system, Cache와 같은 백엔드 서비스는 개발환경과 프로덕션 환경 일치가 중요한 영역입니다.
많은 언어들은 다른 종류의 서비스에 대한 어댑터를 포함하고 간단하게 백엔드 서비스에 접근할 수 있는 라이브러리들을 제공합니다.

12Factor는 개발 환경과 프로덕션 환경의 차이를 최대한 작게 유지해서 지속적인 배포가 가능하도록 설계 되어 있습니다.
위에 언급한것 외에도 https 적용 및 인증서 종류, API CORS, CDN 정책 등 프로덕션 환경으로 배포된 후에야 뒤늦게 문제가 발견되는 경우가 종종 발생하는데,
이러한 문제점들이 발생하지 않도록 최대한 모든 Application의 배포는 같은 종류, 같은 버전을 유지하도록 해야 합니다.

11. 로그 (Logs)

  • 로그를 이벤트 스트림으로 취급

여기서 말하는 로그란 이벤트 스트림 취급을 로그 파일로 작성하거나, 직접 관리하지 않고 각 프로세스에서 이벤트 스트림을 버퍼링 없이 stdout에 출력하는 것을 말합니다.
이 이벤트 스트림은 파일로 보내지거나 터미널에서 실시간으로 보여질 수 있는데, 가장 중요한 점은 로그 분석 시스템이나 범용 데이터 보관소에 보내질 수 있다는 점입니다.
이러한 시스템은 아래와 같은 동작을 조사할 수 있는 강력함과 유연성을 가지게 됩니다.

  • 과거의 특정 이벤트를 찾기
  • 트렌드에 대한 거대한 규모의 그래프 (예: 분당 요청 수)
  • 유저가 정의한 휴리스틱에 따른 알림 (예: 분당 오류 수가 임계 값을 넘는 경우 알림을 발생시킴)

로그의 중요성은 아무리 강조해도 지나치지 않는다고 생각합니다.
서버에 문제가 발생하거나 CS 처리시 해당 이슈를 얼마나 빠르게 파악할 수 있는지는 결국 로그 시스템이 어느 정도로 갖춰져 있는가와 트래킹이 어느 정도까지 가능한지에 달려 있습니다.

이번 규칙의 핵심은 SaaS Application은 언제든지 인스턴스가 삭제되고 생성되므로, 로그를 스트림으로 취급해 별도의 로그 저장소를 사용해야 한다는 점입니다.

12. Admin 프로세스 (Admin processes)

  • admin/maintenance 작업을 일회성 프로세스로 실행

운영 관점에서 일회성 관리나 유지보수 작업이 필요할 수 있는데, 이러한 작업들이 프로덕션 환경과 동일한 환경에서 스크립트나 명령을 통해 실행 되어야 한다는 것을 말합니다.
12factor는 별도의 설치나 구성 필요 없이 실행 환경과 동일한 환경에서 사용 가능하도록, REPL Shell이 제공되는 언어 사용을 권장합니다.

REPL을 지원하는 언어로는 Java-REPL Shell, rails-console, python-perl, Ruby-irb, rails-rails console 등이 있습니다.

마치며..

이렇게 12개의 특성에 대해 알아보았는데요.

많은 부분이 마이크 서비스(MSA) 개발에 있어서도 고려가 되어야 겠다라는 생각이 들었습니다.
12Factor도 Cloud 환경에서 실행되는 Application를 위한 것이고, MSA도 같은 Cloud 환경에서 손쉽게 확장 가능하도록 개발하기 위한 아키텍처로 등장했기 때문입니다.

12Factor가 정답이 될 수는 없습니다.
12가지 특성에 대해 맞다 틀리다 혹은 절대적인 것이냐에 대해선 논하지 않고,
중요한 것은 12Factor의 각각의 원칙이 진정으로 얘기 하고자 하는 방향이 무엇인지와 100% 이해가 되지 않는 부분들이 있을 수 있지만 각자 환경과 상황에 맞게 적용한다면 확장성 높은 Application을 개발하는데 있어서 참고하기에 훌륭한 가이드라고 생각합니다.

긴 글 읽어주셔서 감사합니다.

--

--