배포는 완성된 코드를 서버에 올리는 일이 아니다

vx_developer·2026년 10월 9일

개발하다가

목록 보기
48/49
post-thumbnail

프로그래밍을 처음 배울 때 배포는 보통 개발한 코드를 서버에 올려 사용자가 접속할 수 있게 만드는 일이라고 배운다.

await deploy({
  application: "learning-platform",
  environment: "production",
});

개발 환경에서 완성한 애플리케이션을 운영 환경으로 옮긴다는 뜻이다.

기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 배포할 때는 파일이 서버에 도착했는지보다 변경사항이 사용자에게 어떤 과정으로 전달되는지를 판단해야 한다.

  • 새 버전이 실제 운영 환경에서도 실행 가능한가?
  • 이전 버전과 새 버전이 동시에 실행되어도 호환되는가?
  • 데이터베이스 구조를 언제 변경해야 하는가?
  • 새 버전에 문제가 생기면 어떻게 발견하는가?
  • 어느 시점에 배포를 중단하거나 되돌릴 것인가?
  • 코드 롤백만으로 데이터 변경까지 복구할 수 있는가?

배포는 완성된 코드를 서버에 올리는 일이 아니다.

배포는 변경사항을 검증 가능한 단위로 만들고, 기존 서비스와의 호환성을 유지하며, 관찰과 복구가 가능한 방식으로 사용자에게 전달하는 과정이다.


코드를 합치는 순간과 사용자가 변화를 만나는 순간은 다르다

크리스가 온라인 강의 서비스를 개발한다고 생각해 보자.

강의 영상 아래에 새로운 퀴즈 기능을 추가한 뒤 코드를 기본 브랜치에 합쳤다고 해서 모든 사용자가 즉시 기능을 사용하는 것은 아니다.

변경사항은 여러 단계를 통과한다.

flowchart TD
    A["코드 변경"] --> B["테스트·정적 검사"]
    B --> C["배포 아티팩트 생성"]
    C --> D["스테이징 검증"]
    D --> E["일부 운영 트래픽"]
    E --> F{"지표가 정상인가?"}
    F -->|정상| G["전체 사용자 공개"]
    F -->|이상| H["중단·롤백"]

이 흐름은 배포가 한 번의 업로드가 아니라 여러 판단 지점을 가진 과정이라는 것을 보여준다.

각 단계는 서로 다른 질문에 답한다.

단계확인할 질문
코드 변경의도한 기능과 수정 범위가 명확한가?
자동 검증기존 약속을 깨뜨리지 않는가?
아티팩트 생성동일한 결과물을 다시 식별할 수 있는가?
스테이징운영과 비슷한 환경에서 실행되는가?
일부 공개실제 트래픽에서도 오류와 지연이 정상인가?
전체 공개모든 사용자에게 전달해도 되는가?
복구문제가 생겼을 때 안전하게 중단할 수 있는가?

코드가 저장소에 존재하는 것, 서버에서 실행되는 것, 사용자가 기능을 사용할 수 있는 것은 서로 다른 상태다.


같은 소스 코드라도 빌드 결과가 다르면 같은 버전이 아니다

개발 환경과 운영 환경에서 각각 애플리케이션을 다시 빌드한다고 생각해 보자.

type ReleaseArtifact = {
  commitSha: string;
  imageDigest: string;
  builtAt: string;
};

이 타입은 배포되는 결과물이 어떤 코드에서 언제 만들어졌으며, 어떤 고유한 내용 식별자를 가졌는지 표현한다.

운영 환경에서 다시 빌드하면 다음과 같은 차이가 생길 수 있다.

  • 의존성의 하위 버전이 달라진다.
  • 빌드 도구나 운영체제가 다르다.
  • 생성 시점의 설정이 포함된다.
  • 패키지 저장소의 내용이 변경된다.
  • 빌드 중 네트워크 요청의 결과가 달라진다.

스테이징에서 검증한 결과물과 운영에 올린 결과물이 서로 다르다면 스테이징 검증의 의미가 약해진다.

따라서 한 번 생성한 아티팩트를 여러 환경으로 승격하는 방식을 사용할 수 있다.

async function promoteRelease(
  artifact: ReleaseArtifact
) {
  await deployToStaging(artifact);
  await verifyStaging(artifact);
  await deployToProduction(artifact);
}

이 코드는 스테이징에서 검증한 것과 같은 아티팩트를 운영 환경으로 전달한다.

배포의 Source of Truth는 개발자의 노트북에 있는 폴더나 브랜치 이름이 아니다. 실제 실행 중인 아티팩트의 버전, 이미지 다이제스트, 설정과 배포 기록이 운영 상태를 설명해야 한다.


자동화된 파이프라인은 명령을 줄이는 도구가 아니라 판단 순서를 고정한다

배포 스크립트가 서버에 접속해 코드를 복사하는 명령만 포함한다면 검증 단계가 사람의 기억에 의존하게 된다.

배포 파이프라인에는 변경사항이 통과해야 할 조건을 표현할 수 있다.

jobs:
  verify:
    steps:
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test

  build:
    needs: verify
    steps:
      - run: npm run build

  deploy-production:
    needs: build
    environment: production
    steps:
      - run: npm run deploy

이 파이프라인은 정적 검사와 테스트가 성공한 뒤에만 빌드하고, 빌드가 성공한 결과만 운영 환경에 배포하도록 순서를 제한한다.

자동화가 있다고 안전한 배포가 자동으로 완성되는 것은 아니다. 자동화는 정의된 절차를 일관되게 실행할 뿐이다.

따라서 파이프라인에는 실제 위험에 맞는 검증이 포함되어야 한다.

  • 단위 테스트와 통합 테스트
  • 타입 검사와 정적 분석
  • 보안 취약점과 비밀값 노출 검사
  • 데이터베이스 마이그레이션 검증
  • 빌드 결과물 크기와 의존성 확인
  • 스테이징 스모크 테스트
  • 운영 배포 승인
  • 배포 후 상태 검사

중요한 점은 도구의 개수가 아니라 실패한 변경이 다음 단계로 넘어가지 못하도록 경계를 만드는 것이다.


배포 중에는 이전 버전과 새 버전이 함께 존재할 수 있다

온라인 강의 서비스에 여러 서버 인스턴스가 실행되고 있다고 생각해 보자.

새 버전을 한 번에 모두 교체하지 않는다면 일정 시간 동안 이전 버전과 새 버전이 같은 트래픽을 처리한다.

사용자 요청
    ↓
로드 밸런서
    ├─ 기존 서버 v1
    ├─ 기존 서버 v1
    ├─ 신규 서버 v2
    └─ 신규 서버 v2

이 과정에서 v2 서버가 만든 데이터를 v1 서버도 읽을 수 있어야 한다. 프론트엔드와 API가 서로 다른 시점에 교체된다면 이전 프론트엔드가 새로운 API와 통신하는 상황도 발생할 수 있다.

크리스가 강의 진행률 응답의 필드 이름을 바꾼다고 생각해 보자.

기존 응답은 다음과 같다.

type CourseProgressV1 = {
  courseId: string;
  progressPercent: number;
  isCompleted: boolean;
};

새 버전에서 isCompleted를 제거하고 곧바로 completionStatus만 반환하면 아직 이전 프론트엔드를 사용하는 사용자 화면이 깨질 수 있다.

전환 기간에는 두 필드를 함께 제공할 수 있다.

type CourseProgressTransition = {
  courseId: string;
  progressPercent: number;
  isCompleted: boolean;
  completionStatus:
    | "not_started"
    | "in_progress"
    | "completed";
};

이 타입은 기존 클라이언트가 isCompleted를 읽는 동안 새 클라이언트가 completionStatus를 사용하도록 허용한다.

모든 클라이언트와 서버가 새 계약으로 이동한 뒤에 기존 필드를 제거할 수 있다.

배포 호환성은 현재 코드끼리만 맞는지를 확인하는 일이 아니다. 전환 과정에서 만날 수 있는 이전 버전과 새 버전의 조합까지 고려하는 일이다.


데이터베이스 변경은 코드보다 되돌리기 어렵다

크리스가 강의 진행률을 백분율 대신 시청한 초 단위로 관리하도록 변경한다고 생각해 보자.

기존 컬럼을 바로 삭제하고 새 컬럼을 필수로 추가할 수 있다.

ALTER TABLE course_progress
DROP COLUMN progress_percent;

ALTER TABLE course_progress
ADD COLUMN watched_seconds INTEGER NOT NULL;

이 변경은 기존 서버가 여전히 progress_percent를 읽고 쓰는 동안 실행되면 오류를 만든다. 기존 데이터에는 watched_seconds 값도 없으므로 NOT NULL 추가가 실패할 수 있다.

배포 중 여러 버전이 공존할 수 있다면 데이터베이스 변경도 단계적으로 진행해야 한다.

첫 번째 단계에서는 기존 구조를 유지한 채 새 컬럼을 추가한다.

ALTER TABLE course_progress
ADD COLUMN watched_seconds INTEGER;

이 변경은 기존 코드가 계속 progress_percent를 사용하는 동안 새 코드가 watched_seconds를 인식할 수 있게 한다.

그다음 새 버전은 전환 기간 동안 두 값을 함께 기록할 수 있다.

await progressRepository.update({
  enrollmentId,
  progressPercent,
  watchedSeconds,
});

이 코드는 이전 버전과 새 버전이 모두 필요한 값을 읽을 수 있도록 두 표현을 함께 유지한다.

기존 데이터는 별도의 작업으로 채운다.

UPDATE course_progress
SET watched_seconds =
  ROUND(
    total_duration_seconds
    * progress_percent
    / 100.0
  )
WHERE watched_seconds IS NULL;

이 쿼리는 기존 진행률을 사용해 새로운 컬럼의 초기값을 계산한다. 실제 서비스에서는 데이터 양을 고려해 작은 배치로 나누고 실행 시간과 잠금 영향을 관찰해야 한다.

모든 코드가 새 컬럼을 사용하고 데이터 전환이 검증된 뒤에 제약을 강화하고 기존 컬럼을 제거할 수 있다.

ALTER TABLE course_progress
ALTER COLUMN watched_seconds SET NOT NULL;

ALTER TABLE course_progress
DROP COLUMN progress_percent;

이 과정을 확장, 전환, 축소의 흐름으로 볼 수 있다.

새 구조 추가
    ↓
이전·새 구조를 함께 지원
    ↓
기존 데이터 변환
    ↓
모든 사용처 전환 확인
    ↓
이전 구조 제거

스키마 변경은 코드 배포와 생명주기가 다르다. 코드 롤백은 이전 이미지를 실행하면 될 수 있지만, 삭제하거나 변환한 데이터는 같은 방법으로 되돌아오지 않는다.


서버가 실행 중이라는 사실과 요청을 받을 준비가 되었다는 사실은 다르다

새 인스턴스의 프로세스가 시작되었더라도 아직 요청을 처리할 준비가 되지 않았을 수 있다.

  • 설정 파일을 읽는 중일 수 있다.
  • 데이터베이스 연결을 준비하는 중일 수 있다.
  • 필요한 캐시를 초기화하고 있을 수 있다.
  • 마이그레이션된 구조와 호환되지 않을 수 있다.
  • 외부 저장소 접근 권한이 잘못되었을 수 있다.

준비 상태를 확인하는 엔드포인트를 제공할 수 있다.

@Controller("health")
export class HealthController {
  constructor(
    private readonly database: DatabaseService
  ) {}

  @Get("live")
  checkLiveness() {
    return { status: "alive" };
  }

  @Get("ready")
  async checkReadiness() {
    await this.database.ping();

    return {
      status: "ready",
      release: process.env.RELEASE_SHA,
    };
  }
}

live 응답은 프로세스 자체가 동작하는지 확인한다. ready 응답은 데이터베이스 연결처럼 요청 처리에 필요한 조건까지 확인한다.

두 검사를 구분하지 않으면 일시적인 외부 의존성 문제로 프로세스를 계속 재시작하거나, 준비되지 않은 서버에 사용자 요청을 전달할 수 있다.

다만 준비 상태 검사에 모든 외부 서비스를 무조건 포함해서도 안 된다. 선택 기능 하나의 장애가 전체 서버를 트래픽에서 제외해야 하는지는 서비스의 의존 관계에 따라 판단해야 한다.


새 버전은 한 번에 모든 사용자에게 전달하지 않아도 된다

배포 방식은 변경 위험과 서비스 규모에 따라 달라진다.

방식동작적합한 상황주의할 점
재생성기존 버전을 종료하고 새 버전을 시작중단을 허용할 수 있는 작은 서비스배포 중 서비스 중단
롤링 배포기존 인스턴스를 새 인스턴스로 점진적 교체일반적인 무중단 교체이전·새 버전 호환성
블루·그린기존 환경과 새 환경을 함께 준비한 뒤 트래픽 전환빠른 전환과 복구가 중요두 환경의 비용과 데이터 변경
카나리일부 사용자나 트래픽에 먼저 공개실제 환경에서 위험을 작게 검증사용자 그룹 비교와 지표 필요

예를 들어 새 퀴즈 채점 기능을 전체 트래픽의 5%에 먼저 배포할 수 있다.

function selectQuizVersion(userId: string) {
  const bucket =
    stableHash(userId) % 100;

  return bucket < 5 ? "v2" : "v1";
}

이 함수는 같은 사용자가 요청할 때마다 같은 그룹에 배정되도록 안정적인 해시를 사용한다.

카나리 배포 후에는 단순히 서버가 살아 있는지만 확인해서는 안 된다.

  • 퀴즈 제출 성공률이 달라졌는가?
  • 채점 오류가 증가했는가?
  • 응답 시간이 느려졌는가?
  • 특정 브라우저나 지역에서만 문제가 생기는가?
  • 학습 진행률 저장이 누락되지 않는가?

트래픽을 조금씩 늘리는 것은 위험을 작게 나누는 방법이다. 문제를 판단할 지표와 중단 기준이 없다면 느리게 전체 장애로 이동하는 과정이 될 수도 있다.


배포와 기능 공개를 분리하면 복구 선택지가 늘어난다

코드는 운영 환경에 배포했지만 기능은 아직 사용자에게 보이지 않게 할 수 있다.

const canUseQuizV2 =
  await featureFlags.isEnabled({
    key: "quiz-v2",
    userId: currentUser.id,
  });

if (canUseQuizV2) {
  return quizV2Service.getQuiz(lectureId);
}

return quizV1Service.getQuiz(lectureId);

이 코드는 같은 배포 안에 이전 기능과 새 기능을 두고 사용자별로 공개 여부를 결정한다.

이렇게 하면 다음 작업을 분리할 수 있다.

  • 코드가 운영 환경에서 정상적으로 실행되는지 확인한다.
  • 내부 사용자에게 먼저 기능을 공개한다.
  • 일부 학습자에게 점진적으로 확장한다.
  • 문제가 생기면 새 배포 없이 기능을 끈다.
  • 제품 실험이 끝난 뒤 사용하지 않는 경로를 제거한다.

그러나 기능 플래그도 영구적인 안전장치가 아니다.

오래된 플래그가 쌓이면 테스트해야 할 코드 조합이 증가한다. 플래그의 담당자, 제거 조건과 만료 시점을 함께 관리해야 한다.

배포는 코드를 환경에 전달하는 일이고, 릴리스는 사용자가 기능을 사용할 수 있게 하는 일이다. 두 과정은 같을 수도 있지만 반드시 같아야 하는 것은 아니다.


환경 설정은 아티팩트를 바꾸지 않고 실행 조건을 결정해야 한다

스테이징과 운영 환경은 데이터베이스 주소, 외부 서비스 키와 기능 정책이 다를 수 있다.

환경별 값을 코드에 직접 넣으면 같은 아티팩트를 승격하기 어려워진다.

const RuntimeConfigSchema = z.object({
  databaseUrl: z.string().url(),
  videoStorageRegion: z.string().min(1),
  releaseSha: z.string().min(7),
  quizV2Enabled: z.coerce.boolean(),
});

외부에서 주입되는 설정은 검증 전까지 신뢰할 수 없다. 이 스키마는 애플리케이션 시작 시 필수 설정과 형식을 확인한다.

const runtimeConfig =
  RuntimeConfigSchema.parse({
    databaseUrl: process.env.DATABASE_URL,
    videoStorageRegion:
      process.env.VIDEO_STORAGE_REGION,
    releaseSha: process.env.RELEASE_SHA,
    quizV2Enabled:
      process.env.QUIZ_V2_ENABLED,
  });

잘못된 설정으로 일부 요청만 실패하게 두기보다 시작 단계에서 문제를 명확히 발견할 수 있다.

설정에는 서로 다른 책임이 존재한다.

설정 성격관리 위치
데이터베이스 주소환경 설정 또는 비밀 관리 시스템
외부 서비스 인증정보비밀 관리 시스템
배포된 코드 버전배포 메타데이터
운영자가 바꾸는 기능 정책설정 저장소 또는 기능 플래그 시스템
사용자별 강의 진행률데이터베이스
코드에 반복되는 규칙애플리케이션 또는 도메인 로직

환경변수를 사용한다는 사실보다 누가 변경할 수 있고, 언제 적용되며, 변경 이력이 남는지가 중요하다.


배포 성공은 명령의 종료 코드가 아니라 사용자 지표로 판단해야 한다

배포 명령이 성공했다는 것은 인프라가 요청을 받아들였다는 의미일 수 있다. 사용자가 정상적으로 강의를 듣고 있다는 뜻은 아니다.

배포된 버전을 로그와 지표에 포함할 수 있다.

logger.info("lecture_progress_saved", {
  enrollmentId,
  releaseSha: runtimeConfig.releaseSha,
  durationMs,
});

이 로그는 진행률 저장 이벤트가 어느 버전에서 발생했는지 연결한다. 실제 운영에서는 개인정보와 민감한 식별자를 로그 정책에 맞게 마스킹하거나 대체해야 한다.

배포 전후에는 기술 지표와 사용자 흐름을 함께 확인해야 한다.

종류확인할 예
인프라준비된 인스턴스 수, 재시작 횟수
애플리케이션오류율, 응답 시간, 예외 유형
데이터마이그레이션 실패, 저장 누락, 중복
사용자 흐름영상 재생 성공률, 퀴즈 제출 성공률
비즈니스강의 완료율, 결제 또는 등록 실패

예를 들어 전체 API 오류율은 정상이어도 퀴즈 제출 API만 실패할 수 있다. 서버 CPU가 안정적이어도 강의 진행률이 저장되지 않을 수 있다.

따라서 배포 판단에는 다음 세 가지가 필요하다.

  1. 어떤 지표를 확인할 것인가?
  2. 어느 값부터 이상으로 판단할 것인가?
  3. 이상이 발생하면 누가 어떤 조치를 할 것인가?

관찰 기준이 배포 전에 정해져 있어야 문제가 생긴 뒤에 성공 기준을 바꾸지 않게 된다.


롤백은 이전 코드로 돌아가는 것보다 넓은 복구 계획이다

새 퀴즈 기능에서 오류가 발생하면 이전 애플리케이션 이미지를 다시 실행할 수 있다.

await rollbackDeployment({
  application: "learning-platform",
  releaseSha: previousReleaseSha,
});

이 코드는 실행 중인 애플리케이션 버전을 이전 버전으로 전환한다.

하지만 배포 과정에서 다음 변경이 이미 일어났다면 코드 롤백만으로는 충분하지 않을 수 있다.

  • 기존 컬럼이 삭제되었다.
  • 데이터 형식이 비가역적으로 변환되었다.
  • 새 버전이 이전 코드가 이해하지 못하는 값을 저장했다.
  • 외부 서비스에 요청이 전달되었다.
  • 사용자에게 알림이나 이메일이 발송되었다.
  • 캐시와 검색 인덱스가 새 구조로 갱신되었다.

복구 방법은 변경의 성격에 따라 달라진다.

변경가능한 복구
애플리케이션 코드이전 아티팩트 재배포
기능 플래그새 기능 비활성화
호환 가능한 컬럼 추가이전 코드 유지 가능
데이터 변환역변환 또는 백업 복원 검토
외부 메시지 발송취소보다 후속 정정 필요
잘못 생성된 데이터보정 작업과 감사 기록

모든 배포가 즉시 롤백 가능하다고 가정해서는 안 된다. 되돌릴 수 없는 변경에는 전진 수정, 데이터 보정과 사용자 안내가 필요할 수 있다.


빠른 롤백보다 안전한 중단 기준이 먼저 필요하다

자동 롤백은 유용하지만 모든 지표 변화에 즉시 반응하면 정상적인 일시 변동에도 배포가 반복해서 되돌아갈 수 있다.

중단 기준은 구체적으로 정의해야 한다.

type ReleaseGuard = {
  maximumErrorRate: number;
  maximumP95LatencyMs: number;
  minimumQuizSubmissionSuccessRate: number;
  observationMinutes: number;
};

이 타입은 배포를 계속할지 판단하기 위한 지표와 관찰 시간을 표현한다.

예를 들어 다음 상황에서는 배포 확장을 중단할 수 있다.

  • 새 버전 오류율이 이전 버전보다 의미 있게 높다.
  • 퀴즈 제출 성공률이 정한 기준 아래로 내려간다.
  • 데이터베이스 잠금 대기가 급증한다.
  • 영상 재생 시작 시간이 지속적으로 악화된다.
  • 데이터 정합성 검사에서 누락이 발견된다.

중단은 반드시 실패를 의미하지 않는다. 더 많은 사용자에게 영향을 주기 전에 판단을 보류하는 안전장치다.


작은 변경은 배포 원인과 복구 범위를 좁힌다

여러 기능, 의존성 업데이트와 데이터베이스 변경을 하나의 큰 배포에 포함하면 문제가 발생했을 때 원인을 찾기 어렵다.

퀴즈 기능 변경
+ 동영상 플레이어 교체
+ 인증 라이브러리 업데이트
+ 데이터베이스 구조 변경
+ 런타임 버전 변경

이 배포에서 오류가 발생하면 어느 변경이 원인인지 분리하기 어렵다. 이전 버전으로 돌아가면 서로 관련 없는 개선까지 모두 되돌아간다.

변경을 작게 나누면 다음 장점이 있다.

  • 검토 범위가 작아진다.
  • 테스트할 행동이 명확해진다.
  • 실패 원인을 찾기 쉬워진다.
  • 롤백 범위가 줄어든다.
  • 사용자 지표와 변경사항을 연결하기 쉬워진다.

배포 횟수를 줄이는 것이 항상 안전한 것은 아니다. 큰 변경을 드물게 배포하는 것보다 검증 가능한 작은 변경을 자주 전달하는 편이 위험을 작게 나눌 수 있다.


무중단 배포가 항상 가장 좋은 선택은 아니다

모든 서비스가 복잡한 롤링, 블루·그린이나 카나리 인프라를 필요로 하지는 않는다.

사용자가 적고 짧은 중단을 허용할 수 있는 내부 교육 서비스라면 공지된 유지보수 시간에 배포하는 편이 더 단순하고 안전할 수 있다.

중요한 것은 “무중단”이라는 기술적 목표가 아니라 다음 조건에 맞는 방식을 선택하는 일이다.

  • 사용자가 허용할 수 있는 중단 시간
  • 데이터 손실 가능성
  • 배포 빈도와 변경 크기
  • 운영 인력과 복구 능력
  • 추가 인프라 비용
  • 규제 또는 감사 요구사항

간단한 배포 방식도 백업, 검증, 공지와 복구 절차가 명확하면 적절한 설계가 될 수 있다.


배포를 설계하기 전에 물어봐야 할 질문

무엇을 사용자에게 전달하는가

  1. 이번 변경의 사용자 영향은 무엇인가?
  2. 코드, 데이터베이스, 설정과 외부 시스템 중 무엇이 바뀌는가?
  3. 변경사항을 하나의 버전으로 식별할 수 있는가?
  4. 스테이징에서 검증한 것과 같은 아티팩트를 배포하는가?
  5. 배포와 기능 공개를 분리해야 하는가?

어떤 조건을 통과해야 하는가

  1. 자동 테스트와 타입 검사가 성공했는가?
  2. 데이터베이스 마이그레이션을 검증했는가?
  3. 필요한 보안 검사가 포함되어 있는가?
  4. 운영 배포에 승인 절차가 필요한가?
  5. 배포 후 스모크 테스트가 정의되어 있는가?

전환 중에도 호환되는가

  1. 이전 서버와 새 서버가 동시에 실행될 수 있는가?
  2. 이전 프론트엔드가 새 API 응답을 이해할 수 있는가?
  3. 새 서버가 이전 데이터 구조를 읽을 수 있는가?
  4. 메시지 소비자와 생산자의 버전이 달라도 되는가?
  5. 제거하려는 필드를 사용하는 클라이언트가 남아 있지 않은가?

데이터베이스 변경을 단계적으로 적용하는가

  1. 기존 컬럼을 삭제하기 전에 새 구조를 추가했는가?
  2. 이전 코드와 새 코드가 함께 동작할 수 있는가?
  3. 기존 데이터 변환을 작은 배치로 실행할 수 있는가?
  4. 제약 조건을 강화하기 전에 데이터를 검증했는가?
  5. 데이터 변경을 실제로 되돌릴 수 있는가?

새 인스턴스가 요청을 받을 준비가 되었는가

  1. 프로세스 생존과 요청 준비 상태를 구분하는가?
  2. 준비 상태 검사가 필수 의존성을 확인하는가?
  3. 느린 시작을 장애로 오해해 반복 재시작하지 않는가?
  4. 종료 중인 인스턴스의 요청을 안전하게 마무리하는가?
  5. 충분한 정상 인스턴스를 유지하면서 교체하는가?

어떻게 위험을 나눌 것인가

  1. 한 번에 전체 트래픽을 전환해야 하는가?
  2. 내부 사용자나 일부 트래픽에 먼저 공개할 수 있는가?
  3. 카나리 그룹이 전체 사용자를 대표하는가?
  4. 기능 플래그로 위험 기능을 분리할 수 있는가?
  5. 트래픽 확대와 중단 기준이 명확한가?

무엇을 관찰하고 언제 중단하는가

  1. 배포 버전을 로그와 지표에 연결하는가?
  2. 오류율과 응답 시간뿐 아니라 핵심 사용자 행동도 측정하는가?
  3. 정상 범위와 중단 임계값이 정의되어 있는가?
  4. 데이터 누락과 중복을 감지할 수 있는가?
  5. 문제가 발생하면 담당자에게 즉시 전달되는가?

어떻게 복구할 것인가

  1. 이전 아티팩트를 다시 배포할 수 있는가?
  2. 기능 플래그만 끄는 것으로 영향을 차단할 수 있는가?
  3. 스키마와 데이터가 이전 코드와 호환되는가?
  4. 외부에 전달된 작업은 어떻게 보정하는가?
  5. 롤백할 수 없다면 어떤 전진 수정 절차가 있는가?

흔한 실수는 서버가 새 코드를 실행하면 배포가 끝났다고 생각하는 것이다

운영 환경에서 다시 빌드한다

스테이징에서 검증한 결과물과 운영 결과물이 달라질 수 있다.

한 번 만든 식별 가능한 아티팩트를 환경 사이에서 승격해야 한다.

테스트가 성공하면 운영에서도 안전하다고 생각한다

테스트는 알려진 조건을 검증하지만 실제 트래픽, 데이터 규모와 환경 차이를 모두 재현하지 못한다.

일부 트래픽 배포와 운영 지표 관찰이 필요하다.

기존 API 필드와 데이터베이스 컬럼을 즉시 제거한다

이전 버전이 배포 중 함께 실행되거나 오래된 클라이언트가 남아 있을 수 있다.

추가, 전환, 제거를 서로 다른 단계로 나눠야 한다.

준비되지 않은 서버에 바로 트래픽을 보낸다

프로세스가 실행 중이어도 데이터베이스 연결이나 초기화가 끝나지 않았을 수 있다.

준비 상태를 확인한 뒤 요청 대상으로 포함해야 한다.

배포 성공을 서버 상태만으로 판단한다

서버는 정상이어도 영상 재생, 퀴즈 제출이나 진행률 저장이 실패할 수 있다.

사용자의 핵심 흐름을 함께 관찰해야 한다.

데이터베이스 변경도 코드처럼 쉽게 롤백할 수 있다고 생각한다

삭제된 데이터와 외부로 전달된 작업은 이전 이미지를 실행한다고 복구되지 않는다.

변경별 복구 가능성을 배포 전에 확인해야 한다.

기능 플래그를 제거하지 않는다

오래된 플래그는 실행 경로와 테스트 조합을 계속 늘린다.

공개가 안정된 뒤 플래그와 이전 코드를 정리해야 한다.

큰 배포가 관리하기 쉽다고 생각한다

여러 변경을 한 번에 묶으면 배포 횟수는 줄지만 실패 원인과 복구 범위가 커진다.

독립적으로 검증하고 되돌릴 수 있는 크기로 변경을 나누는 편이 낫다.


배포의 목적은 변경사항을 안전하게 사용자에게 전달하는 데 있다

프로그래밍을 처음 배울 때는 배포를 개발한 코드를 서버에 올리는 일이라고 이해해도 충분하다.

await deploy({
  application: "learning-platform",
  environment: "production",
});

하지만 실제 서비스에서 배포는 업로드 명령이 성공한 뒤에도 계속된다.

  • 어떤 코드와 설정이 실제로 실행 중인가?
  • 검증한 것과 동일한 아티팩트를 배포했는가?
  • 이전 버전과 새 버전이 함께 동작할 수 있는가?
  • 데이터베이스 변경을 단계적으로 적용했는가?
  • 새 인스턴스가 요청을 받을 준비가 되었는가?
  • 일부 사용자에게 먼저 공개할 수 있는가?
  • 배포와 기능 공개를 분리할 필요가 있는가?
  • 어떤 지표로 성공과 실패를 판단하는가?
  • 어느 시점에 배포를 중단할 것인가?
  • 코드, 데이터와 외부 작업을 어떻게 복구할 것인가?

배포는 완성된 코드를 서버에 올리는 일이 아니다.

배포는 변경사항을 검증 가능한 단위로 만들고, 기존 서비스와의 호환성을 유지하며, 관찰과 복구가 가능한 방식으로 사용자에게 전달하는 과정이다.


어떤 자료를 참고했는가

profile
Vision eXperience Developer

0개의 댓글