탐구 생활/개발 탐구

하나의 컨테이너에 하나의 프로세스

개발프로브 2025. 7. 9. 10:04

종종 하나의 Docker 컨테이너에 2개 이상의 프로세스를 Entrypoint의 Script 를 통해 실행하는 경우를 봅니다. 그리고 이러한 행태가 "전혀 문제 없는 방식" 이라고 말하는 사람도 만납니다. 이런 방식이 본질적으로 잘못된게 맞습니다. 어떤 점에서 잘못됐는지 알아보겠습니다.


문제점

gRPC 와 FastAPI 를 하나의 컨테이너에서 운영하는 경우를 예시로 들겠습니다.

1. 운영 리스크를 키운다.

서로 다른 프로세스 두 개를 하나의 컨테이너에서 운영하는 것은 운영 측면에서 리스크를 키웁니다.

  • 프로세스 생명주기: Docker는 PID 1 하나만 관리합니다. 추가로 띄운 자식 프로세스가 신호(SIGTERM/SIGINT)를 못 받아 좀비 프로세스가 남거나, graceful-shutdown 없이 강제 종료될 수 있습니다.
  • 장애의 전체 전파: 하나의 컨테이너에서 gRPC 와 FastAPI 를 모두 구동한다면 gRPC 서버만 죽어도 FastAPI까지 함께 재시작하는 현상이 일어납니다. 각각 내부/외부 담당하는 소통 영역이 다르지만 같은 생명주기를 갖게 되므로 부분 장애가 전체 전파로 이어지게 됩니다.
  • 헬스체크 불가분성: 서로 다른 2개 프로세스에 대해 Liveness/Readiness probe 를 설정해야 하므로 설정이 복잡해지고 실제 K8s 에서 probe 를 설정할때 하나의 Pod에 대해서 gRPC HealthCheck 와 HTTP HealthCheck 를 함께 설정할 수 없어서 서비스 품질 관리에 문제가 발생합니다.
  • 로그/모니터링 혼선: 두 프로세스의 STDOUT/STDERR 스트림이 섞여서 수집되므로 로그스트림이 일관된 정보를 담기 어렵습니다.
  • 리소스 경합: CPU/메모리 제한을 하나로 묶으므로, 트래픽 패턴이 다른 두 서비스가 서로 스로틀링을 일으킬 수 있습니다.

2. 컨테이너의 철학에 위배된다.

기본적으로 컨테이너라는 개념이 왜 등장했는지 그리고  무엇을 추구하며 발전해왔는지 다시 생각해볼 필요가 있습니다.

  • Single Responsibility: Docker 공식 가이드에 따르면 “한 컨테이너에는 한 서비스를 넣어 관심사를 분리하라” 라고 합니다. 이 지침에 따라 컨테이너를 작고 예측 가능하게 유지해야 이식성과 디버깅, 배포 이점이 극대화됩니다.
  • 변경 단위 최소화: 컨테이너 이미지는 불변(immutable) 아티팩트여야 합니다. 하나의 이미지에 여러 바이너리를 번들하면, 한 컴포넌트 수정이 있을 때마다 전체 이미지를 재빌드, 재배포해야 합니다.

3. DevOps 를 복잡하게 만든다.

클라우드 환경에서 컨테이너를 활용한 CI/CD 그리고 가시성확보가 보편화되었습니다. 그런데 기본적인 컨테이너의 철학을 깨면 암묵적인 룰을 깨트리는 것이죠.

  • 독립 스케일링 불가: HPA(Kubernetes Horizontal Pod Autoscaler)를 프로메트릭스(QPS, CPU) 기준으로 튜닝할 때, FastAPI CPU 스파이크가 gRPC 컨테이너 증설을 강제하여 비용 상승을 초래합니다.
  • 배포 전략 제약: Blue/Green 배포, Canary 배포와 같이 군집마다 두 서비스가 동시에 교체하는 전략은 절반만 새 버전으로 돌려서 A/B 테스트하기 어렵습니다.
  • 관측 스택 복잡화: OpenTelemetry, Prometheus exporter를 둘 다 탑재하면 스크래핑 엔드포인트가 겹치거나 포트 충돌 위험이 있습니다. 이를 회피하기 위해서는 사이드카 컨테이너를 이용할 수 있는데, 굳이 그렇게까지 해야할까요?

몇가지 추가적인 이야기들

이러한 문제점을 이야기를 했을때 들었던 이야기도 공유할까 합니다. 

그럼 Sidecar Container 로 빼면 되는 건가요?

결국 하나의 컨테이너를 공유하고 있던 여러 프로세스들은 많은 경우 개념적으로 공통적이 많은 프로세스들입니다. 그러다보니 해당 프로세스를 개별 컨테이너로 분리하는것에 대해 이야기할때 "아 그럼 Sidecar 로 분리하면 되는건가" 라는 말을 들을 때가 있습니다. 둘을 완전히 분리하고 싶지 않은 것이지요.

 

이건 경우에 따라 다릅니다. 우선 K8s 의 Sidecar 란 주된 프로세스(Primary App)에 기능을 추가하거나 확장할때 사용되는 기능입니다.  그렇다면 하나의 컨테이너에서 돌아가던 프로세스가 로깅, 보안, 모니터링 등을 위한 장치였다면 Sidecar 로 분리하는게 합리적일 것입니다. 하지만 그냥 다른 서비스라면 Sidecar 로 분리하는것은 설계 철학에 위배되는 행위입니다. 그리고  설계 철학을 따르지 않으면 실질적인 문제가 따르게 되어 있습니다.

 

Sidecar 로 별도 서비스(Secondary App)를 배포하면 독립적인 스케일링이 안되며, 주된 프로세스(Primary App)과 Lifecycle 이 연동되어 버리고 보안경계가 흐려진다는 문제가 있습니다.

어차피 동일한 로직, DB 접근을 공유하는데 코드는 어떡하죠?

이런 질문은 아직 모듈화의 개념이 잡히지 않은 개발자에게 주로 듣게되는 것 같습니다. 아직은 Python 진영의 특성상 모듈화를 강조하지 않아서 그런지 Java 진영보다 Python 진영 개발자로부터 더 자주 듣게됩니다.

 

동일한 로직, DB 접근을 공유하는데 서비스가 나눠져있다면 로직과 DB 접근을 core 모듈(예를들어)로 분리하고 Application 혹은 Service 계층을 분리하여 2개의 서로다른 컨테이너로 말아올릴 수 있습니다. 

 

디렉터리 구조로 보자면 이런 식이죠. 이 코드는 core 에 있는 주요 로직을 apps 의 fastapi_app 과 grpc_app 이 공유하는 형태입니다.

├── apps
│   ├── fastapi_app
│   │   └── pyproject.toml
│   └── grpc_app
│       └── pyproject.toml
├── core
│   ├── pyproject.toml
│   └── src
│       └── core
├── Dockerfile.fastapi
├── Dockerfile.grpc
├── pyproject.toml
└── uv.lock

현실적인 회색지대

위의 문제점에도 불구하고 여러 프로세스를 하나의 컨테이너에서 운영하는 상황이 이해되는 경우가 있습니다. 바로 초기 PoC 단계의 비용 압박인데요. 여러 컨테이너를 돌릴 컴퓨팅 리소스는 제한적인 상황에서 빠르게 아이디어를 검증해봐야 한다면 하나의 컨테이너에서 여러 프로세스를 운용해볼 수 있을 것입니다.

 

실제 Docker 진영에서도 하나의 컨테이너에서 여러개의 프로세스를 구동하는 방법을 소개하고 있습니다. 하지만 해당 문서에서도 하나의 서비스가 하나의 컨테이너에 대응 되어야 한다는 것을 명확히 하고 있습니다. 즉, 가능하다는 거지 절대 위의 문제가 해소된다는 것이 아닙니다.

It's ok to have multiple processes, but to get the most benefit out of Docker, avoid one container being responsible for multiple aspects of your overall application.

참고

Baeldung, Why Is It Recommended to Run One Process per Container?

Couchbase, Docker Container Anti Pattern

K8s, Sidecar Containers
Docker Docs, Run multiple processes in a container