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 | 조회 | O | 200 OK | GET /api/users/1 |
| POST | 생성 | X | 201 Created | POST /api/users |
| PUT | 전체 수정 | O | 200 OK | PUT /api/users/1 |
| PATCH | 부분 수정 | X | 200 OK | PATCH /api/users/1 |
| DELETE | 삭제 | O | 204 No Content | DELETE /api/users/1 |
| 상태 코드 | 의미 | 사용 시점 |
|---|---|---|
| 200 | OK | 조회/수정 성공 |
| 201 | Created | 리소스 생성 성공 |
| 204 | No Content | 삭제 성공 (응답 본문 없음) |
| 400 | Bad Request | 잘못된 요청 (유효성 검증 실패) |
| 401 | Unauthorized | 인증 필요 |
| 403 | Forbidden | 권한 없음 |
| 404 | Not Found | 리소스 없음 |
| 409 | Conflict | 중복 데이터 (이메일 등) |
| 500 | Internal 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 Limiting | IP/사용자별 요청 횟수 제한 | 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 문서가 자동으로 완성됩니다.