본문으로 건너뛰기

스택 띄우기: docker compose up 한 번으로

📍 현재 위치: 1부 2장 — 지난 장에서 설계도를 읽었으니, 이제 노트북 위에서 플랫폼 전체를 부팅합니다. 그래야 이후 모든 장에서 실제로 돌려볼 대상이 생깁니다.

쉽게 말하면

동반 저장소(companion repo)를 조립식 데이터 공장이라고 생각하세요. 데이터베이스, 메시지 브로커(message broker), 대시보드, 바이오리액터 시뮬레이터(bioreactor simulator) — 모든 기계가 저마다 밀봉된 상자(컨테이너, container)에 담겨 옵니다. 한 장의 설명서(compose.yaml)가 어떤 상자를 열지, 서로 어떻게 연결할지, 각각이 제대로 켜졌는지 어떻게 확인할지를 알려줍니다. 명령 하나를 입력하면 공장이 스스로 조립됩니다. 다른 명령 하나를 입력하면 다시 상자 안으로 접혀 들어가며, 여러분의 컴퓨터에 아무 흔적도 남기지 않습니다.

이 장에서 다루는 내용

이 장은 이 책의 실습 전환점입니다. 끝날 무렵이면 여러분은 저장소 하나를 클론(clone)하고, 실제로 작동하는 멀티서비스 바이오프로세스 데이터 플랫폼을 노트북 한 대 위에 띄워 놓게 됩니다. 우리는 다음을 합니다.

  • 핵심 스택 전체를 정의하는 단 하나의 compose.yaml 파일을 훑어보고, 각 서비스가 왜 거기 있는지 설명합니다.
  • 고정된 이미지 태그(pinned image tag)가 왜 중요한지 설명합니다 — influxdb:latest→v3 라이선스 함정이 그 교훈담입니다.
  • 이 책이 그대로 출력하는 명령 표면(command surface)인 Makefile을 실행합니다.
  • 첫 데이터 포인트 스모크 테스트(smoke test)로 스택이 살아 있음을 확인합니다.
  • 이 책 전체가 데이터를 공급받는 원천인 결정론적 CHO(Chinese Hamster Ovary, 중국 햄스터 난소 — 항체 의약품을 만드는 표준 포유류 세포주; 『바이오의약품 제조』 /cell-line-development 참고) 시뮬레이터를 만나봅니다.

아래의 모든 것은 examples/에 실제로 존재하며 직접 실행된 파일에서 나온 것입니다. 지어낸 플래그도, 지어낸 출력도 없습니다. historian(히스토리안), hypertable(하이퍼테이블), profile(프로파일), healthcheck(헬스체크) 같은 새 용어는 장 끝의 핵심 용어 목록에 모아 두었습니다 — 원한다면 먼저 훑어봐도 좋습니다.

파일 하나에 핵심 전부

현대적인 컨테이너 플랫폼은 일련의 서비스를 선언적으로(declaratively) 기술할 수 있게 해줍니다 — 각 서비스가 어떤 이미지를 실행하는지, 어떤 포트를 노출하는지, 어떤 볼륨(volume)을 마운트하는지, 정상 상태인지를 어떻게 판단하는지 — 그리고 단일 명령으로 이 모두를 띄웁니다 [1]. 그 기술서는 정식 산출물입니다. Compose 명세(Compose Specification)가 서비스, 네트워크, 볼륨의 스키마를 정의하므로, 같은 YAML이 여러분의 컴퓨터에서도, 동료의 컴퓨터에서도, CI 러너(CI runner — 변경이 있을 때마다 스택 전체를 자동으로 다시 실행하는 지속적 통합(continuous-integration) 서버)에서도 동일하게 동작합니다 [2].

다음은 examples/platform/compose/compose.yaml에 있는 실제 파일의 윗부분입니다(capture 줄의 약어들은 처음 읽기 좋도록 여기서는 풀어 적었습니다. 디스크에 있는 주석은 한 줄에 들어가도록 간결하게 유지합니다).

# compose.yaml — the base stack for "Open-Source Bioprocess Data Systems".
# One file defines every service; Docker Compose PROFILES gate what comes up so a
# reader only pays for the chapter they are on:
# core Ch 1-2, 4-6, 16-18 (db + broker + dashboards; the CHO simulator is a
# separate Python package run via `make data`)
# capture Ch 3, 7-15 (the OPC UA server + collector — the OSS (open-source)
# <-> DCS (plant distributed control system) mirror)
# semantics Ch 19 (triplestore)
# commercial Ch 20-22 (PI, LIMS, DeltaV mocks — laptop-unrunnable systems)
# trust Ch 23-24 (identity, signing, object store)
# analytics Ch 29 (notebooks, model tracking)
# Bring up just the foundation with: docker compose --profile core up -d
#
# Images are pinned by tag for reproducibility; the matching manifest digests are
# recorded in versions.lock (revisited in the supply-chain chapter, Ch 25).

name: bioprocess-data-stack

핵심 설계 아이디어는 두꺼운 공유 플랫폼 위의 얇은 장(chapter)들입니다. 이 책의 모든 서비스는 이 파일 하나에 정확히 한 번만 선언되고, Compose 프로파일(profile)(core, capture, semantics, commercial, trust, analytics. trust는 파일 헤더 주석에 문서화되어 있을 뿐, 아직 살아 있는 서비스 블록이 담지 않습니다)로 태그가 붙습니다. docker compose --profile core up은 1부 청사진 장들에 필요한 것만 — 대략 3 GB의 RAM 정도 — 시작하고, 이후 각 부(Part)에서 프로파일을 하나씩 더 켭니다. 스택을 다시 선언하는 일은 결코 없으며, 그저 프로파일을 켜기만 하면 됩니다. 여러분 노트북의 메모리와 CPU 사용량은 지금 실제로 읽고 있는 장에 맞춰 늘어납니다.

core 프로파일은 항상 켜져 있는 기반입니다. examples/platform/compose/compose.yaml에 담긴 한 가지 의도적 선택에 주목하세요.

postgres:
# timescale/timescaledb IS PostgreSQL + TimescaleDB, so the historian
# hypertable and the ISA-88/95 batch model live in one joinable database.
image: timescale/timescaledb:2.17.2-pg17
profiles: ["core"]
<<: *restart
environment:
POSTGRES_USER: ${POSTGRES_USER:-bioproc}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-bioproc}
POSTGRES_DB: ${POSTGRES_DB:-bioproc}
ports: ["5432:5432"]
volumes:
- pgdata:/var/lib/postgresql/data
- ../db:/docker-entrypoint-initdb.d:ro # 00-60 schema files run on first init
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bioproc} -d ${POSTGRES_DB:-bioproc}"]
interval: 5s
timeout: 5s
retries: 20

별도의 "시계열 데이터베이스(time-series database)" 컨테이너는 없습니다. timescale/timescaledb 이미지는 그 자체로 PostgreSQL [3]이며, TimescaleDB 확장(extension)이 이미 설치되어 있습니다 [4]. 이 단 하나의 결정이 이 책의 나머지 내내 빛을 발합니다. 고속의 센서 이력과 ISA-88/95 배치 모델(배치를 장비에서 실행되는 레시피로 기술하는 ISA 표준 방식 — 88은 배치/레시피 모델, 95는 장비/기업 계층; 다음 장에서 구축됩니다)이 같은 데이터베이스 안에 살기 때문에, 하나의 쿼리로 "용존 산소(dissolved oxygen) 프로브가 14:32에 읽은 값"과 "어떤 배치와 레시피 단계(recipe phase)가 실행 중이었는지"를 시스템 간 데이터 복사 없이 조인(join)할 수 있습니다. 문맥화(contextualization) 장에서 우리는 이 조인에 크게 기댈 것입니다.

그 블록에는 제 몫을 하는 두 가지 세부 사항이 더 있습니다. volumes 줄은 ../db를 PostgreSQL의 첫 부팅 초기화 디렉터리에 마운트하므로, 번호가 매겨진 스키마 파일(00-init.sql부터 60-views.sql까지)이 데이터베이스가 처음 시작될 때 자동으로 실행됩니다 — 스키마가 수동 단계가 아니라 코드인 것입니다. 그리고 healthcheck는 5초마다 pg_isready를 실행하여, 플랫폼이 데이터베이스가 언제 연결을 받아들일 준비가 되었는지를 추측이 아니라 확실히 알 수 있게 합니다. 헬스체크(healthcheck)는 이후 장의 테스트가 실행 전에 깨끗한 의존성을 기다리는 방법입니다.

Compose 서비스 정의의 해부

postgres: 블록은 특별한 경우가 아닙니다 — 그것은 이 책 전체의 대표 산출물입니다. compose.yaml의 모든 서비스는 — mosquitto부터 victoriametrics, 그리고 캡처 장에서 만나는 OPC UA 컬렉터까지 — 똑같은 여덟 개 남짓의 필드를 다르게 채운 것일 뿐입니다. 하나를 읽을 줄 알면 전부 읽을 수 있습니다. 그러니 이 하나를 필드별로 해부해 볼 가치가 있습니다. 각 줄은 플랫폼이 서비스를 안전하게 실행하기 전에 답해야 하는 서로 다른 질문에 답하기 때문입니다.

postgres Compose 서비스 정의를 필드별로 해부한 신원 카드: 고정된 timescale/timescaledb 태그와 그 versions.lock 다이제스트를 보여주는 녹색 강조 image 필드, 이어서 profiles 게이트, restart YAML 앵커 병합, environment 기본값 치환, 호스트-컨테이너 포트 매핑, 두 개의 volumes(이름 붙은 볼륨과 읽기 전용 init 바인드 마운트)에 대한 행들, 그리고 interval·timeout·retries와 함께 pg_isready 프로브가 서비스를 healthy로 뒤집는 모습을 보여주는 보라색 healthcheck 패널. 이 책의 모든 서비스는 필드를 다르게 채운 이 카드 한 장입니다. 녹색 블록은 image(지금은 태그, 잠금 파일에는 다이제스트)이고, 보라색 블록은 의존 서비스가 "시작됨"이 아니라 "준비됨"을 기다리게 해주는 healthcheck입니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

카드를 위에서 아래로 읽으면 다음과 같습니다.

  • image — 유일한 필수 필드이자, 재현성 논증 전체가 매달리는 줄입니다. timescale/timescaledb:2.17.2-pg17은 저장소(repository)와 고정된 MAJOR.MINOR.PATCH 태그를 지정하며, 동반 파일 versions.lock은 그에 대응하는 불변의 sha256: 매니페스트 다이제스트(sha256:3324f81c…)를 기록하여 사람이 읽는 파일과 콘텐츠 주소 지정된 진실이 어긋나지 않게 합니다. 태그 대 다이제스트는 아래에서 따로 해부합니다.
  • profiles: ["core"] — 게이트입니다. 이 서비스는 docker compose --profile core up에서만 시작되며, 다른 프로파일을 켜면 휴면 상태로 남습니다. "두꺼운 플랫폼 위의 얇은 장"을 작동하게 하는 바로 그 한 줄입니다.
  • <<: *restart — 파일 상단에 한 번 정의된 공유 &restart 앵커(restart: unless-stopped)의 YAML 병합입니다. 한 곳에 선언되어 모든 서비스에 병합되므로, 재시작 정책이 서비스 간에 결코 어긋날 수 없습니다.
  • environment — 환경 변수로 주입되는 설정입니다. ${POSTGRES_USER:-bioproc} 구문은 기본값 치환(default-substitution)입니다. .env 파일이나 셸에 값이 설정되어 있으면 그것을 쓰고, 없으면 bioproc으로 되돌아갑니다. 스택은 .env 없이도 바로 실행되지만, 모든 자격 증명은 실제 배포를 위해 재정의할 수 있습니다.
  • ports: ["5432:5432"]호스트:컨테이너 매핑입니다. 왼쪽 숫자는 여러분 노트북의 포트이고, 오른쪽은 컨테이너 내부의 포트입니다. 그래서 psql … -h localhost -p 5432가 Docker 네트워크 바깥에서 PostgreSQL에 닿습니다.
  • volumes — 두 가지 다른 종류의 볼륨 두 개이며, 이것이 놓치기 쉬운 미묘한 지점입니다. pgdata:/var/lib/postgresql/data이름 붙은 볼륨(named volume)입니다. Docker가 관리하는 저장소로 make down 후에도 살아남습니다(그래서 재시작 후에도 데이터가 남아 있고, 진짜로 지우는 것은 make clean-v입니다). ../db:/docker-entrypoint-initdb.d:ro는 호스트 디렉터리를 PostgreSQL의 첫 부팅 init 폴더에 넣는 읽기 전용 바인드 마운트(bind mount)로, 첫 시작 시 0060 스키마 파일을 자동 실행하는 메커니즘입니다.
  • healthcheck — "컨테이너가 실행 중"과 "서비스가 준비됨"을 가르는 필드로, 바로 다음 절에서 따로 해부합니다.

헬스체크 읽기: 스택은 준비됨을 어떻게 아는가

컨테이너는 내부 프로그램이 유용한 일을 할 수 있게 되기 한참 전에 실행 중일 수 있습니다. PostgreSQL은 연결을 받아들이기 전에 기동하고, 미리 쓰기 로그(write-ahead log)를 재생하고, 소켓을 열어야 합니다. healthcheck는 Compose가 그 간극을 메우는 방법입니다 — 프로브 명령을 시계에 맞춰 실행하고, 프로브가 성공할 때만 서비스를 started에서 healthy로 뒤집습니다. postgres 블록은 네 개의 하위 필드를 선언합니다.

  • test — 프로브 자체: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bioproc} -d ${POSTGRES_DB:-bioproc}"]. pg_isready는 PostgreSQL 자체의 준비 상태 유틸리티이며 [3], 서버가 연결을 받아들일 때만 종료 코드 0을 반환합니다. CMD-SHELL은 문자열을 셸을 통해 실행하므로 ${…} 치환과 플래그가 적힌 대로 동작합니다.
  • interval: 5s — 5초마다 프로브를 다시 실행합니다.
  • timeout: 5s — 이보다 오래 걸리는 단일 프로브는 실패로 간주합니다.
  • retries: 20 — 서비스를 unhealthy로 선언하기 전에 최대 스무 번 연속 실패를 허용합니다(느린 첫 부팅은 견디되, 정말로 죽은 데이터베이스는 결국 잡아냅니다).

그 대가는 grafana 서비스의 depends_on: { postgres: { condition: service_healthy } }입니다. Grafana는 단지 Postgres 컨테이너가 존재하기를 기다리는 것이 아니라, 그 헬스체크가 통과하기를 기다립니다. 헬스체크는 계약이고, depends_on은 그 계약의 소비자입니다. 이후 모든 장의 테스트 묶음은, 기동을 마치지 못한 데이터베이스에 쿼리하는 고전적인 불안정함(flake)을 피하기 위해 같은 계약에 기댑니다.

core의 나머지는 브로커, 대시보드, 그리고 다른 프로파일 아래에 얹혀 가는 트리플스토어/메트릭 한 쌍입니다.

mosquitto:
image: eclipse-mosquitto:2.0.22
profiles: ["core"]
<<: *restart
ports: ["1883:1883"]
volumes:
- ../mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
healthcheck:
test: ["CMD-SHELL", "mosquitto_sub -t '$$SYS/#' -C 1 -W 3 -h localhost || exit 1"]
interval: 10s
timeout: 5s
retries: 10

grafana:
image: grafana/grafana-oss:11.4.0
profiles: ["core"]
<<: *restart
ports: ["3000:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ../dashboards/provisioning:/etc/grafana/provisioning:ro
- grafana:/var/lib/grafana
depends_on:
postgres:
condition: service_healthy

mosquitto는 MQTT(Message Queuing Telemetry Transport) 브로커입니다 — 생산자가 이름 붙은 토픽(topic)에 판독값을 발행(publish)하고 소비자가 이를 구독(subscribe)하는 경량 메시지 버스로, 캡처(capture) 장에서 시뮬레이터 텔레메트리(telemetry)가 흘러갈 경로입니다 [5]. grafana-oss는 히스토리안(historian)을 쿼리하여 이 책이 만드는 배치 오버레이(batch-overlay) 차트와 골든 배치(golden-batch) 차트를 그리는 대시보드 계층입니다 [6]. grafanapostgres에 대해 condition: service_healthydepends_on을 선언한 점에 주목하세요. 데이터베이스가 헬스체크를 통과하기 전까지 Grafana는 그리기를 시작하지 않습니다. 두 서비스 fuseki(지식 그래프 트리플스토어, knowledge-graph triplestore — 사실을 주어-술어-목적어 트리플로 저장하는 데이터베이스이며 19장에서 구축됩니다)와 victoriametrics(스택 자체 모니터링 — 스택 자신의 메트릭을 기록합니다)는 semanticsanalytics 프로파일 뒤에 자리하며, 해당 부에 이르기 전까지는 휴면 상태로 있습니다. 헤더 주석은 analytics 프로파일에 notebooks, model tracking을 그 부의 지향으로 적어 두었지만, 실제로 제공되는 스택에는 그 프로파일에 VictoriaMetrics만 담겨 있습니다. Jupyter와 MLflow 기반의 서비스 경로는 전체 저장소(full repo)에 있으며, 분석 장이 이를 정직하게 짚어 줍니다.

DCS — 공장의 분산 제어 시스템(distributed control system) — 를 와이어 너머로 미러링하는 OPC UA 서버와 OPC UA 컬렉터는 core 프로파일에 없습니다. 이들은 나중에 capture 프로파일과 함께, 정확히 같은 방식으로 켜집니다. 요점은, 지금 여러분이 읽고 있는 이 파일이 이미 그들의 자리를 선언하고 있다는 것입니다. (Telegraf [7]나 Node-RED [8] 같은 현장 게이트웨이 도구는 엣지 게이트웨이(edge-gateway) 장에서 소개되며, 이 compose 파일의 서비스가 아닙니다.)

오픈소스 바이오공정 데이터 시스템 핵심 스택의 계층형 다이어그램: 단일 compose.yaml이 프로파일로 게이팅된 컨테이너 — PostgreSQL+TimescaleDB, Mosquitto, Grafana — 를 띄우고, Docker 네트워크로 연결하며, CHO 시뮬레이터는 별도의 Python 패키지로서 데이터셋을 안으로 공급하고, make 타깃이 data, up, seed, load 및 스모크 테스트를 구동한다.

하나의 Compose 파일이 고정되고 헬스체크된 몇 개의 컨테이너로 펼쳐지고, Makefile은 여러분이 입력하는 유일한 명령 표면이며, 프로파일이 공장의 어느 만큼에 전원을 넣을지 결정합니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

왜 고정된 태그가 중요한가 (latest 함정)

모든 image: 줄을 다시 보세요. timescale/timescaledb:2.17.2-pg17, eclipse-mosquitto:2.0.22, grafana/grafana-oss:11.4.0. 그 어느 것도 :latest라고 적혀 있지 않습니다. 이것은 까다로움이 아닙니다. 재현 가능한 공장과 시한폭탄 사이의 차이입니다.

컨테이너는 OCI 이미지로 배포됩니다 — 콘텐츠 주소 지정(content-addressable) 매니페스트와 레이어들로 이루어지며, 불변의 다이제스트(immutable digest)로 식별됩니다 [9]. 2.17.2-pg17 같은 태그(tag)는 그런 다이제스트 하나를 가리키는 친근한 라벨이고, latest 같은 태그는 메인테이너(maintainer)가 가장 최근에 푸시한 무엇이든을 가리킵니다. 시맨틱 버저닝(semantic versioning)이 태그에 의미를 부여합니다. MAJOR.MINOR.PATCH이며, MAJOR가 올라가면 호환성을 깨는 변경(breaking change)을 알립니다 [10]. 버전을 고정하면, 업그레이드가 무엇을 깨고 무엇을 깨지 않을지 추론할 수 있습니다.

versions.lock 한 줄의 해부: 태그 대 다이제스트

고정 논증 전체는 platform/versions.lock의 한 줄 안에 살며, 그 줄은 머릿속에서 떼어둘 만한 정확히 두 절반으로 이루어집니다.

versions.lock 한 줄을 대조한 신원 카드: 위쪽에 가변적이고 재지정 가능한 태그 timescale/timescaledb:2.17.2-pg17을 담은 녹색 블록, 아래에 불변의 콘텐츠 주소 지정 sha256 다이제스트를 담은 슬레이트 블록, 그리고 같은 influxdb 이름이 InfluxDB 2 다이제스트에서 InfluxDB 3 재작성판으로 조용히 재지정되는 colon-latest 함정과 그 해법(태그를 고정하고 다이제스트를 기록)을 보여주는 장미색 패널. versions.lock 한 줄은 <image:tag> sha256:<digest>입니다. 태그는 재지정할 수 있는 라벨이고 다이제스트는 콘텐츠 그 자체입니다. :latest는 라벨은 그대로 둔 채 그 아래의 콘텐츠를 바꿔치기합니다. 저자가 AI의 도움을 받아 직접 제작한 그림입니다.

  • 태그(tag) — timescale/timescaledb:2.17.2-pg17 — 는 가변적이고 재지정 가능한 라벨입니다. 사람이 읽을 수 있고 시맨틱 버전의 의미를 담고 있는데, 바로 그래서 compose.yaml이 태그로 작성됩니다. 파일은 읽기 쉽게 유지되고 라이선스 표는 이름으로 줄이 맞습니다. 하지만 태그는 이름일 뿐이며, 메인테이너는 내일 그것을 새 이미지로 재지정할 수 있습니다.
  • 다이제스트(digest) — sha256:3324f81c… — 는 불변의 콘텐츠입니다. 이미지 매니페스트 자체의 해시이므로, 다른 다이제스트가 되지 않고는 바뀔 수 없습니다. 다이제스트로 고정하면(image: <repo>@sha256:…) 바이트 단위로 정확한 이미지를 얻거나 오류가 나며 — 결코 놀랄 일이 없습니다.

versions.lock은 모든 핵심 이미지(TimescaleDB, Mosquitto, Grafana, VictoriaMetrics)에 대해 태그→다이제스트 쌍을 기록하여, 읽기 쉬운 파일과 콘텐츠 주소 지정된 진실을 나란히 적어두고 서로 대조할 수 있게 합니다. 이 잠금 파일은 심지어 커뮤니티 저장소가 이동한 이미지 하나를 표시하여, 여러분이 풀(pull)하는 컨테이너 레지스트리(이미지 호스트 — 예: Docker Hub 또는 조직의 미러)에서 실제 sha256: 다이제스트를 다시 가져와 기록할 때까지 그 다이제스트를 VERIFY-BEFORE-USE로 남겨둡니다 — "같은 태그"가 "같은 바이트"의 보장이 아니라는 작지만 정직한 상기입니다.

:latest가 조용한 시한폭탄인가 (현장 기록)

대표적인 공포담은 InfluxDB입니다. 2024년에 influxdb:latest라고 적었던 독자는 어느 날 아침 InfluxDB 3를 내려받게 되었습니다 — 저장 엔진이 바뀌고 라이선스 태도까지 바뀐 거의 전면 재작성판이, 아무 경고도 없이 조용히, 그 자리에서요. 이 책은 이런 부류의 사고 전체를 비켜갑니다. InfluxDB를 피하고(대신 Apache-2.0 라이선스의 VictoriaMetrics를 제공합니다), 더 중요하게는 모든 것을 고정함으로써 말입니다. compose.yaml은 각 이미지를 사람이 읽을 수 있는 태그로 고정하고, 동반 파일 platform/versions.lock은 각각에 대응하는 불변 매니페스트 다이제스트(<image:tag> sha256:…)를 기록합니다. 공급망(supply-chain) 장(25장)은 이 잠금 파일을 바탕으로 실행 중인 스택, 라이선스 인벤토리, 공급자 등록부를 하나의 고정된 목록에 대조하여 — 이들이 조용히 어긋나지 않도록 합니다.

그 일화는 일회성이 아니라, 문서화된 부류의 실패입니다. 컨테이너 보안 연구자들은 재지정 가능한 태그를 가변(mutable, "mutant") 태그라고 부르며, :latest를 스타일상의 트집이 아니라 살아 있는 위협으로 취급합니다. 태그는 언제든 다른 다이제스트로 재지정될 수 있기 때문에, 여러분이 스캔하고 승인한 이미지와 실제로 내려받혀 실행되는 이미지가 같은 바이트일 필요가 없습니다 — 스캔이 통과한 뒤 깨끗한 태그가 악성 이미지로 바꿔치기되는 입장 제어(admission-control) 우회로 실증된 간극입니다 [13]. InfluxDB 바꿔치기는 바로 그 메커니즘의 무해해 보이는 판본입니다. 같은 이름, 새 콘텐츠, 경고 없음. 같은 연구자들이 권하는 방어 자세는 바로 이 스택이 하는 것입니다 — 태그가 해석되는 다이제스트를 검증하여 "같은 태그"가 "같은 이미지"를 뜻하도록 강제하는 것입니다 [13]. 고정된 태그에 기록된 다이제스트를 더하면 "라벨을 믿는다"가 "바이트를 증명할 수 있다"로 바뀝니다.

버전을 고를 때 짚어둘 만한 2026년의 라이선스 함정 몇 가지가 있습니다. 조용히 물기 때문입니다. TimescaleDB는 핵심부 — 하이퍼테이블, time_bucket, 그리고 오래된 시간 청크(chunk)를 삭제하는 데이터 보존 명령 drop_chunks — 를 관대한 오픈소스 라이선스인 Apache-2.0으로 제공합니다. 그러나 이 스택이 실제로 사용하는 연속 집계(continuous aggregate — 사전 계산된 롤링 요약)와 add_retention_policy 자동화는 무료 TSL Community 기능(Timescale License — 소스가 공개되어 있고 무료로 실행할 수 있지만 OSI 승인 오픈소스 라이선스는 아님)이며, 반면 Hypercore 컬럼스토어/압축과 HA(high availability, 고가용성)는 우리가 의도적으로 피하는 라이선스 TSL 계층입니다. 따라서 엄격히 Apache-2.0만 쓰는 빌드라면 직접 cron으로 구동하는 drop_chunks를 예약해야 합니다(16장). Grafana는 AGPL-3.0입니다 — 로컬에서 실행하는 것은 전혀 문제없지만, 재배포하거나 타인을 위한 서비스로 호스팅하면 소스 공개 의무가 발생합니다. InfluxDB v3, EMQX의 BSL(Business Source License), Redpanda의 RCL(Redpanda Community License) — 모두 상업적 또는 호스팅 사용을 제한하는 소스 공개 라이선스 — 은 이 스택이 의도적으로 피해 가는 다른 지뢰들입니다.

Makefile이 명령 표면이다

이 책에서 여러분은 날것의 docker compose 주문을 결코 입력하지 않을 것입니다. 모든 동작은 make를 거치며, 책은 여러분이 입력하는 그대로를 출력합니다. 다음은 실제 examples/Makefile입니다.

COMPOSE := docker compose -f platform/compose/compose.yaml
PY := sim/.venv/bin/python
export DATABASE_URL ?= postgresql://bioproc:bioproc@localhost:5432/bioproc

.DEFAULT_GOAL := help
.PHONY: help venv up down seed data load contextualize alcoa soft-sensor test clean

help: ## list targets
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " %-14s %s\n", $$1, $$2}'

venv: ## create the Python env and install the simulator (uv)
cd sim && uv venv --python 3.12 .venv && uv pip install --python .venv -e . "psycopg[binary]" "asyncua==2.0" scikit-learn

up: ## bring up the core stack (postgres+timescale, mosquitto, grafana)
$(COMPOSE) --profile core up -d
@echo "waiting for postgres..." && sleep 3
@until docker exec bioprocess-data-stack-postgres-1 pg_isready -U bioproc >/dev/null 2>&1; do sleep 2; done
@echo "core stack up."

venv 타깃은 빠른 Python 패키지·가상환경 관리자인 uv로 Python 환경을 구축합니다(이후의 (uv) 표시도 같은 도구를 가리킵니다). make help## 주석으로부터 스스로 문서화하므로, 메뉴와 코드가 결코 어긋나지 않습니다. make upcore 프로파일을 띄운 다음 pg_isready가 성공할 때까지 블록(block)되므로, 데이터베이스가 실제로 연결을 받을 수 있게 되기 전까지는 "완료"를 반환하지 않습니다. 그 폴링 루프(polling loop)는 "컨테이너가 시작되었다"와 "서비스가 준비되었다" 사이의 작지만 정직한 차이입니다.

기동 핸드셰이크: depends_on, 조건, 그리고 make up 폴링 루프

준비 상태 계약을 처음부터 끝까지 따라가 볼 가치가 있습니다. 그 계약은 같은 사실에 모두 동의하는 세 계층을 관통하기 때문입니다.

  1. 서비스 내부에서postgres 자신의 healthcheck가 5초마다 pg_isready를 실행하고, 통과하면 서비스를 healthy로 뒤집습니다(위 절).
  2. 서비스 사이에서grafanadepends_on: { postgres: { condition: service_healthy } }가 그 플래그를 읽으므로, Compose는 데이터베이스가 단지 존재하는 것이 아니라 진짜로 연결을 받아들일 때까지 Grafana를 붙잡아 둡니다. condition이 없으면 depends_on컨테이너가 생성되기만을 기다리는 — "첫 부팅 시 connection refused" 불안정함을 일으키는 더 약한 약속이 됩니다.
  3. 명령 표면에서make up은 어느 계층도 맹목적으로 믿지 않습니다. docker compose --profile core up -d가 반환된 뒤, 자체적으로 until docker exec … pg_isready …; do sleep 2; done 루프를 돌리고 프로브가 응답할 때만 core stack up.을 출력합니다. 그래서 셸 명령, 서비스 간 의존성, 컨테이너 내부 프로브가 모두 같은 pg_isready 진실에 게이팅됩니다.

이렇게 이중·삼중으로 거는 이유는 세 계층이 서로 다른 소비자를 보호하기 때문입니다. 컨테이너 내부 검사는 Compose 자체의 스케줄링을 보호하고, depends_on 조건은 형제 서비스를 보호하며, make 루프는 여러분 — 준비되어 있어야 마땅한 데이터베이스에 대해 곧 make seed를 입력할 사람(또는 CI 러너) — 을 보호합니다. 프로브 하나, 가드 셋.

기반을 위한 전체 빌드 순서는 짧습니다.

make venv # Python env + the simulator (uv)
make data # generate every dataset deterministically + MANIFEST.sha256
make up # bring up the core stack (postgres+timescale, mosquitto, grafana)
make seed # load the ISA-88/95 reference CHO line
make load # load the datasets into the historian + lab tables

make load는 풀 해상도 parquet을 TRUNCATE한 뒤 대량 COPY하므로 멱등적(idempotent)입니다 — 다시 실행하면 골든 배치를 중복 적재하는 것이 아니라 다시 적재합니다. make down은 스택을 멈추되 데이터를 이름 붙은 볼륨(named volume)에 보존합니다. 진짜 깨끗한 새 출발을 원할 때는 make cleandocker compose down -v를 실행하여 볼륨을 삭제합니다. 전체 환경이 선언적 파일 하나에 명령 하나이므로, 이를 허물어도 여러분의 컴퓨터 곳곳에 흩어진 흔적이 남지 않습니다.

첫 데이터 포인트: 스모크 테스트

서비스를 띄우는 것과, 플랫폼이 처음부터 끝까지 작동함을 증명하는 것은 같지 않습니다. 이 스택의 스모크 테스트는 가능한 한 가장 단순한 질문입니다. 숫자 하나가 히스토리안에 안착하고, 배치에 조인되어 되돌아올 수 있는가?

make up && make seed && make load 이후, 시뮬레이터의 데이터셋이 PostgreSQL+TimescaleDB 안에 들어옵니다. 첫 번째 온전성 검사(sanity check)는 히스토리안 하이퍼테이블(hypertable)에 직접 들어온 것을 세어보는 것입니다.

docker exec -e PGPASSWORD=bioproc bioprocess-data-stack-postgres-1 \
psql -U bioproc -d bioproc \
-c "select tag, count(*), round(min(value)::numeric,2) lo, round(max(value)::numeric,2) hi \
from ts.sensor_reading where batch_id='BATCH-2026-001' group by tag order by tag limit 4;"
tag | count | lo | hi
---------------+-------+-------+-------
BR101.DO.PV | 20160 | 30.04 | 43.77
BR101.Temp.PV | 20160 | 36.36 | 37.12
BR101.Titer.PV| 20160 | -0.11 | 5.82
BR101.pH.PV | 20160 | 6.91 | 7.08

BR101.DO.PV 같은 태그는 장비.측정.역할로 읽습니다 — 바이오리액터 BR101, 그것의 용존 산소(DO) 프로브가, 설정값(SP)이 아니라 공정값(PV, process value)을 보고한다는 뜻입니다. 역가, 생존 세포 밀도, 생존율, lactate, 삼투압, 유가식 급이 같은 배양 용어는 『바이오의약품 제조』(/production-bioreactor)에서 설명합니다. 여기서는 그 숫자들이 현실적이라는 점만 알면 됩니다. 이 범위들은 유가식(fed-batch) 공정이 스스로에 대해 진실을 말하는 것입니다. 온도는 37 °C 근처를 유지했고, pH는 대략 6.9–7.1을 오갔으며, 용존 산소는 약 30–44 %sat 사이를 탔고, 역가(titer — 배양액 속 항체 산물의 농도, 단위는 그램/리터)는 사실상 0(접종 시점의 약간 음수인 측정값으로, 0에 가까운 온라인 역가 신호의 측정 노이즈에서 비롯됩니다)에서 시작해 한 회분(run) 동안 약 6 g/L까지 올라갔습니다. 이들은 특징적인 CHO 설정값입니다 — DO는 공기 포화도의 약 30–50%로 유지되고, pH는 7.0 근처, 온도는 생리적 37 °C입니다 — 그리고 이 회분은 건강한 14일 배치가 끝나야 할 방식으로 끝납니다. 생존 세포 밀도가 약 2천만 cells/mL 부근에서 정점을 찍고, 생존율은 약 96%에서 약 64%로 미끄러지며(유가식 후기에 영양분이 고갈되면서 세포가 자연스럽게 노화하고 죽기 때문 — 예상되는, 건강한 회분 종료 프로파일입니다), lactate는 몇 g/L까지 누적되고 삼투압(osmolality)은 340 mOsm/kg를 넘어 올라갑니다(모두 오프라인 분석 데이터셋에서 확인할 수 있습니다). 각 태그는 20,160개의 행을 가집니다 — 14일 배치 전체에 걸쳐 1분에 하나씩입니다(make load가 적재하는 풀 해상도 fedbatch_timeseries.parquet이며, datasets/에는 파일 재생(file-replay) 장을 위한 10분 간격 CSV 다운샘플도 함께 들어 있습니다).

그러나 진짜 스모크 테스트는 조인입니다. make contextualize(문맥화 장에서 제대로 만듭니다)는 같은 스택에 대해 정확히 이 쿼리를 실행합니다.

select phase_name, count(*) n, round(avg(value)::numeric,1) avg_DO
from s88.v_batch_sensor where batch_id='BATCH-2026-001' and tag='BR101.DO.PV'
group by phase_name order by min(ts);

이것이 레시피 단계(phase)별로 나뉜 용존 산소 평균을 반환한다면, 플랫폼은 중요한 의미에서 살아 있는 것입니다. 히스토리안에 캡처된 날것의 센서 값이, 하나의 쿼리 안에서 자신의 ISA-88 공정 문맥과 다시 결합된 것입니다. 그것이 곧 축소판 플랫폼 전체이며, 이 책의 나머지가 그 위에 세워지는 증거입니다.

batch_id 조인은 또한 이 관계형 스택이 나중에 시맨틱 스택이 되는 이음매이기도 합니다. SQL 뷰가 반환하는 그 사실 — 이 판독값은 이 배치에 속한다 — 은, 지식 그래프 장에서 하나의 RDF(Resource Description Framework — 사실을 주어-술어-목적어 트리플로 적는 표준 방식) 트리플 BATCH-2026-001 derivedFrom SEED-001입니다. SQL이 외래 키로 조정하는 히스토리안 값과 배치 문맥을, RDF는 재귀적으로 걸어갈 수 있는 일급(first-class) 엣지로 저장하며, 바로 그것이 단일 SPARQL 쿼리로 한 로트의 전체 계보를 셀 뱅크까지 거슬러 추적하게 해줍니다. 그 번역과, semantics 프로파일이 자리를 예약해 둔 Apache Jena Fuseki 트리플스토어는 시맨틱·지식 그래프 장에서 구축되고, 그 배후의 더 넓은 온톨로지 엔지니어링 — 역량 질문(competency question), 클래스와 관계, SHACL 릴리스 게이트 — 은 4권의 주제입니다(클래스와 분류 체계, 관계와 계보, 릴리스 게이트와 SHACL). 지금 가져갈 만한 요점은, 우리가 방금 증명한 조인이 나중의 SHACL 셰이프가 폐쇄 세계(closed-world)로 강제할 역량 질문 — "이 로트에 필요한 모든 결과가 존재하고, 단일하며, 규격 안에 있는가?" — 바로 그것이라는 점입니다. 행이 누락된 LIMS 통합이 실패하는 질문이자, SQL의 개방 세계(open-world) LEFT JOIN이 스스로는 제기할 수 없는 질문입니다.

이 책 전체가 데이터를 공급받는 CHO 시뮬레이터

이 책에는 진짜 바이오리액터가 없으므로, 결정론적인 것을 하나 제공합니다. Python 패키지 bioproc_sim(make venv로 설치되고 make data로 구동됨)은 이 책의 모든 데이터셋을 하나의 고정 마스터 시드(master seed) SIM_SEED=2026으로부터 생성하므로, 14일 유가식 트레이스(trace)는 모든 컴퓨터에서 바이트 단위로 동일합니다. 그 결정론(determinism)은 눈속임이 아닙니다 — 그것이 바로 CI가 MANIFEST.sha256을 단언(assert)하고 데이터의 조용한 변동(drift)을 잡아낼 수 있게 하는 것입니다.

이 유가식 회분은 로지스틱 생존 세포(viable-cell) 성장, 모노드(Monod) 글루코스/글루타민 동역학(lactate는 성장 중 부산물로 생성되고 후기에 소비되며, 제한 기질이 아닙니다), 생존 세포의 적분과 함께 누적되는 항체 역가(주로 비성장 연관 생산 항 — 항체는 성장이 느려질수록 가장 빠르게 누적되며, 실제 CHO 배양이 생산기에 역가의 대부분을 만들어내는 방식입니다), 그리고 경계가 있는 노이즈를 가진 PID 제어 DO 및 pH로 CHO 배양을 모델링합니다. 심지어 7일 차에 의도적인 0.5 °C 일탈(excursion)을 심고, 3, 5, 7, 9, 11, 13일 차에 예정된 볼러스 피드(bolus feed)를 넣어 — 이후 장에서 감지하고, 경보를 울리고, 검토할 실제 이벤트가 있도록 합니다. 골든 트레이스의 한 행은 다음과 같습니다.

ts,tag,value,unit,quality,batch_id
2026-01-05 00:00:00+00:00,BR101.DO.PV,40.8224,%sat,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.Temp.PV,37.0145,degC,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.Titer.PV,-0.0045,g/L,192,BATCH-2026-001
2026-01-05 00:00:00+00:00,BR101.pH.PV,7.0511,pH,192,BATCH-2026-001

quality 열은 레거시 OPC DA(Classic) 품질 코드를 담는데, 여기서 192 = Good, 64 = Uncertain, 0 = Bad입니다. (역전에 주의하세요: 더 새로운 별개 표준인 현대 OPC UA — 7장에서 풀어 설명합니다 — 에서는 StatusCode 0Good을 뜻하므로, 같은 숫자가 두 시스템에서 정반대를 의미합니다. 이 페이지에서는 192/64/0 척도만 사용합니다.) unit 열은 공학 단위(engineering unit)를 숫자에 붙여 두며, batch_id는 모든 판독값을 ISA-88/95 모델로 다시 묶는 실입니다. 같은 엔진은 캡처 장을 위해 OPC UA 서버와 Mosquitto로 실시간 스트리밍할 수도 있고, 프로듀서(producer)를 부팅하지 않고 파일을 재생하며 따라가고 싶은 장을 위해 평평한 골든 파일을 datasets/에 덤프할 수도 있습니다. 하나의 시드, 하나의 단일 진실 원천(source of truth), 책 속 모든 숫자.

batch_id 열은 머신러닝 장들이 절대적으로 의존하는 또 하나의 조용한 일을 합니다 — 바로 정직한 모델 검증을 위한 그룹화 키(grouping key)입니다. 한 배치 안의 분 단위 행들은 독립적인 추출(draw)이 아닙니다 — BATCH-2026-001의 모든 판독값은 같은 세포, 같은 배지 로트, 같은 프로브 보정을 공유합니다 — 그래서 소프트 센서의 훈련 행과 테스트 행을 무작위로 나누면 배치 정체성이 분할(split)을 가로질러 누설(leak)되고, 모델이 실제 운영에서는 결코 보지 못할 정확도를 보고하게 됩니다. 방어 가능한 분할은 그룹화된, 한 배치씩 빼는(leave-one-batch-out) 교차 검증(batch_id를 키로 한 GroupKFold, 그래서 단일 행이 아니라 배치 전체가 분할의 한쪽에만 떨어짐)이며, 바로 이것이 시뮬레이터가 하나의 긴 트레이스가 아니라 여러 별개의 배치를 제공하는 이유입니다. 여기서 센서 판독값을 문맥화하는 그 batch_id 실은, 모델의 검증이 스스로를 속이지 않게 지키는 실이기도 합니다. 누설 함정, 그룹화된 교차 검증, 그리고 적용 범위(applicability domain — 모델이 실제로 보정된 입력 영역으로, 그 너머의 예측은 외삽임)는 5권의 모델과 검증 장에서 다룹니다. 결정론적 MANIFEST.sha256은 ML에 두 번째 이유로도 중요합니다 — 모델은 그 훈련 데이터가 재현 가능할 때만 재현 가능하므로, 기록된 데이터셋 해시는 모델 계보(model lineage)의 상류 절반입니다. 데이터 다이제스트를 모델 버전 곁에 고정하면, 검토자는 정확히 어떤 바이트가 정확히 어떤 모델을 만들었는지 증명할 수 있으며, 이는 MLOps와 라이프사이클 장이 드리프트(drift) 감지와 통제된 재훈련을 세우는 토대입니다. (ML 장들이 강조하는 한 가지 주의: 어디서나 바이트 단위로 동일한 시뮬레이터는 재현성의 선물이지만 공변량 변화(covariate shift)의 사각지대이기도 합니다 — 이 한 시드의 세계만 학습한 모델은 실제 세포주의 회분 간 흔들림을 아직 만나지 못했으며, 이는 그 자체로 또 다른 종류의 드리프트입니다.)

시뮬레이터가 공급하는 스택의 데이터 흐름 그래프: SIM_SEED=2026을 가진 bioproc_sim이 make data와 라이브 Mosquitto MQTT 스트림을 통해 데이터셋을 내보내고, make load는 그 데이터셋을 ts.sensor_reading TimescaleDB 하이퍼테이블에 안착시키며, make seed는 platform/db/seed를 s88 ISA-88/95 모델로 적재한다. 히스토리안과 모델은 batch_id로 조인하는 s88.v_batch_sensor에서 수렴하여 Grafana 대시보드로 흘러간다.

왜 중요한가

다운스트림의 모든 것 — 히스토리안, 배치 모델, 문맥화, 감사 체인(audit chain), 역가 소프트 센서 — 은 작동하는, 재현 가능한 기반을 전제합니다. 이를 잘못 잡으면 이후 모든 장이 그 불안정함을 물려받습니다. 제대로 잡으면, 이 책은 읽는 것이 아니라 실행하는 무언가가 됩니다.

규제 측면의 배당도 있습니다. 버전이 고정되고, 선언적이며, 자동화된 환경은 정확히 적격성 평가(qualification — 시스템이 설치되어 의도대로 작동함을 문서로 증명하는 규제 산업 용어) 작업이 원하는 산출물입니다. GAMP 5(2판) — GxP("Good x Practice", FDA/EU 제약 품질 규정의 우산 용어) 컴퓨터화 시스템을 검증하기 위한 업계 지침 — 는 위험 기반 라이프사이클을 제시하며, 인프라 적격성 평가와 오픈소스 소프트웨어에 명시적인 주의를 기울입니다 [11]. 인프라가 코드일 때 — Compose 파일 하나, 다이제스트 잠금 파일 하나(versions.lock), Makefile 하나 — 여러분의 설치(installation) 증거는 누군가의 터미널 스크린샷이 아니라 재현 가능하고 검토 가능한 것이 됩니다. FDA의 컴퓨터 소프트웨어 보증(Computer Software Assurance, CSA) 최종 가이던스도 같은 방향을 가리킵니다. 보증은 위험 기반이고 최소 부담이어야 하며, 의례적 문서화보다 로그, 자동화, 공급자 증거에 기대야 한다는 것입니다 [12]. 고정된 스택을 부팅하고 헬스체크를 통과하는 깔끔한 make up은 바로 그러한 프레임워크가 보상하는, 객관적이고 반복 가능한 증거입니다 — 그리고 결정론적 재빌드에 기록된 MANIFEST.sha256/다이제스트(make alcoa 타깃이 검사하는 것)를 더하면, 데이터가 ALCOA+가 됩니다. 이는 기록이 귀속 가능(Attributable)하고, 판독 가능(Legible)하며, 동시적(Contemporaneous)이고, 원본(Original)이며, 정확(Accurate)하다(여기에 완전(Complete)·일관(Consistent)·영속(Enduring)·이용 가능(Available)까지 더한다)는 규제 당국의 약칭입니다. 바이트가 바뀌지 않았음을 증명할 수 있는 것입니다. (검증 사례는 뒤에서 온전히 다룹니다 — ALCOA+ 데이터 무결성은 23장, Part 11 / Annex 11은 24장, GAMP 5 + CSA 워크스루는 25장.)

실제 현장에서는

물론 실제 공장은 노트북 한 대의 Compose 파일로 돌아가지 않습니다. 프로덕션 히스토리안은 전용의 고가용성(highly-available) 하드웨어 위에서 돌아가는 AVEVA PI일 수 있고, DCS는 Emerson DeltaV나 Siemens일 수 있으며, LIMS는 상용이고 검증(validated)되어 있습니다. 이런 시스템들은 노트북에서 돌아갈 수 없고 오픈소스도 아닙니다 — 그래서 이 책은 자신이 하이브리드(hybrid)임을 정직하게 밝힙니다. 여기 있는 오픈소스 핵심은 여러분을 어쩌면 80% 지점까지 데려다주고, GxP의 마지막 1마일(Part 11 — FDA 21 CFR Part 11, 전자 기록과 서명을 규율하는 규칙 — 전자 서명, 벤더 책임성, 검증된 HA(high availability, 고가용성))은 상용 시스템과 정식 검증이 넘겨받는 지점입니다. 이 스택의 어떤 도구도 즉시 Part 11을 준수하지 않으며, 그렇지 않다고 말하는 것은 엔지니어링이 아니라 마케팅일 것입니다.

그러나 이 장이 세우는 아키텍처는, 단지 노트북 규모일 뿐, 큰 회사들이 쓰는 것과 똑같습니다. 관계형 기록 시스템 옆의 시계열 히스토리안, 현장(floor)과 IT 사이의 메시지 버스, 그 위의 대시보드. 재현 가능하고 프로파일로 게이팅된 개발 스택은 클린룸 없이도 그 아키텍처를 실험하는 방법이며 — 같은 Compose-and-Make 규율은 실제 시설이 요구할 IQ/OQ 증거(Installation Qualification 및 Operational Qualification — 규제 대상 시스템이 올바르게 설치되고 명세대로 작동함을 문서로 증명하는 것)로 곧장 확장됩니다. 정직한 간극은 데이터 공장의 형태가 아니라 운영적 특성 — 가동 시간 보장, 인증된 지원, 정식 검증 패키지 — 에 있습니다.

이것이 적격성 평가 사다리에 어떻게 대응되는지 정확히 짚어 둘 가치가 있습니다. 그 대응은 느슨한 비유가 아니라 정확합니다. 고정된 compose.yamlversions.lock 다이제스트를 더한 것은 여러분의 IQ 증거입니다 — 올바른 구성요소가 올바른 버전으로 설치되었다는 증명이며, 콘텐츠 주소 지정이므로 스크린샷보다 강합니다. 검토자가 다시 풀(pull)해 바이트를 확인할 수 있기 때문입니다. 헬스체크와 make up 폴링 루프는 OQ 증거입니다 — 각 서비스가 명세대로 작동한다는(데이터베이스가 연결을 받고, 브로커가 응답하고, 대시보드가 히스토리안에 닿는다는) 증명을 적힌, 반복 가능한 테스트로 보입니다. 노트북 스택이 공급할 수 없는 것은 PQ(Performance Qualification — 전체 공정이 실제 자재와 작업자, 생산 부하 아래 시간에 걸쳐 재현성 있게 수행됨을 증명하는 것)입니다. PQ는 소프트웨어가 아니라 살아 있는 GMP 공정의 속성이기 때문입니다. PQ에는 실제 세포 배양 캠페인, 자격을 갖춘 작업자, 그리고 14일 시뮬레이터 트레이스가 아니라 수개월치 실제 배치를 담은 히스토리안이 필요합니다. 그 노트북 위의 IQ/OQ, 공장에서만의 PQ라는 경계는 이 장의 나머지가 지키는 하이브리드 정직성과 같으며, 이 코드-as-증거가 IQ/OQ로 인정받게 해주는 CSV→CSA 전환은 2권의 GAMP 5와 CSA 장의 주제입니다.

시뮬레이터가 업스트림 바이오리액터만 모델링하므로, 바이오공정 독자를 위해 범위를 하나 짚어 둡니다. 이 스택이 기록하는 BATCH-2026-001 자재는 수확(harvest)에서 멈추지 않습니다 — 실제 캠페인에서 그것은 여기 시계열 히스토리안이 결코 내부를 보지 못할 다운스트림 정제 트레인을 통과합니다. Protein A 캡처 크로마토그래피(항체를 결합시키고 나머지를 모두 씻어내는 친화성 단계), 저 pH 바이러스 불활화, 폴리싱 크로마토그래피(잔류 숙주 세포 단백질, DNA, 응집체를 제거하는 이온교환·혼합 모드 단계), 바이러스 여과, 그리고 원료 의약품(drug substance)으로의 UF/DF(한외여과/투석여과 — 항체를 농축하고 최종 제형으로 완충액 교환하는 것)입니다. 이들 단계는 저마다의 공정 중(in-process) 태그, 저마다의 일탈, 저마다의 릴리스 결과를 갖는 배치 작업이며 — batch_id 실과 ISA-88/95 모델이 그토록 중요한 이유는, 이들이 바로 나중의 단일 쿼리가 BATCH-2026-001을 이 바이오리액터 트레이스부터 그것이 되는 원료 의약품 로트의 숙주 세포 단백질·단량체 순도 수치까지 따라가게 해주는 것이기 때문입니다.

핵심 용어

  • 컨테이너 / OCI 이미지(Container / OCI image) — 애플리케이션과 그 의존성을 밀봉해 담은 이식 가능한 묶음으로, 불변의 콘텐츠 다이제스트로 식별되며, 각 서비스가 배포되는 단위.
  • Docker Compose / compose.yaml — 멀티서비스 애플리케이션을 정의하고 명령 하나로 모두 띄우는 선언적 파일(이자 도구).
  • 프로파일(Profile) — 어떤 서비스가 시작될지를 게이팅하는 Compose 라벨로, 독자가 현재 장에 필요한 계층(core, capture, semantics, commercial, trust, analytics)만 켤 수 있게 함.
  • 태그 고정(Tag pinning) — 이미지를 :latest가 아니라 특정 MAJOR.MINOR.PATCH 버전(및 다이제스트)으로 고정하여, 환경이 재현 가능하고 업그레이드가 의도적이게 만드는 것.
  • 헬스체크(Healthcheck) — 서비스가 실제로 준비되었는지 판단하기 위해 플랫폼이 실행하는 명령으로, 의존하는 서비스(및 테스트)가 올바르게 기다릴 수 있게 함.
  • 이미지 다이제스트(Image digest, sha256:…) — 이미지 매니페스트의 불변·콘텐츠 주소 지정 해시. 태그와 달리 재지정할 수 없으므로, 다이제스트로 고정하면 바이트 단위로 정확한 이미지가 보장됨. versions.lock이 각 태그 뒤의 다이제스트를 기록함.
  • 이름 붙은 볼륨 대 바인드 마운트(Named volume vs bind mount) — 컨테이너가 저장소를 얻는 두 가지 방식. 이름 붙은 볼륨(pgdata)은 Docker가 관리하며 make down 후에도 지속되고, 바인드 마운트(../db:…:ro)는 호스트 디렉터리를 매핑하는데 여기서는 첫 부팅 시 스키마를 자동 실행하기 위해 읽기 전용으로 둠.
  • ISA-88/95 — 배치/레시피 모델(88)과 장비/기업 계층(95)을 위한 국제 표준. 다음 장이 구축하는 관계형 백본으로, 센서 판독값을 그것이 속한 배치·레시피·단계에 묶을 수 있게 한다.
  • 히스토리안(Historian) — 고속 공정 데이터를 위한 시계열 저장소. 여기서는 배치 모델과 같은 PostgreSQL 데이터베이스 안의 TimescaleDB 하이퍼테이블.
  • 하이퍼테이블(Hypertable) — 빠른 시계열 쓰기와 쿼리를 위해 시간 기준으로 자동으로 청크(chunk)로 파티셔닝되는 TimescaleDB의 PostgreSQL 테이블.
  • SIM_SEED=2026 — CHO 시뮬레이터의 출력을 어디서나 바이트 단위로 동일하게 만드는 고정 마스터 시드로, 데이터셋이 재현 가능하고 CI가 이를 검증할 수 있게 함.
  • 그룹화된 / 한 배치씩 빼는 교차 검증(Grouped / leave-one-batch-out cross-validation) — 모델의 훈련·테스트 데이터를 무작위 행이 아니라 배치 전체 단위로(batch_id를 키로) 나누어, 같은 배치의 상관된 행이 분할을 가로지르며 보고 정확도를 부풀리지 못하게 하는 것. 소프트 센서에 필요한 정직한 검증(5권).
  • IQ / OQ / PQ — 적격성 평가 사다리. 설치 적격성 평가(Installation Qualification, 올바른 구성요소가 설치됨 — 여기서는 고정된 compose.yamlversions.lock), 운영 적격성 평가(Operational Qualification, 각 서비스가 명세대로 작동함 — 여기서는 헬스체크와 make up 폴링 루프), 성능 적격성 평가(Performance Qualification, 전체 공정이 시간에 걸쳐 재현성 있게 수행됨 — 노트북이 아니라 살아 있는 GMP 공장만이 공급할 수 있는 속성).

다음 이야기

스택이 떠 있고 시뮬레이터의 숫자들이 PostgreSQL에 들어와 있습니다 — 하지만 지금 BR101.DO.PV = 48.6 같은 행은 라벨이 붙은 부동소수점(float)일 뿐입니다. 그것이 무언가를 의미하게 하려면, 그 판독값이 어떤 장비, 어떤 레시피, 어떤 단계, 어떤 배치에 속하는지를 말해주는 골격이 필요합니다. 다음 장 배치 및 장비 데이터 모델: PostgreSQL 속의 ISA-88/95는 바로 그 관계형 등뼈를 세웁니다 — 이후의 모든 숫자를 한 배치에 관한 사실로 바꾸는 모델입니다.