테스트, 배포, 운영 전략 수립
온라인 코드 편집기의 단위·통합 테스트, 컨테이너 배포, SLO와 복구·보안 기준을 하나의 운영 전략으로 묶습니다.
지난 절에서는 NestJS REST API와 React 파일 편집 화면을 연결하는 기본 흐름을 살펴보았습니다. 예제에는 생략한 준비 코드와 미완성 협업 경로가 있으므로, 먼저 실제로 확인한 기능과 앞으로 확인할 기능을 나누어 보겠습니다.
이제 실전 프로젝트의 마지막 단계로, 개발된 애플리케이션의 품질을 확보하고, 사용자에게 안정적으로 서비스를 제공하며, 지속적으로 운영 및 개선하기 위한 전략을 수립하는 시간을 갖겠습니다.
테스트, 배포, 운영은 소프트웨어 개발 생명주기(SDLC)에서 개발만큼 중요한 부분입니다.
이 단계들을 계획해야 구현 결과를 검증 가능한 서비스 운영 흐름으로 연결할 수 있습니다. 기본 확인은 로그인한 소유자가 준비한 파일을 편집·저장하고 다시 조회하는 것입니다. 두 사용자의 실시간 반영·공유 권한은 협업 확장을 구현한 뒤 별도로 검증합니다.
테스트 전략 수립
테스트는 소프트웨어의 버그를 줄이고, 기능이 요구사항에 맞게 동작하는지 확인하며, 미래의 변경 사항으로부터 시스템을 보호하는 데 필수적입니다.
이 절의 테스트 코드는 Jest를 구성한 프로젝트를 전제로 합니다. 현재 Nest TypeScript starter는 Vitest를 사용하므로, 생성한 프로젝트의 runner·설정과 예제의 jest.fn()·Supertest 설정을 먼저 맞춰야 합니다.
백엔드 (NestJS) 테스트
- 목표: 개별 함수, 클래스, 메서드와 같이 애플리케이션의 가장 작은 단위가 예상대로 동작하는지 검증합니다. 외부 의존성(데이터베이스, 외부 API 등)은 모킹(Mocking) 처리합니다.
- 대상: 서비스(Service), 유틸리티 함수, 엔티티의 비즈니스 로직, DTO 유효성 검사 등.
- Jest 활용: NestJS CLI는 서비스, 컨트롤러, 게이트웨이 등을 생성할 때 기본 단위 테스트 스펙 파일(
.spec.ts)을 함께 생성합니다.src/auth/auth.service.spec.ts (예시) import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; describe('AuthService', () => { let service: AuthService; let usersRepository: any; // Mock Repository let jwtService: JwtService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, { provide: getRepositoryToken(User), useValue: { findOne: jest.fn(), create: jest.fn(), save: jest.fn(), }, }, { provide: JwtService, useValue: { sign: jest.fn(() => 'mock_jwt_token'), }, }, { provide: ConfigService, useValue: { get: jest.fn((key: string) => { if (key === 'JWT_SECRET') return 'test_secret'; return null; }), }, }, ], }).compile(); service = module.get<AuthService>(AuthService); usersRepository = module.get(getRepositoryToken(User)); jwtService = module.get<JwtService>(JwtService); }); it('should be defined', () => { expect(service).toBeDefined(); }); it('should register a user', async () => { usersRepository.findOne.mockResolvedValue(null); // 기존 사용자 없음 usersRepository.create.mockReturnValue({ id: '1', email: '[email protected]' }); usersRepository.save.mockResolvedValue({ id: '1', email: '[email protected]' }); const result = await service.register({ email: '[email protected]', password: 'password123', nickname: 'Tester', }); expect(result).toEqual({ message: 'User registered successfully' }); expect(usersRepository.create).toHaveBeenCalledWith( expect.objectContaining({ email: '[email protected]' }) ); }); it('should throw ConflictException if email already registered', async () => { usersRepository.findOne.mockResolvedValue({}); // 기존 사용자 존재 await expect( service.register({ email: '[email protected]', password: 'password123', nickname: 'Tester', }) ).rejects.toThrow('Email already registered'); }); // ... 로그인 테스트 등 });
- 목표: 여러 컴포넌트(컨트롤러, 서비스, 리포지토리)가 함께 동작하며 예상대로 통합되는지 검증합니다. 실제 데이터베이스 또는 외부 서비스의 모의 객체를 사용합니다.
- 대상: API 엔드포인트, 미들웨어, 가드, 데이터베이스 상호작용 등.
- Jest + Supertest 활용: NestJS는 통합 테스트를 위해
E2E Testing섹션에서app.e2e-spec.ts파일을 제공하며,supertest라이브러리를 사용하여 HTTP 요청을 시뮬레이션합니다.src/project/project.controller.e2e-spec.ts (간략화된 예시) import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { AppModule } from '../app.module'; import { AuthModule } from '../auth/auth.module'; import { ProjectModule } from './project.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../auth/entities/user.entity'; import { Project } from './entities/project.entity'; import { File } from '../file/entities/file.entity'; import { Folder } from '../folder/entities/folder.entity'; import { AuthService } from '../auth/auth.service'; import { JwtService } from '@nestjs/jwt'; describe('ProjectController (e2e)', () => { let app: INestApplication; let authService: AuthService; let jwtService: JwtService; let accessToken: string; let testUser: User; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ AppModule, // 전체 AppModule 임포트 (통합 테스트용 DB 설정) TypeOrmModule.forRoot({ // 테스트용 인메모리 DB 또는 별도 DB 설정 type: 'sqlite', // 개발용으로 sqlite 사용 database: ':memory:', entities: [User, Project, File, Folder], synchronize: true, }), AuthModule, ProjectModule, ], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); authService = moduleFixture.get<AuthService>(AuthService); jwtService = moduleFixture.get<JwtService>(JwtService); // 테스트 유저 생성 및 토큰 발급 testUser = await authService['usersRepository'].save({ // private 접근 주의 email: '[email protected]', password: await (authService as any)['bcrypt'].hash('password123', 10), nickname: 'TestUser', }); accessToken = (await authService.login({ email: '[email protected]', password: 'password123' })).accessToken; }); afterAll(async () => { await app.close(); }); it('/projects (POST) - creates a project', async () => { return request(app.getHttpServer()) .post('/projects') .set('Authorization', `Bearer ${accessToken}`) .send({ name: 'My Test Project' }) .expect(201) .expect((res) => { expect(res.body.name).toEqual('My Test Project'); expect(res.body.ownerId).toEqual(testUser.id); }); }); it('/projects (GET) - gets all user projects', async () => { await request(app.getHttpServer()) .post('/projects') .set('Authorization', `Bearer ${accessToken}`) .send({ name: 'Another Project' }); return request(app.getHttpServer()) .get('/projects') .set('Authorization', `Bearer ${accessToken}`) .expect(200) .expect((res) => { expect(res.body.length).toBeGreaterThanOrEqual(2); expect(res.body[0].ownerId).toEqual(testUser.id); }); }); // ... 기타 프로젝트, 파일, 폴더 API 테스트 });
- 목표: 실제 사용자 시나리오를 모방하여 전체 시스템(프론트엔드, 백엔드, 데이터베이스)이 완벽하게 통합되어 동작하는지 검증합니다.
- 도구: Cypress, Playwright, Selenium 등.
- 기본 적용: 사용자 가입 -> 로그인 -> 프로젝트 생성 -> API로 파일 준비 -> 파일 편집 -> REST 저장 -> 재조회 확인.
- 협업 확장: Gateway와 원격 적용을 구현한 뒤, 두 브라우저의 변경 반영·권한 거부·연결 실패를 추가로 확인합니다.
통합 테스트 코드는 구조를 설명하는 미완성 fixture입니다. AppModule의 기존 DB 설정과 테스트 DB의 중복 등록을 분리하고, AuthService에 없는 private bcrypt 접근을 직접 bcrypt import 또는 공개 등록 경로로 바꾼 뒤 검증해야 합니다. 이 예제만으로 통합 테스트가 통과했다고 판단하지 않습니다.
예시
가입 단위 테스트의 저장소 save는 mockResolvedValue()로 성공 값을 돌려줍니다. 이 테스트에서 등록 메시지와 create 호출을 확인해도 PostgreSQL에 사용자 행이 남았다는 증거는 아닙니다. 실제 저장 확인에는 DB가 연결된 등록 요청 뒤 같은 사용자를 조회하는 별도 관측이 필요합니다. 모의 객체로 확인한 서비스 분기와 실제 DB·HTTP·브라우저까지 연결한 범위를 나눠 결과를 기록하세요.
프론트엔드 (React) 테스트
- 단위 테스트: 컴포넌트의 렌더링, 이벤트 처리 등 개별 UI 컴포넌트의 동작을 Jest + React Testing Library로 테스트합니다.
- 통합 테스트: 여러 컴포넌트가 결합된 페이지의 동작을 테스트합니다. API 모킹을 통해 백엔드 의존성을 제거합니다.
- E2E 테스트: 백엔드와 마찬가지로 Cypress나 Playwright를 사용하여 사용자 관점에서 전체 애플리케이션의 흐름을 테스트합니다.
테스트 자동화 (CI)
- 지속적 통합 (CI): GitHub Actions, GitLab CI/CD, Jenkins와 같은 CI 도구를 사용하여 코드가 저장소에 푸시될 때마다 자동으로 테스트를 실행하고 코드 품질을 검증합니다. 모든 테스트가 통과해야만 다음 단계(배포)로 넘어갈 수 있도록 설정합니다.
배포 전략 수립 (Deployment Strategy)
안정적이고 효율적인 배포는 서비스 운영의 핵심입니다.
환경 구성
- 개발 환경 (Development): 개발자가 로컬에서 코드 작성 및 테스트를 수행하는 환경. Docker Compose를 사용하여 PostgreSQL, Redis 등을 쉽게 실행할 수 있도록 구성합니다.
- 스테이징 환경 (Staging): 프로덕션 환경과 최대한 유사하게 구성하여 최종 테스트 및 QA를 수행하는 환경.
- 운영 환경 (Production): 실제 사용자들이 접근하는 서비스 환경. 고가용성, 확장성, 보안을 최우선으로 고려합니다.
백엔드 (NestJS) 배포
아래 Node 20 Dockerfile은 축약 예제입니다. 현재 지원 runtime을 선택하고, builder에는 TypeScript·Nest CLI 등 빌드 의존성을 설치해야 합니다. npm install --omit=dev만 수행하면 build에 필요한 개발 의존성이 빠질 수 있으므로, 전체 의존성으로 빌드한 뒤 별도 runtime 단계에 production 의존성을 구성해 검증하세요.
- 컨테이너화: NestJS 애플리케이션을 Docker 이미지로 빌드합니다.
Dockerfile을 작성하여 Node.js 런타임과 애플리케이션 코드를 포함시킵니다.# Dockerfile (간략화) # 빌드 환경 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev # 프로덕션 종속성만 설치 COPY . . RUN npm run build # NestJS 애플리케이션 빌드 # 실행 환경 FROM node:20-alpine WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY package.json ./package.json EXPOSE 3000 CMD ["node", "dist/main"] -
클라우드 서비스 선택
- AWS ECS (Fargate): 서버리스 컨테이너 서비스로, 인프라 관리가 적고 확장성이 뛰어납니다. WebSocket 지원.
- Google Cloud Run: 컨테이너를 서버리스 방식으로 실행하는 서비스로, 자동 스케일링과 HTTP/WebSocket 지원이 강력합니다.
- Kubernetes (EKS/GKE): 더 세밀한 제어와 복잡한 오케스트레이션이 필요할 때 선택. (초기 단계에서는 오버헤드가 클 수 있음)
- 데이터베이스 배포: AWS RDS (PostgreSQL) 또는 Google Cloud SQL (PostgreSQL)과 같은 관리형 데이터베이스 서비스를 사용합니다. 안정성, 백업, 복구, 확장성을 자동으로 처리해 줍니다.
- Redis 배포: AWS ElastiCache (Redis) 또는 Google Cloud Memorystore (Redis)와 같은 관리형 캐싱 서비스를 사용합니다.
-
CI/CD 파이프라인
- 소스 코드 관리: GitHub, GitLab.
- CI 도구: GitHub Actions, GitLab CI/CD, AWS CodePipeline.
-
워크플로우 예시
개발자가
main브랜치에 코드 푸시.CI 도구가 자동으로 트리거.
코드 빌드, 단위/통합 테스트 실행.
테스트 통과 시 Docker 이미지 빌드 및 컨테이너 레지스트리(ECR, GCR, Docker Hub)에 푸시.
CD 도구가 새 이미지를 사용하여 스테이징 환경에 배포.
QA 및 최종 승인 후, 동일 이미지를 운영 환경에 배포.
프론트엔드 (React) 배포
- 정적 파일 호스팅: React 애플리케이션은 빌드 시 정적 HTML, CSS, JavaScript 파일로 변환됩니다.
-
클라우드 서비스 선택
- AWS S3 + CloudFront: S3에 빌드된 정적 파일을 호스팅하고, CloudFront (CDN)를 사용하여 전 세계 사용자에게 빠르게 콘텐츠를 전송합니다. HTTPS 설정 및 캐싱에 유리합니다.
- Google Cloud Storage + Cloud CDN: AWS S3/CloudFront와 유사한 기능 제공.
- Vercel, Netlify: 개발자 친화적인 배포 플랫폼으로, Git 저장소 연동을 통해 CI/CD를 쉽게 구축할 수 있습니다.
- CI/CD 파이프라인: 백엔드와 유사하게 Git 푸시 시 자동으로 빌드되고 정적 호스팅 서비스로 배포되도록 설정합니다.
운영 전략 수립
서비스를 배포한 후에도 안정적으로 운영하고 지속적으로 개선하기 위한 전략이 필요합니다.
모니터링 및 로깅
-
로깅
- 백엔드: NestJS의 내장 로깅 기능을 활용하거나, Winston, Pino와 같은 라이브러리를 사용하여 구조화된 로그를 생성합니다. 로그는 CloudWatch Logs, Google Cloud Logging, ELK Stack(Elasticsearch, Logstash, Kibana)과 같은 중앙 집중식 로깅 시스템으로 전송합니다.
- 프론트엔드: 브라우저 콘솔 로그 외에, Sentry, LogRocket, Datadog RUM과 같은 클라이언트 측 에러 모니터링 도구를 사용하여 사용자 경험에 영향을 미치는 문제를 추적합니다.
-
모니터링
- 인프라 모니터링: CPU 사용량, 메모리 사용량, 네트워크 트래픽, 디스크 사용량 등을 클라우드 공급자(AWS CloudWatch, Google Cloud Monitoring)의 기본 모니터링 도구로 추적합니다.
- 애플리케이션 모니터링 (APM): 서비스의 응답 시간, 에러율, 트랜잭션 수 등을 New Relic, Datadog APM, Prometheus + Grafana와 같은 도구로 모니터링합니다. NestJS 애플리케이션의 특정 엔드포인트 성능, 데이터베이스 쿼리 시간 등을 측정합니다.
- 알림 (Alerting): 정의된 임계값을 초과할 때(예: 에러율 5% 이상, CPU 사용량 80% 이상) 개발팀에 자동으로 알림(Slack, Email, PagerDuty)이 전송되도록 설정합니다.
운영 기준선 (SLO/RTO/RPO) 정의
운영은 도구보다 목표 수치가 먼저입니다.
- SLO: 예) API p95 300 ms 이하, 월간 성공률 99.9% 이상
- RTO (Recovery Time Objective): 장애 발생 후 서비스 복구 허용 시간. 예) 30분
- RPO (Recovery Point Objective): 데이터 손실 허용 범위. 예) 5분
배포/장애 대응 정책은 위 수치를 기준으로 의사결정해야 합니다.
예를 들어 SLO 에러 버짓 소진 속도가 빠르면 기능 배포를 동결하고 성능/안정화 작업을 우선합니다.
운영 수치 계산해 보기
계산 연습에서는 월간 성공률을 성공 요청 수/전체 요청 수로 정의하고 전체 요청이 100,000건이라고 합시다. SLO 99.9%를 만족하려면 성공 요청은 최소 99,900건, 실패 요청은 최대 100건입니다. 실패가 이미 80건이면 이번 계산 조건에서 남은 실패 예산은 20건입니다. 관측 창·성공 판정이 다르면 예산도 다시 정해야 합니다.
복구 판단 문제
장애 뒤 서비스 복구에 20분이 걸렸고 복원된 데이터가 장애 직전보다 8분 오래되었습니다. 위 예시의 RTO 30분과 RPO 5분 중 어느 목표를 만족하나요? 정상 응답이 돌아왔다는 이유로 데이터 손실 목표도 만족했다고 볼 수 있나요?
시간의 기준을 나누어 확인
복구에 걸린 20분은 RTO 30분 이내입니다. 장애 직전과 복원 데이터의 차이 8분은 RPO 5분을 넘었습니다. 따라서 서비스 복구 시간 목표는 만족하지만 데이터 손실 범위 목표는 만족하지 못합니다. 이 수치는 조건을 둔 연습이며 실제 백업이나 장애 복구를 실행한 결과가 아닙니다.
트래픽 보호 패턴 (Rate Limiting / Circuit Breaker / Idempotency)
운영 환경에서는 공격/급증 트래픽, 다운스트림 장애, 중복 요청을 동시에 방어해야 합니다.
- Rate Limiting: 엔드포인트별 요청 한도를 강제해 과도한 트래픽을 제어합니다.
- Circuit Breaker: 외부 의존 서비스 실패가 누적될 때 오류를 빠르게 반환하는 방식(fail-fast)으로 전환해 전체 장애 전파를 막습니다.
- Idempotency Key: 결제/주문 API에서 중복 제출이 와도 결과를 한 번만 반영합니다.
import { ThrottlerGuard, Throttle } from '@nestjs/throttler';
import { UseGuards, Controller, Post } from '@nestjs/common';
@UseGuards(ThrottlerGuard)
@Controller('payments')
export class PaymentController {
@Post()
@Throttle({ default: { limit: 10, ttl: 60_000 } }) // 1분에 10회
createPayment() {
return { ok: true };
}
}// 1) 요청 헤더의 Idempotency-Key를 수신
// 2) (userId, key) 조합으로 Redis/DB에 처리 상태 저장
// 3) 이미 성공 처리된 키면 기존 응답 반환, 아니면 비즈니스 로직 수행 후 결과 저장보안 관리
- 정기적인 보안 업데이트: Node.js 런타임, NestJS 및 모든 npm 패키지를 최신 보안 패치가 적용된 버전으로 유지합니다. Dependabot과 같은 도구를 사용하여 취약점을 자동으로 감지하고 알립니다.
- 환경 변수 관리: 민감한 정보(DB 비밀번호, API 키, JWT Secret)는 코드에 직접 하드코딩하지 않고, 환경 변수, AWS Secrets Manager, Google Secret Manager와 같은 보안 서비스를 통해 관리합니다.
- WAF (Web Application Firewall): AWS WAF, Cloudflare WAF 등을 사용하여 SQL Injection, XSS와 같은 일반적인 웹 공격으로부터 애플리케이션을 보호합니다.
- HTTPS/WSS 강제: 모든 클라이언트-서버 통신은 TLS/SSL (HTTPS/WSS)을 통해 암호화되도록 강제합니다.
백업 및 복구
- 데이터베이스 백업: PostgreSQL 데이터베이스의 정기적인 자동 백업을 설정하고, 복구 전략을 수립합니다. (AWS RDS의 자동 백업 기능 활용)
- 데이터 복원 테스트: 주기적으로 백업된 데이터로 복원 테스트를 수행하여 복구 절차가 정상적으로 작동하는지 확인합니다.
스케일링 전략
- 자동 스케일링: 사용자 트래픽 변화에 따라 백엔드 서버 인스턴스(ECS Fargate, Cloud Run)가 자동으로 늘어나거나 줄어들도록 설정합니다.
- 데이터베이스 확장: 읽기 부하가 커지면 읽기 복제본(Read Replicas)을 추가하고, 쓰기 부하가 커지면 샤딩(Sharding) 또는 클러스터링을 고려합니다.
- WebSocket 스케일링: Socket.IO Redis Adapter를 사용하여 여러 NestJS WebSocket 서버 인스턴스 간에 이벤트가 동기화되도록 구성합니다.
이번 13장에서는 온라인 코드 에디터의 인증·개인 프로젝트·파일 REST API와 React 편집 화면을 연결하는 과정을 살펴보았습니다. 기본 목표는 편집 내용을 저장하고 다시 조회하는 흐름입니다.
실시간 협업·파일 생성 UI·폴더 트리와 운영 배포는 남은 확장입니다. 이 절의 테스트 fixture와 Dockerfile도 준비·보완이 필요한 예시이므로, 실제 실행 결과와 관측값을 남긴 뒤 완료를 판단합니다.
소프트웨어 개발은 단순히 코드를 작성하는 것을 넘어, 비즈니스 요구사항을 이해하고, 효율적으로 설계하며, 품질을 보증하고, 최종적으로 사용자에게 가치를 전달하는 전체 과정입니다.
이 장의 실전 프로젝트 흐름은 다음 프로젝트를 설계할 때 참고할 수 있습니다.
마지막으로 운영 환경에 올리기 전 확인해야 할 기준을 다음 다이어그램처럼 한 장의 점검 보드로 정리할 수 있습니다.
배포 승인 기준은 기능 완성뿐 아니라 실패를 탐지하고 되돌릴 준비가 되었는지까지 포함해야 합니다.
이 기준을 실제 운영 루프로 옮기면 검증, 배포, 관측, 복구가 반복됩니다.
마지막으로 테스트와 배포 절차를 하나의 릴리스 흐름으로 묶어, 변경 위험을 분류하고 필요한 검증과 배포 방식을 선택하는 과정을 정리합니다.
검증한 이미지를 배포하고 운영 결과로 개선하는 흐름
계획된 build/test·staging 승인·동일 image 운영·SLO 관측·복구/개선·재검증으로 릴리스 근거가 바뀐다.
Nest · Release strategy
검증·배포·관측·개선 루프
단위·통합 테스트 뒤 만든 이미지를 스테이징에서 승인하고 같은 이미지를 운영에 배포하는 전략입니다. SLO·RTO·RPO와 정기 복원 테스트는 준비할 기준이며, 예제 실행이나 검증 완료를 뜻하지 않습니다.
- 빌드
코드 빌드와 단위·통합 테스트 후 이미지를 만듭니다.
- 스테이징
같은 이미지를 배포하고 QA 및 최종 승인을 거칩니다.
- 복구 준비
정기 복원 테스트와 장애 시 복구 목표·절차를 준비합니다.
- 운영 배포
승인한 이미지를 운영에 배포합니다. 노출 단계는 환경에 맞게 정합니다.
- SLI 관측
오류율과 응답 시간을 정한 SLO에 대조합니다.
- 복구·개선
장애에 대응하고 관측 결과를 다음 개선 작업에 반영합니다.
실선은 배포 전략의 진행 순서, 점선은 관측 후 다음 개선으로 돌아가는 경로입니다. 복원 테스트는 정기 운영 계획이며 매 배포에서 수행했다는 증거는 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 계획된 빌드·테스트와 스테이징 승인 뒤 같은 이미지를 운영에 배포하고 SLO 관측을 복구·개선과 다음 검증에 연결한다. RTO·RPO와 실제 복원 결과는 따로 확인한다.
- 빌드·스테이징 검증·복구 준비·운영 배포·관측·개선을 연결한다. 복원 테스트는 정기 운영 계획이며 각 배포에서 실행됐다는 관측을 뜻하지 않는다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| build·staging 승인 계획된 build·test 뒤 staging에서 승인합니다. | 대상: 검증 image · 근거: staging 승인 | 운영 입력으로 사용할 image가 정해집니다. |
| 같은 image 배포 승인한 같은 image를 운영에 배포합니다. | 운영: 동일 image · 상태: 배포 | 검증 대상과 실행 대상을 연결합니다. |
| SLO·복구·개선 SLO 관측 결과를 복구·개선과 다음 검증에 반영합니다. | 자료: SLO · RTO·RPO: 실제 복원 별도 | 운영 증거가 후속 검증 계획을 바꿉니다. |
검증한 이미지를 배포하고 운영 결과로 개선하는 흐름
계획된 build/test·staging 승인·동일 image 운영·SLO 관측·복구/개선·재검증으로 릴리스 근거가 바뀐다.
Nest · Release strategy
검증·배포·관측·개선 루프
단위·통합 테스트 뒤 만든 이미지를 스테이징에서 승인하고 같은 이미지를 운영에 배포하는 전략입니다. SLO·RTO·RPO와 정기 복원 테스트는 준비할 기준이며, 예제 실행이나 검증 완료를 뜻하지 않습니다.
- 빌드
코드 빌드와 단위·통합 테스트 후 이미지를 만듭니다.
- 스테이징
같은 이미지를 배포하고 QA 및 최종 승인을 거칩니다.
- 복구 준비
정기 복원 테스트와 장애 시 복구 목표·절차를 준비합니다.
- 운영 배포
승인한 이미지를 운영에 배포합니다. 노출 단계는 환경에 맞게 정합니다.
- SLI 관측
오류율과 응답 시간을 정한 SLO에 대조합니다.
- 복구·개선
장애에 대응하고 관측 결과를 다음 개선 작업에 반영합니다.
실선은 배포 전략의 진행 순서, 점선은 관측 후 다음 개선으로 돌아가는 경로입니다. 복원 테스트는 정기 운영 계획이며 매 배포에서 수행했다는 증거는 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 계획된 빌드·테스트와 스테이징 승인 뒤 같은 이미지를 운영에 배포하고 SLO 관측을 복구·개선과 다음 검증에 연결한다. RTO·RPO와 실제 복원 결과는 따로 확인한다.
- 빌드·스테이징 검증·복구 준비·운영 배포·관측·개선을 연결한다. 복원 테스트는 정기 운영 계획이며 각 배포에서 실행됐다는 관측을 뜻하지 않는다.
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| build·staging 승인 계획된 build·test 뒤 staging에서 승인합니다. | 대상: 검증 image · 근거: staging 승인 | 운영 입력으로 사용할 image가 정해집니다. |
| 같은 image 배포 승인한 같은 image를 운영에 배포합니다. | 운영: 동일 image · 상태: 배포 | 검증 대상과 실행 대상을 연결합니다. |
| SLO·복구·개선 SLO 관측 결과를 복구·개선과 다음 검증에 반영합니다. | 자료: SLO · RTO·RPO: 실제 복원 별도 | 운영 증거가 후속 검증 계획을 바꿉니다. |
실패율의 분모와 중복 처리의 원자성
전체 평균만 보면 새 버전의 문제가 희석될 수 있습니다. 같은 관측 창에 요청 10,000건 중 새 버전 500건에서만 실패 5건이 있었다고 합시다. 전체 실패율은 5/10,000 = 0.05%이고 새 버전 실패율은 5/500 = 1%입니다. 전체 성공률 99.95%는 예시 SLO 99.9%를 만족하지만, 새 버전 실패율이 0.2% 이하라는 별도 승격 조건을 택했다면 새 버전은 그 조건을 통과하지 못합니다. 이 숫자는 구성한 계산 예제로 실제 배포 관측이 아닙니다. 승격 판단에는 관측 창, 버전별 분모와 최소 표본 수를 함께 정합니다.
실패 예산도 분모가 바뀌면 다시 계산합니다. 앞의 100,000건·실패 80건에서 요청이 1,000건 늘고 그중 10건이 실패했다면 누적 실패는 90건, 전체는 101,000건입니다. 같은 요청 기반 SLO 99.9%에서는 허용 실패가 최대 101건이므로 남은 예산은 11건입니다. 처음 계산한 20건에서 새 실패 10건만 빼서 10건이라고 보고하면 새 분모를 반영하지 않은 셈입니다. 시간 기반 가용성과 요청 기반 성공률의 예산은 별도로 정의합니다.
앞의 Idempotency Key 주석도 단순 조회→처리→결과 저장만으로 동시 중복을 막는 구현이 되지는 않습니다. 요청 A와 B가 같은 (userId, key)를 동시에 조회해 모두 “없음”을 본 뒤 각각 업무 변경을 수행하면 변경이 두 번 발생할 수 있습니다. 처리 권한을 원자적으로 선점하고, 같은 키에 다른 요청 본문이 들어왔는지도 확인하는 계약이 필요합니다. DB 업무 변경이 성공했는데 완료 기록을 남기기 전에 프로세스가 종료되는 경계도 다뤄야 합니다. 같은 DB에서 처리한다면 업무 변경과 완료 기록을 한 트랜잭션에 묶는 방식을 설계할 수 있고, 외부 시스템의 효과까지 포함하면 그 시스템과의 중복 방지·재조정 계약이 추가로 필요합니다. 예제의 주석은 이러한 구현과 검증을 대신하지 않습니다.
통합 fixture의 Supertest는 esModuleInterop을 사용하는 TypeScript 구성에서 기본 import로 호출하도록 맞췄습니다. 이는 공식 Nest 테스트 예제의 import 형태와 일치합니다. DB 중복 등록과 private bcrypt 접근을 보완해야 한다는 기존 fixture 한계는 여전히 남습니다. 이 import 교정만으로 Jest·HTTP·DB 통합 테스트가 통과했다고 판단하지 않습니다.
운영 판단을 계산하는 문제
문제 1. 위 표본의 전체 오류율이 0.05%이므로 새 버전 오류율도 0.2% 이내라고 보고하고 승격해도 되나요?
풀이. 안 됩니다. 새 버전의 분모는 500건이고 실패는 5건이므로 1%입니다. 전체 성공률 목표 통과와 새 버전 승격 조건 통과를 별도로 판단합니다. 표본 수나 관측 기간이 부족하면 추가 관측이 필요한지도 정한 계약에 따라 확인합니다.
문제 2. 전체 요청 101,000건·실패 90건에서 요청 성공률 SLO가 99.9%이면 남은 실패 예산은 얼마인가요?
풀이. 허용 실패는 101,000 × 0.001 = 101건이고 남은 예산은 101 − 90 = 11건입니다. 계산에는 같은 관측 창의 누적 분모와 실패 수를 사용합니다. 실패 예산 11건이 새로운 배포의 성공을 보장하는 수치는 아닙니다.
문제 3. 업무 변경이 DB에 저장된 뒤 완료 키 기록 전에 프로세스가 종료되었습니다. 재시도에서 키가 없다고 다시 업무 변경을 해도 중복 방지가 보장되나요?
풀이. 보장되지 않습니다. 첫 변경은 이미 반영되었을 수 있어서 두 번째 변경이 중복 효과를 만들 수 있습니다. 같은 DB의 변경과 키 완료 기록을 원자적으로 커밋하는 계약이나, 이미 발생한 효과를 조회·재조정하는 절차가 필요합니다. 요청 두 개의 동시 선점과 장애 뒤 재시도는 서로 다른 경계 사례이므로 각각 검증합니다. 외부 효과가 있다면 DB 트랜잭션 하나만으로 외부 효과까지 원자적이라고 설명하지 않습니다.