CH_03_01_애플리케이션과 컨테이너의 구성
Chapter 3: 실용적인 컨테이너 구축과 배포
3.1 애플리케이션과 컨테이너의 구성
현대의 시스템을 구축할 때, 우리는 여러 애플리케이션과 미들웨어 이미지로 생성된 다수의 컨테이너를 조합하여 사용하는 경우가 많습니다. 예를 들어, 웹 서버, 애플리케이션 서버, 데이터베이스가 각각 별도의 컨테이너로 실행되는 구성이 일반적입니다.

또한, 특정 기능의 부하를 분산시키기 위해 동일한 컨테이너 이미지를 복제하여 여러 호스트에 분산 배포하는 상황도 흔히 발생합니다. 이처럼 컨테이너 기반 아키텍처는 다양한 조합과 확장 가능성을 열어주므로, 시스템의 요구사항에 맞춰 컨테이너를 어떻게 구성할지 신중하게 설계하는 것이 매우 중요합니다.
3.1.1 '하나의 컨테이너 = 하나의 프로세스' 원칙의 함정
컨테이너는 애플리케이션의 배포를 간소화하는 데 특화된 기술입니다. 이 때문에 "하나의 컨테이너에는 오직 하나의 프로세스만 실행해야 한다"는 원칙이 널리 알려져 있으며, 많은 경우에 이는 훌륭한 지침이 됩니다. 하지만 이 원칙을 지나치게 경직되게 해석하면 오히려 비효율적인 구조를 낳을 수 있습니다.

예를 들어, 주기적으로 특정 작업을 실행해야 하는 잡(Job) 컨테이너가 있다고 가정해 봅시다. '하나의 컨테이너 = 하나의 프로세스' 원칙을 맹목적으로 따른다면, 다음과 같이 복잡한 구성을 고려해야 할 수 있습니다.
- 방법 1: 잡 컨테이너에 작업 실행 API를 만들고, 스케줄링을 담당하는 별도의
cron컨테이너에서 API를 호출하게 합니다. - 방법 2:
cron컨테이너 내부에 도커(Docker)를 설치하여, 잡 컨테이너를 직접 실행하도록 만듭니다.
단순히 주기적인 작업을 실행하는 것임에도 불구하고, 컨테이너 간 통신, API 설계, 혹은 '도커 속의 도커(Docker in Docker)'와 같은 복잡한 아키텍처가 필요해집니다.
하지만 다음과 같이 접근 방식을 바꾸면 훨씬 실용적인 해결책을 찾을 수 있습니다.
# 기본 이미지로 Ubuntu 23.10 버전을 사용합니다.
FROM ubuntu:23.10
# 패키지 목록을 최신 상태로 업데이트하고 cron 데몬을 설치합니다.
RUN apt-get update && apt-get install -y cron
# 실행할 작업 스크립트와 cron 설정 파일을 컨테이너에 복사합니다.
COPY task.sh /usr/local/bin/
COPY cron-example /etc/cron.d/
# cron 설정 파일에 적절한 권한을 부여합니다.
RUN chmod 0644 /etc/cron.d/cron-example
# cron 데몬을 포어그라운드에서 실행하여 컨테이너가 종료되지 않도록 합니다.
CMD ["cron", "-f"]
이 방식은 하나의 컨테이너 안에서 cron 데몬이 주 프로세스로 실행되고, 정해진 시간에 task.sh라는 자식 프로세스를 실행합니다. 이처럼 주된 역할(주기적인 작업 수행)을 완수하기 위해 보조 프로세스를 활용하는 것은 매우 효율적이고 직관적인 설계입니다. 프로세스의 개수에만 집착하여 컨테이너를 구성하면, 배포와 관리에 더 큰 비용을 초래할 수 있습니다.
3.1.2 핵심 원칙: 하나의 컨테이너, 하나의 관심사
그렇다면 올바른 컨테이너 설계 원칙은 무엇일까요? 도커 공식 문서에서는 "하나의 컨테이너는 하나의 관심사만 가져야 한다(Each Container Should Have Only One Concern)"고 강조합니다. 이는 객체 지향 설계의 단일 책임 원칙(Single Responsibility Principle)과 매우 유사한 개념입니다.
여기서 '하나의 관심사'란, 하나의 명확한 역할 또는 독립적인 문제 영역(도메인)을 의미합니다. 일반적인 웹 애플리케이션의 스택을 예로 들어보겠습니다.

이 구조에서 각 컨테이너는 독립적인 역할을 수행하며 시스템 전체를 구성합니다.
- 웹 서버 컨테이너 (e.g., Nginx): 정적 파일 서빙 및 리버스 프록시 역할 담당
- 애플리케이션 컨테이너 (e.g., Spring Boot, Node.js): 비즈니스 로직 처리 담당
- 데이터베이스 컨테이너 (e.g., MySQL, PostgreSQL): 데이터 영속성 담당
이렇게 관심사를 기준으로 컨테이너를 분리하면 다음과 같은 장점을 얻을 수 있습니다.
- 독립적인 확장(Scale-out): 트래픽이 증가하여 애플리케이션 서버의 증설이 필요할 때, 다른 컴포넌트에 영향을 주지 않고 애플리케이션 컨테이너만 복제하여 확장할 수 있습니다.
- 장애 격리: 특정 컨테이너에 문제가 발생하더라도 전체 시스템의 장애로 이어지지 않고, 문제의 범위를 해당 컨테이너로 한정할 수 있습니다.
- 유연한 기술 스택: 각 컴포넌트에 가장 적합한 기술과 버전을 자유롭게 선택하고 개별적으로 업데이트할 수 있습니다.
결론: 독립적인 문제 영역을 설정하여, 컨테이너가 복제되어도 부작용이 없는 스택으로 설계해야 합니다.
성공적인 컨테이너 아키텍처의 핵심은 프로세스의 개수가 아닌 '관심사'의 분리에 있습니다. 각 컨테이너가 독립적인 문제 영역에만 집중하도록 설계함으로써, 복제나 변경 시에도 부작용 없이 예측 가능하고 안정적인 시스템을 구축할 수 있습니다.