왜 배포 순서가 문제가 되는가

모노레포에서는 여러 서비스와 라이브러리가 한 저장소에 모여 있다. 문제는 이들이 서로를 참조한다는 점이다. 공용 라이브러리 core가 바뀌면 이를 쓰는 api와 worker도 함께 배포되어야 한다. 순서를 무시하고 api를 먼저 배포하면, 아직 반영되지 않은 core 계약에 의존해 런타임 오류가 나거나, 반대로 이미 배포된 하위 서비스가 상위 서비스가 기대하지 않는 스키마를 노출한다.

단순히 "바뀐 것만 배포"하는 접근도 위험하다. 변경 감지는 직접 수정된 패키지만 잡아내지, 그 패키지에 의존하는 다운스트림까지 자동으로 포함하지 않기 때문이다. 결국 배포는 의존 그래프의 위상 정렬(topological sort) 문제로 귀결된다.

변경 영향 범위 계산하기

먼저 무엇이 바뀌었고, 그 변경이 어디까지 전파되는지를 계산해야 한다. Nx, Turborepo, Bazel 같은 도구는 그래프를 내장하지만, 원리는 동일하다. 변경된 패키지에서 시작해 역방향(의존받는 쪽)으로 그래프를 확장한다.

import networkx as nx

# edge: (A, B) == "A는 B를 의존한다"
g = nx.DiGraph()
g.add_edges_from([("api", "core"), ("worker", "core"), ("core", "utils")])

def affected(changed):
    # 변경된 노드 + 그것을 (직간접) 의존하는 모든 노드
    impacted = set(changed)
    for c in changed:
        impacted |= nx.ancestors(g, c)   # c를 의존하는 상위들
    return impacted

# utils가 바뀌면 core, api, worker 모두 영향
print(affected(["utils"]))   # {'utils', 'core', 'api', 'worker'}

여기서 ancestors는 "이 노드를 향해 화살표가 들어오는" 상위 노드들이다. 변경 대상의 상위를 모두 포함해야 다운스트림 누락이 없다.

위상 정렬로 배포 순서 도출

영향 범위를 구했으면, 이제 그 부분 그래프를 위상 정렬해 배포 순서를 만든다. 의존 대상(하위)이 먼저, 의존하는 쪽(상위)이 나중이다. 같은 레벨(서로 의존이 없는)은 병렬 배포가 가능하다.

def deploy_order(impacted):
    sub = g.subgraph(impacted)
    # 하위 → 상위 순으로 배포되도록 역순 위상 정렬
    ordered = list(reversed(list(nx.topological_sort(sub))))
    # 병렬 레벨(generation)로 묶기
    levels = list(nx.topological_generations(sub))
    return ordered, list(reversed(levels))

order, levels = deploy_order(affected(["utils"]))
print(order)    # ['utils', 'core', 'api', 'worker'] 형태
print(levels)   # 같은 레벨은 병렬 배포 가능

순환 의존(cycle)이 있으면 위상 정렬이 실패한다. 이건 배포 도구의 버그가 아니라 아키텍처의 신호다. nx.find_cycle로 잡아 CI에서 조기에 실패시키는 편이 낫다.

CI 파이프라인에 녹이기

계산된 레벨을 CI에서 순차 스테이지로 펼친다. 아래는 레벨별로 병렬 배포하되, 이전 레벨이 헬스체크를 통과해야 다음으로 넘어가는 예시다.

deploy:
  stages:
    - level-0   # utils
    - level-1   # core
    - level-2   # api, worker (병렬)

.deploy_template: &deploy
  script:
    - ./deploy.sh "$SERVICE"
    - ./healthcheck.sh "$SERVICE" --timeout 120
  retry: 1

deploy_core:
  stage: level-1
  variables: { SERVICE: core }
  <<: *deploy

핵심은 스테이지 경계마다 헬스체크를 강제하는 것이다. 하위 서비스가 정상 기동을 확인하기 전에 상위를 배포하면 순서를 지킨 의미가 사라진다.

계약 호환성과 배포 방향

순서만큼 중요한 것이 변경의 호환성 방향이다. 스키마나 API 계약을 바꿀 때 하위와 상위 중 누구를 먼저 배포할지가 무중단 여부를 가른다.

변경 유형배포 순서이유
필드 추가(하위 호환)생산자 먼저소비자는 무시하면 되므로 안전
필드 제거/변경소비자 먼저참조를 끊은 뒤 제거해야 오류 없음
DB 컬럼 삭제2단계 배포사용 중단 → 코드 배포 → 컬럼 삭제

즉 그래프가 알려주는 순서와 반대로 가야 할 때가 있다. 파괴적 변경은 "확장 후 수축(expand-contract)" 패턴으로 나눠, 먼저 새 것을 추가하고 소비자를 옮긴 뒤 옛것을 제거하는 두 번의 배포로 처리한다.

실무에서 자주 밟는 지뢰

  • 암묵적 의존: 코드상 import는 없지만 같은 DB나 메시지 큐를 공유하는 서비스는 그래프에 안 잡힌다. 런타임 의존은 별도로 명시해야 한다.
  • 롤백 순서: 배포가 하위→상위였다면 롤백은 상위→하위여야 한다. 롤백 스크립트가 순서를 뒤집는지 반드시 검증한다.
  • 부분 실패: 레벨-2에서 하나만 실패하면 그래프는 이미 일관성이 깨진 상태다. 실패 지점 이후를 자동 롤백할지, 멈추고 사람이 판단할지 정책을 미리 정한다.
  • 그래프 신뢰성: 의존 정보가 낡으면 오케스트레이션 전체가 무너진다. 그래프 생성은 빌드 산출물에서 자동 추출하고, 수기 관리는 피한다.

정리

모노레포 배포는 "무엇을 바꿨나"가 아니라 "그 변경이 어디까지 번지나"에서 출발한다. 영향 범위를 역방향 그래프로 계산하고, 위상 정렬로 순서와 병렬 레벨을 뽑고, 각 경계에서 헬스체크로 문을 잠근다. 여기에 계약 호환성 방향과 롤백 순서까지 함께 설계하면, 커진 저장소에서도 배포는 예측 가능한 작업이 된다.