5.14 AI 활용 DevOps & CI/CD

DevOps + AI = 자동화의 극대화: DevOps는 개발(Dev)과 운영(Ops)을 통합하여 소프트웨어 배포 주기를 단축하는 문화이자 방법론입니다. AI 코딩 도구를 활용하면 CI/CD 파이프라인 구성, Docker 설정, 배포 스크립트 작성을 자연어 한 줄로 처리할 수 있습니다.

CI/CD 파이프라인 전체 흐름

graph LR A[코드 Push] --> B[CI: 빌드] B --> C[CI: 테스트] C --> D{테스트 통과?} D -->|성공| E[CD: 스테이징 배포] D -->|실패| F[개발자 알림] E --> G[CD: 프로덕션 배포] G --> H[모니터링] H -->|이상 감지| I[자동 롤백] style D fill:#ff9800,color:#fff style G fill:#4caf50,color:#fff style I fill:#f44336,color:#fff

GitHub Actions 기초

GitHub Actions는 GitHub 저장소에서 자동화 워크플로우를 실행하는 CI/CD 플랫폼입니다. YAML 파일로 정의하며, push/PR 이벤트에 반응합니다.

Claude Code로 CI/CD 워크플로우 생성
Node.js 프로젝트에 GitHub Actions CI/CD를 구성해줘.

요구사항:
1. main 브랜치 push 시 자동 실행
2. PR 생성 시 테스트만 실행
3. Node.js 20 버전, npm ci 사용
4. ESLint + Jest 테스트 실행
5. 테스트 통과 시 Docker 이미지 빌드 → GitHub Container Registry 푸시
6. 프로덕션은 수동 승인 후 배포

.github/workflows/ 디렉토리에 파일 생성해줘.

실전 GitHub Actions 워크플로우 (Node.js)

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Docker 기초 — AI로 Dockerfile 생성

Claude Code로 Docker 설정 생성
이 Node.js Express 프로젝트에 Docker 설정을 만들어줘.
- 멀티스테이지 빌드로 이미지 최소화
- 보안: non-root 사용자 실행
- 헬스체크 포함
- docker-compose로 앱 + PostgreSQL + Redis 구성
- .dockerignore도 생성
# Dockerfile (멀티스테이지 빌드)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine

volumes:
  pgdata:

배포 전략 비교

전략 설명 장점 단점 적합한 상황
Rolling Update 인스턴스를 하나씩 순차 교체 추가 리소스 불필요, 간단 배포 중 구/신 버전 공존 일반적인 웹 서비스
Blue-Green 구(Blue)/신(Green) 환경을 동시 운영 후 전환 즉시 롤백 가능, 다운타임 제로 인프라 비용 2배 무중단 배포 필수 서비스
Canary 트래픽 일부(5~10%)만 신 버전으로 전환 후 확대 점진적 검증, 리스크 최소화 모니터링 체계 필수 대규모 사용자 서비스
graph TD subgraph "Blue-Green 배포" LB1[로드 밸런서] --> B1[Blue v1.0 - 운영 중] LB1 -.->|전환| G1[Green v2.0 - 대기] end subgraph "Canary 배포" LB2[로드 밸런서] -->|90%| C1[v1.0 기존] LB2 -->|10%| C2[v2.0 카나리] end style B1 fill:#2196f3,color:#fff style G1 fill:#4caf50,color:#fff style C2 fill:#ff9800,color:#fff

실습 과제

Claude Code에게 여러분의 프로젝트에 맞는 GitHub Actions 워크플로우를 생성해달라고 요청해보세요. "우리 프로젝트는 Spring Boot + Gradle이야. PR 시 테스트, main push 시 Docker 빌드 + ECR 푸시하는 워크플로우 만들어줘"처럼 구체적으로 요청하면 바로 사용 가능한 YAML을 생성합니다.

5.15 AI 활용 테스팅 전략

AI가 바꾸는 테스트 문화: 테스트 코드 작성은 개발자들이 가장 미루기 쉬운 작업입니다. AI 코딩 도구를 활용하면 기존 코드에서 테스트를 자동 생성하고, 엣지 케이스를 발견하며, TDD 워크플로우를 가속화할 수 있습니다. "테스트 코드를 짜는 시간"이 아니라 "테스트 코드를 검토하는 시간"으로 바뀝니다.

테스트 종류별 비교

테스트 종류 범위 속도 비용 도구 (JS / Java) 커버리지 목표
단위 테스트 (Unit) 함수/메서드 단위 매우 빠름 낮음 Jest, Vitest / JUnit 5 80%+
통합 테스트 (Integration) 모듈 간 연동 보통 중간 Supertest / Spring Boot Test 60%+
E2E 테스트 (End-to-End) 사용자 시나리오 전체 느림 높음 Playwright / Selenium 핵심 시나리오 100%

AI 기반 TDD 워크플로우

graph TD A["1. 요구사항 정의"] --> B["2. AI에게 테스트 코드 생성 요청"] B --> C["3. 테스트 실행 → 실패 확인 (Red)"] C --> D["4. AI에게 구현 코드 생성 요청"] D --> E["5. 테스트 실행 → 통과 확인 (Green)"] E --> F["6. AI에게 리팩토링 요청"] F --> G["7. 테스트 재실행 → 통과 확인"] G --> H{다음 기능?} H -->|있음| A H -->|완료| I["릴리즈 준비"] style C fill:#f44336,color:#fff style E fill:#4caf50,color:#fff style F fill:#2196f3,color:#fff
Claude Code로 테스트 자동 생성
src/services/userService.js 파일의 모든 public 함수에 대해
Jest 단위 테스트를 작성해줘.

요구사항:
1. 정상 케이스 + 에러 케이스 모두 포함
2. 외부 의존성(DB, API)은 mock 처리
3. 경계값(빈 문자열, null, 0, 음수) 테스트 포함
4. describe/it 구조로 가독성 있게 작성
5. 각 테스트에 한국어 설명 주석 추가

Jest 단위 테스트 예시 (Node.js)

// __tests__/userService.test.js
const { createUser, findUserById } = require('../src/services/userService');
const userRepository = require('../src/repositories/userRepository');

// DB 의존성 mock
jest.mock('../src/repositories/userRepository');

describe('UserService', () => {
  afterEach(() => jest.clearAllMocks());

  describe('createUser', () => {
    it('정상적인 사용자를 생성한다', async () => {
      const input = { name: '홍길동', email: 'hong@test.com' };
      userRepository.save.mockResolvedValue({ id: 1, ...input });

      const result = await createUser(input);

      expect(result.id).toBe(1);
      expect(result.name).toBe('홍길동');
      expect(userRepository.save).toHaveBeenCalledWith(input);
    });

    it('이메일이 없으면 ValidationError를 던진다', async () => {
      await expect(createUser({ name: '홍길동' }))
        .rejects.toThrow('이메일은 필수입니다');
    });

    it('중복 이메일이면 DuplicateError를 던진다', async () => {
      userRepository.save.mockRejectedValue(
        new Error('UNIQUE_VIOLATION')
      );
      await expect(createUser({ name: '홍길동', email: 'dup@test.com' }))
        .rejects.toThrow('이미 등록된 이메일입니다');
    });
  });

  describe('findUserById', () => {
    it('존재하는 사용자를 반환한다', async () => {
      userRepository.findById.mockResolvedValue({ id: 1, name: '홍길동' });
      const user = await findUserById(1);
      expect(user.name).toBe('홍길동');
    });

    it('존재하지 않으면 null을 반환한다', async () => {
      userRepository.findById.mockResolvedValue(null);
      const user = await findUserById(999);
      expect(user).toBeNull();
    });
  });
});

Spring Boot JUnit 5 테스트 예시

// src/test/java/com/example/service/UserServiceTest.java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    @DisplayName("정상적인 사용자를 생성한다")
    void createUser_success() {
        // given
        UserCreateRequest request = new UserCreateRequest("홍길동", "hong@test.com");
        User savedUser = new User(1L, "홍길동", "hong@test.com");
        given(userRepository.save(any(User.class))).willReturn(savedUser);

        // when
        UserResponse result = userService.createUser(request);

        // then
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getName()).isEqualTo("홍길동");
        verify(userRepository).save(any(User.class));
    }

    @Test
    @DisplayName("중복 이메일이면 예외를 던진다")
    void createUser_duplicateEmail_throwsException() {
        // given
        UserCreateRequest request = new UserCreateRequest("홍길동", "dup@test.com");
        given(userRepository.existsByEmail("dup@test.com")).willReturn(true);

        // when & then
        assertThatThrownBy(() -> userService.createUser(request))
            .isInstanceOf(DuplicateEmailException.class)
            .hasMessage("이미 등록된 이메일입니다");
    }
}

Playwright E2E 테스트 — AI 활용

Claude Code로 E2E 테스트 생성
로그인 → 대시보드 → 프로필 수정 플로우의 Playwright E2E 테스트를 작성해줘.

시나리오:
1. /login 페이지에서 이메일/비밀번호로 로그인
2. 대시보드 페이지로 리다이렉트 확인
3. 프로필 편집 버튼 클릭 → 이름 변경 → 저장
4. 성공 토스트 메시지 확인
5. 새로고침 후 변경된 이름 유지 확인

Page Object 패턴으로 구조화해줘.
// e2e/tests/profile-update.spec.js
const { test, expect } = require('@playwright/test');

test.describe('프로필 수정 플로우', () => {
  test.beforeEach(async ({ page }) => {
    // 로그인
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('프로필 이름을 변경하고 저장한다', async ({ page }) => {
    // 프로필 편집
    await page.click('[data-testid="edit-profile"]');
    await page.fill('[name="name"]', '김개발');
    await page.click('[data-testid="save-profile"]');

    // 성공 토스트 확인
    await expect(page.locator('.toast-success'))
      .toHaveText('프로필이 수정되었습니다');

    // 새로고침 후 유지 확인
    await page.reload();
    await expect(page.locator('[name="name"]'))
      .toHaveValue('김개발');
  });
});

테스트 커버리지 목표 가이드

프로젝트 유형 Unit Integration E2E AI 도구 활용 팁
MVP / 프로토타입 50% 핵심 API만 스모크 테스트 "핵심 기능 위주로 테스트 생성해줘"
일반 서비스 70~80% 주요 시나리오 핵심 플로우 "이 모듈 전체의 테스트를 생성해줘"
금융 / 결제 90%+ 전체 API 전체 시나리오 "엣지 케이스와 보안 테스트 포함해줘"
AI 테스트 생성 시 주의: AI가 생성한 테스트 코드도 반드시 검토하세요. mock이 실제 동작과 다를 수 있고, 중요한 엣지 케이스를 누락할 수 있습니다. AI 생성 테스트는 "초안"으로 활용하고, 비즈니스 로직 검증은 직접 확인하세요.

실습 과제

여러분의 프로젝트에서 테스트가 없는 서비스 파일 하나를 선택하세요. Claude Code에게 "이 파일의 단위 테스트를 작성해줘. 정상/에러/경계값 케이스를 모두 포함하고, 외부 의존성은 mock 처리해줘"라고 요청한 뒤 생성된 테스트를 실행해보세요.

5.16 AI 활용 API 설계

좋은 API 설계 = 좋은 제품의 기반: API(Application Programming Interface)는 시스템 간 소통의 규약입니다. 잘 설계된 API는 프론트엔드, 모바일, 외부 파트너 연동을 수월하게 만듭니다. AI를 활용하면 RESTful 설계 원칙에 맞는 API를 빠르게 설계하고, OpenAPI 문서까지 자동 생성할 수 있습니다.

REST API 성숙도 모델 (Richardson Maturity Model)

레벨 이름 특징 예시
Level 0 단일 엔드포인트 하나의 URL에 모든 요청 POST /api (action 파라미터로 구분)
Level 1 리소스 분리 URL로 리소스 구분 /users, /orders (POST만 사용)
Level 2 HTTP 메서드 활용 GET/POST/PUT/DELETE 구분 GET /users, POST /users, DELETE /users/1
Level 3 HATEOAS 응답에 다음 가능한 액션 링크 포함 응답에 { "links": { "next": "/users?page=2" } }
실무 추천: 대부분의 프로젝트는 Level 2를 목표로 설계하면 충분합니다. Level 3(HATEOAS)는 공개 API나 대규모 플랫폼에서 고려하세요.

HTTP 메서드 & 상태 코드 가이드

메서드용도멱등성성공 코드예시
GET조회O200 OKGET /api/users/1
POST생성X201 CreatedPOST /api/users
PUT전체 수정O200 OKPUT /api/users/1
PATCH부분 수정X200 OKPATCH /api/users/1
DELETE삭제O204 No ContentDELETE /api/users/1
상태 코드의미사용 시점
200OK조회/수정 성공
201Created리소스 생성 성공
204No Content삭제 성공 (응답 본문 없음)
400Bad Request잘못된 요청 (유효성 검증 실패)
401Unauthorized인증 필요
403Forbidden권한 없음
404Not Found리소스 없음
409Conflict중복 데이터 (이메일 등)
500Internal Server Error서버 오류

Claude Code로 API 설계하기

API 설계 프롬프트
할 일 관리(Todo) 앱의 REST API를 설계해줘.

요구사항:
- 리소스: 사용자(User), 할 일(Todo), 카테고리(Category)
- 인증: JWT 기반
- 페이지네이션: cursor 기반
- 에러 응답 형식 통일 (code, message, details)
- API 버전: /api/v1 prefix

각 엔드포인트별로:
1. HTTP 메서드 + URL
2. 요청 Body (JSON)
3. 응답 Body (JSON) + 상태 코드
4. 인증 필요 여부

Express.js 라우터 코드도 함께 생성해줘.

Node.js Express API 예시

// routes/todos.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
const todoService = require('../services/todoService');

// 할 일 목록 조회 (페이지네이션)
router.get('/', authenticate, async (req, res, next) => {
  try {
    const { cursor, limit = 20 } = req.query;
    const todos = await todoService.findAll(req.user.id, { cursor, limit });
    res.json({
      data: todos.items,
      pagination: {
        nextCursor: todos.nextCursor,
        hasMore: todos.hasMore
      }
    });
  } catch (err) {
    next(err);
  }
});

// 할 일 생성
router.post('/', authenticate, async (req, res, next) => {
  try {
    const { title, categoryId, dueDate } = req.body;
    const todo = await todoService.create(req.user.id, {
      title, categoryId, dueDate
    });
    res.status(201).json({ data: todo });
  } catch (err) {
    next(err);
  }
});

// 할 일 수정
router.patch('/:id', authenticate, async (req, res, next) => {
  try {
    const todo = await todoService.update(req.params.id, req.user.id, req.body);
    res.json({ data: todo });
  } catch (err) {
    next(err);
  }
});

// 할 일 삭제
router.delete('/:id', authenticate, async (req, res, next) => {
  try {
    await todoService.remove(req.params.id, req.user.id);
    res.status(204).end();
  } catch (err) {
    next(err);
  }
});

// 통일된 에러 핸들러
router.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message || '서버 오류가 발생했습니다',
      details: err.details || null
    }
  });
});

module.exports = router;

Spring Boot REST Controller 예시

// TodoController.java
@RestController
@RequestMapping("/api/v1/todos")
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService;

    @GetMapping
    public ResponseEntity<PageResponse<TodoResponse>> findAll(
            @AuthenticationPrincipal UserPrincipal user,
            @RequestParam(required = false) String cursor,
            @RequestParam(defaultValue = "20") int limit) {
        return ResponseEntity.ok(
            todoService.findAll(user.getId(), cursor, limit)
        );
    }

    @PostMapping
    public ResponseEntity<DataResponse<TodoResponse>> create(
            @AuthenticationPrincipal UserPrincipal user,
            @Valid @RequestBody TodoCreateRequest request) {
        TodoResponse todo = todoService.create(user.getId(), request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(DataResponse.of(todo));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<DataResponse<TodoResponse>> update(
            @AuthenticationPrincipal UserPrincipal user,
            @PathVariable Long id,
            @Valid @RequestBody TodoUpdateRequest request) {
        TodoResponse todo = todoService.update(id, user.getId(), request);
        return ResponseEntity.ok(DataResponse.of(todo));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(
            @AuthenticationPrincipal UserPrincipal user,
            @PathVariable Long id) {
        todoService.delete(id, user.getId());
        return ResponseEntity.noContent().build();
    }
}

API 아키텍처 — 계층 구조

graph TD A["클라이언트 (Web/Mobile)"] --> B["API Gateway / 로드 밸런서"] B --> C["인증 미들웨어 (JWT 검증)"] C --> D["컨트롤러 (라우팅 + 유효성 검증)"] D --> E["서비스 (비즈니스 로직)"] E --> F["리포지토리 (DB 접근)"] F --> G["데이터베이스"] D --> H["DTO 변환"] E --> I["도메인 이벤트"] style C fill:#ff9800,color:#fff style E fill:#4caf50,color:#fff style G fill:#2196f3,color:#fff

API 보안 체크리스트

보안 항목구현 방법도구/라이브러리중요도
인증 (Authentication)JWT 토큰 + Refresh Token 로테이션jsonwebtoken, Spring Security필수
인가 (Authorization)역할 기반 접근 제어 (RBAC)CASL, Spring Method Security필수
Rate LimitingIP/사용자별 요청 횟수 제한express-rate-limit, Bucket4j필수
입력 검증요청 Body/Query 스키마 검증Joi, Zod, Jakarta Validation필수
CORS허용 도메인 화이트리스트cors 미들웨어, WebMvcConfigurer높음
SQL Injection 방지파라미터 바인딩, ORM 사용Prisma, TypeORM, JPA필수

OpenAPI (Swagger) 문서 자동화

Claude Code로 API 문서 생성
현재 Express 라우터 파일들을 분석해서 OpenAPI 3.0 스펙(YAML)을 생성해줘.
각 엔드포인트별 request/response 스키마를 포함하고,
swagger-ui-express로 /api-docs에서 볼 수 있게 설정해줘.
# openapi.yaml (일부)
openapi: 3.0.3
info:
  title: Todo API
  version: 1.0.0
  description: 할 일 관리 REST API

paths:
  /api/v1/todos:
    get:
      summary: 할 일 목록 조회
      security:
        - bearerAuth: []
      parameters:
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: 조회 성공
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Todo'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

    post:
      summary: 할 일 생성
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoCreate'
      responses:
        '201':
          description: 생성 성공

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        completed:
          type: boolean
        dueDate:
          type: string
          format: date

실습 과제

여러분의 프로젝트에서 API를 하나 선택하세요. Claude Code에게 "이 API의 OpenAPI 스펙을 작성하고, 요청/응답 예시를 포함해줘. 에러 케이스(400, 401, 404, 409)도 모두 정의해줘"라고 요청해보세요. 생성된 스펙을 Swagger UI에서 확인하면 API 문서가 자동으로 완성됩니다.

← 이전 5. 개발자 실전 AI 활용 다음 →