시리즈: AI 팀과 함께 기술블로그 만들기 | Episode 6
작성일: 2026-02-08
태그: code-review, docker, security, best-practices, ai
이전 에피소드에서 UX를 다듬었습니다. 이제 돌아보겠습니다. AI 코드 리뷰 에이전트가 초기 설정에서 찾아낸 22개 이슈를 하나씩 분석합니다. 무엇을 수정했고, 무엇을 무시했는지. 그리고 왜 그랬는지.
리뷰 결과 요약
| 등급 | 발견 | 수정 | 채택률 |
|---|---|---|---|
| 🔴 Critical | 7 | 7 | 100% |
| 🟡 Warning | 9 | 8 | 89% |
| 🟢 Info | 6 | 4 | 67% |
| 합계 | 22 | 19 | 86% |
Critical은 전부 수정했습니다. Warning 중 하나는 Cloudflare 환경 때문에 따르지 않았고, Info 중 일부는 우선순위가 낮아서 보류했습니다.
Critical 이슈: 전부 수정
Critical-01: PostgreSQL 포트가 인터넷에 노출
발견:
# docker-compose.yml (초기)
postgres:
ports:
- "5432:5432" # 0.0.0.0:5432에 바인딩
문제점:
- WordPress는 PostgreSQL을 지원하지 않음
- 사용하지도 않는 DB가 인터넷에 포트 열림
- 방화벽 없으면 누구나 DB 접속 시도 가능
수정:
# docker-compose.yml (수정 후)
# postgres 서비스 전체 삭제
PostgreSQL을 완전히 제거했습니다. 불필요한 서비스는 보안 리스크입니다.
Critical-02: WordPress latest 태그
발견:
wordpress:
image: wordpress:latest
문제점:
- 빌드 시점마다 다른 버전 설치 가능
- 메이저 업데이트로 사이트 깨질 위험
- 재현 불가능한 배포
수정:
wordpress:
image: wordpress:6.9-php8.3-apache
PHP 8.3을 명시한 것도 중요합니다. JIT 컴파일러를 활용하기 위해서입니다.
Critical-03: Health Check 없음
발견:
# 모든 서비스에 healthcheck 없음
문제점:
- 프로세스는 살아있지만 응답하지 않는 “좀비” 상태 감지 불가
docker compose up완료 ≠ 서비스 준비 완료
수정:
wordpress:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
mariadb:
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 30s
start_period: 30s
redis:
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
MariaDB의 --innodb_initialized가 핵심입니다. 포트만 열렸는지가 아니라, InnoDB 엔진이 완전히 초기화되었는지까지 확인합니다.
Critical-04: MariaDB와 WordPress 비밀번호 불일치
발견:
# .env (초기)
MARIADB_PASSWORD=d&a9X5McNt7s2PX^@$M^
WORDPRESS_DB_PASSWORD=different_password_causes_error
문제점:
- 끝자리가
^와8로 다름 - MariaDB가
^로 사용자 생성, WordPress가8로 접속 → 인증 실패 - 게다가
$M이 Docker 환경변수 보간으로 빈 문자열로 치환됨
수정:
# .env (수정 후)
MARIADB_PASSWORD=your_secure_password_here_hex32
WORDPRESS_DB_PASSWORD=your_secure_password_here_hex32
openssl rand -hex 16으로 생성한 hex-only 비밀번호입니다. $, @, & 같은 특수문자가 없으므로:
- Docker
$보간 문제 없음 - 셸 이스케이핑 문제 없음
- URL 인코딩 문제 없음
.env.example에 주석으로 명시했습니다.
MARIADB_PASSWORD= # 필수! WORDPRESS_DB_PASSWORD와 반드시 동일한 값
WORDPRESS_DB_PASSWORD= # 필수! MARIADB_PASSWORD와 반드시 동일한 값
Critical-05: WordPress Salt가 기본값
발견:
// wp-config.php (Docker 이미지 기본값)
define('AUTH_KEY', getenv_docker('WORDPRESS_AUTH_KEY',
'ebe9df86132e4bef46fbc0cfa0f3869f7e23bc1f'));
문제점:
- 기본값은 Docker 이미지 소스코드에 공개됨
- 누구든 이 값을 알면 세션 쿠키 위조 가능
수정:
# .env.example
WORDPRESS_AUTH_KEY=
WORDPRESS_SECURE_AUTH_KEY=
WORDPRESS_LOGGED_IN_KEY=
WORDPRESS_NONCE_KEY=
WORDPRESS_AUTH_SALT=
WORDPRESS_SECURE_AUTH_SALT=
WORDPRESS_LOGGED_IN_SALT=
WORDPRESS_NONCE_SALT=
8개의 Salt를 환경변수로 분리했습니다. https://api.wordpress.org/secret-key/1.1/salt/ 에서 생성한 값을 사용합니다.
Critical-06: Resource Limits 없음
발견:
# 모든 서비스에 리소스 제한 없음
문제점:
- 하나의 서비스가 서버 전체 메모리 소비 가능
- OOM으로 서버 다운 시 SSH 접속도 불가
수정:
wordpress:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
mariadb:
deploy:
resources:
limits:
memory: 512M
redis:
deploy:
resources:
limits:
memory: 192M
합계: 최소 576M, 최대 1,216M. 1GB 메모리 VPS에서도 동작 가능한 사이즈입니다.
Critical-07: 로깅 설정 없음
발견:
# 로그 파일 크기 제한 없음
문제점:
- Docker 로그가 디스크를 가득 채울 수 있음
- Redis는 디버그 레벨에서 초당 수십 줄 로그
수정:
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
wordpress:
logging: *default-logging
mariadb:
logging: *default-logging
redis:
logging: *default-logging
각 서비스의 로그가 최대 30MB(10MB × 3파일)로 제한됩니다.
Warning 이슈: 8개 수정, 1개 무시
Warning-01: depends_on에 condition 미사용
발견:
wordpress:
depends_on:
- mariadb
- redis
문제점:
- 컨테이너 시작 순서만 보장
- MariaDB가 “실제로 쿼리를 받을 준비가 됨”을 보장하지 않음
수정:
wordpress:
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_healthy
Health check와 조합하여 서비스가 완전히 준비된 후에 WordPress가 시작됩니다.
Warning-02~05: 이미지 버전, 네트워크, Redis 인증
| 항목 | Before | After |
|---|---|---|
| Redis 이미지 | redis:alpine | redis:7-alpine |
| internal 네트워크 | 기본 | internal: true |
| Redis 인증 | 없음 | --requirepass ${REDIS_PASSWORD} |
| MariaDB 이미지 | mariadb | mariadb:10.11 |
모두 수정했습니다.
Warning-06: Caddy HTTPS 설정 (유일하게 무시한 Warning)
AI 권장:
labels:
caddy: "yndl.dev" # http:// 제거하면 auto-HTTPS 활성화
실제 적용:
labels:
caddy: "http://yndl.dev" # http:// 유지
왜 무시했는가:
AI 권장대로 http://를 제거하면 Caddy가 auto-HTTPS를 활성화합니다. 하지만 이 블로그는 Cloudflare 뒤에 있습니다.
[사용자] --HTTPS--> [Cloudflare] --HTTP--> [Caddy] --HTTP--> [WordPress]
Cloudflare가 이미 SSL을 처리합니다. Caddy가 또 HTTPS를 하려고 하면:
- Caddy가 auto-HTTPS 활성화
- Let’s Encrypt에 인증서 발급 요청
- DNS-01 챌린지 시도 → Cloudflare 프록시 뒤에 있어서 실패
- 사이트 전체 다운 (308 무한 리다이렉트)
Episode 3에서 자세히 다뤘습니다.
핵심 원칙:
Cloudflare Proxy 뒤에서는 Caddy의 auto-HTTPS를 끄세요. SSL은 Cloudflare가 처리합니다.
AI는 개별 컴포넌트의 Best Practice는 정확히 아는 반면, 컴포넌트 간의 상호작용(Cloudflare + Caddy + WordPress 체인)은 놓칠 수 있습니다.
Info 이슈: 4개 수정, 2개 보류
Info-01: wp-config.php 보안 상수
권장:
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('WP_AUTO_UPDATE_CORE', 'minor');
수정: ✅ 전부 적용
DISALLOW_FILE_EDIT는 특히 중요합니다. WordPress 관리자 패널의 “테마 편집기”를 비활성화합니다. 공격자가 관리자 계정을 탈취해도 편집기로 백도어를 심을 수 없습니다.
Info-02: OPcache 설정
권장:
opcache.jit=1255
opcache.jit_buffer_size=64M
수정: ✅ 적용
PHP 8.3의 JIT 컴파일러를 활성화했습니다. 1255는 Tracing JIT 모드로, CPU-bound 작업에서 30~50% 성능 향상 가능성이 있습니다.
Info-03~04: 백업 스크립트, .gitignore
| 항목 | 권장 | 적용 | 상태 |
|---|---|---|---|
| 백업 스크립트 | 4가지 모드 (db, content, full, pre-deploy) | ✅ | scripts/backup.sh |
| .gitignore | wp_content/plugins/*, themes/*, languages/ 제외 | ✅ | 설치 가능한 항목 제외 |
둘 다 적용했습니다.
Info-05~06: 보류
| 항목 | 권장 | 상태 |
|---|---|---|
| Redis 메모리 정책 | maxmemory-policy allkeys-lru | ⏸️ 추후 적용 예정 |
| DB 쿼리 최적화 | Slow query log 활성화 | ⏸️ 필요시 적용 |
Redis 메모리 정책은 Episode 4에서 이미 적용했습니다. DB 쿼리 최적화는 아직 병목이 없어서 보류했습니다.
코드 리뷰 에이전트의 가장 큰 공헌
1. 연쇄 이슈 발견
AI는 독립적인 이슈를 나열한 것이 아니라, 연쇄적 관계를 파악했습니다.
비밀번호에 $가 포함
↓
Docker 변수 보간으로 $M이 빈 문자열로 치환
↓
MARIADB_PASSWORD와 WORDPRESS_DB_PASSWORD가 실제로는 다른 값
↓
DB 인증 실패
↓
WordPress 설치 불가
이 연쇄를 한 번에 파악하고 “hex-only 비밀번호를 사용하라”는 근본 해결책을 제시한 것은 코드 리뷰 에이전트의 가장 큰 공헌입니다.
2. 우선순위 분류의 정확성
22개 이슈를 Critical / Warning / Info로 분류한 것이 매우 정확했습니다.
Critical 7개는 실제로 전부 보안 또는 안정성에 직접 영향을 미치는 것들이었습니다:
- PostgreSQL 포트 노출
- 비밀번호 불일치
- Salt 기본값
- Health check 없음
- Resource limits 없음
- 로깅 무제한
Warning 9개는 “지금 당장은 괜찮지만, 프로덕션에서는 문제가 될 수 있는 것들”이었습니다.
Info 6개는 “Best Practice이지만 필수는 아닌 것들”이었습니다.
이 분류 덕분에 “Critical은 무조건, Warning은 선택적으로, Info는 시간 날 때”라는 명확한 우선순위를 세울 수 있었습니다.
3. 컨텍스트 없는 추천의 한계를 보여줌
Caddy HTTPS 설정(Warning-06)은 AI가 틀린 것이 아닙니다. Caddy 단독으로 보면 100% 맞는 권장 사항입니다.
하지만 Cloudflare → Caddy → WordPress라는 전체 인프라 체인을 모른 채 권장했습니다. 이것은 AI의 한계를 명확히 보여줍니다.
인프라의 전체 그림은 사람이 판단해야 한다.
AI는 개별 파일을 완벽히 분석하지만, 외부 서비스(Cloudflare, DNS, 방화벽)와의 상호작용은 프로젝트 디렉토리 밖에 있어서 알 수 없습니다.
수정 전후 비교
docker-compose.yml
| 항목 | Before | After |
|---|---|---|
| 이미지 태그 | wordpress:latest | wordpress:6.9-php8.3-apache |
| Health check | 없음 | 3개 서비스 모두 |
| Resource limits | 없음 | 합계 최대 1,216M |
| 네트워크 | 기본 | proxy_net + internal(격리) |
| 로깅 | 무제한 | 서비스당 30MB |
| 불필요 서비스 | PostgreSQL 노출 | 삭제 |
.env
| 항목 | Before | After |
|---|---|---|
| 비밀번호 | 특수문자 포함 ($, @, &) | hex-only |
| DB 비밀번호 | 불일치 | 동일 (명시적 주석) |
| Salt | 기본값 | 환경변수 분리 (8개) |
| Redis 인증 | 없음 | 환경변수 비밀번호 |
PHP 설정
| 항목 | Before | After |
|---|---|---|
| OPcache | 기본값 | JIT 활성화 (1255) |
| 업로드 제한 | 2M | 64M |
| 메모리 제한 | 기본 | 256M |
채택률 86%의 의미
22개 중 19개 수정 (86%)
AI 권장을 따르지 않은 3건:
- Caddy HTTPS (Warning-06): Cloudflare 환경 때문에 무시
- Redis 메모리 정책 (Info-05): 이미 적용함
- DB 쿼리 최적화 (Info-06): 아직 필요 없음
3건 모두 “AI가 틀렸기” 때문이 아니라 “AI가 모르는 컨텍스트가 있었기” 때문입니다.
이 경험에서 배운 것
1. Critical은 무조건 따라라
보안과 안정성에 관한 문제는 “나중에”가 없습니다. PostgreSQL 포트 노출, 비밀번호 불일치, Salt 기본값 — 이런 것들은 발견 즉시 수정해야 합니다.
AI가 Critical로 분류한 7건은 실제로 전부 치명적이었습니다. 하나라도 놓쳤으면 보안 사고나 서비스 다운으로 이어질 수 있었습니다.
2. 외부 컨텍스트는 AI가 알 수 없다
Caddy HTTPS 설정처럼, 프로젝트 디렉토리 밖의 정보(Cloudflare, DNS, 서버 방화벽)는 AI가 알 수 없습니다.
AI 권장을 맹목적으로 따르면 위험합니다. 전체 시스템 아키텍처를 아는 사람이 최종 판단을 내려야 합니다.
3. hex-only 비밀번호 원칙
openssl rand -hex 16으로 생성한 hex-only 비밀번호는:
- Docker 환경변수 보간 문제 없음
- 셸 이스케이핑 문제 없음
- URL 인코딩 문제 없음
- 특수문자 입력 오류 없음
이후 모든 프로젝트에서 이 원칙을 따르고 있습니다.
4. 보고서는 “의사 결정 프레임워크”다
AI 코드 리뷰 보고서를 “실행 매뉴얼”로 쓰면 위험합니다. Caddy HTTPS처럼 맹목적으로 따르면 사이트가 다운됩니다.
대신 “어떤 문제가 있고, 각각의 심각도가 어느 정도인지”를 파악하는 도구로 쓰면 매우 효율적입니다. 최종 결정은 전체 컨텍스트를 아는 사람이 내려야 합니다.
다음 에피소드 예고
지금까지 인프라, UX, 코드 리뷰를 다뤘습니다. 다음 에피소드에서는 마크다운으로 글을 쓰고 자동으로 WordPress에 발행하는 시스템을 구축합니다. Episode 1~6을 모두 마크다운으로 작성했는데, 어떻게 WordPress 블록 마크업으로 변환하고 발행했는지 공개합니다.
예고: Episode 7 — 마크다운으로 글 쓰고 자동 발행하기
시리즈 안내: AI 팀과 함께 기술블로그 만들기
| 회차 | 제목 | 상태 |
|---|---|---|
| 1 | Agent Teams란? 3인 AI 팀 구성기 | 발행 |
| 2 | AI 3인이 내 블로그를 진단했다 | 발행 |
| 3 | Caddy와 싸운 하루: 리다이렉트 루프 3연전 | 발행 |
| 4 | Docker + WordPress + Redis: AI가 설계한 인프라 적용기 | 발행 |
| 5 | 기술블로그 UX: 코드가 읽히는 블로그 | 발행 |
| 6 | AI 코드 리뷰어가 찾아낸 22개 이슈 | 현재 글 |
| 7 | 마크다운으로 글 쓰고 자동 발행하기 | 예정 |
| 8 | AI 팀 vs 혼자 코딩: 비용 대비 효과 분석 | 예정 |
이 글은 Claude Code를 사용하여 작성되었습니다.
프로젝트: yndl.dev