‹ 조하원
실습코치 업무 · 교보재 신청 자동화 AX 과제

교보재 신청 통합 플랫폼

엑셀로 받던 신청을
하나의 서비스로 옮겼어요.

교육생의 교육용 기자재(교보재) 신청부터 승인·지급까지 한 곳에서 처리하고, 과거 신청 사례를 RAG로 검색하는 웹 서비스예요. 교육생이 실제로 쓰는 운영 도구라서 무중단 배포와 보안 경계를 먼저 설계했어요.

형태실습코치 업무로 직접 구축
대상기수당 300명 미만
서버단일 서버 · Docker Compose
1,723건분석한 기존 재고
9.7%구매 링크 보유율
7.9%가격 정보 보유율
80·443외부 공개 포트
왜 만들었나

가격 자동 조회부터 만들려고 했어요.

처음엔 교보재 가격을 자동으로 찾아주는 기능을 우선 후보로 봤어요. 그런데 기존 재고 1,723건을 분석해 보니 구매 링크가 있는 건 9.7%, 가격 정보가 있는 건 7.9%뿐이었어요. 조회할 원천 정보가 대부분 비어 있었던 거예요. 그래서 우선순위를 신청·승인 절차를 한 곳으로 모으는 쪽으로 바꿨어요.

  1. React화면
  2. Spring Boot인증 · 권한 · 입력 검증
  3. FastAPIRAG 추론 전용 · 내부망
  4. Vector Store / LLM근거 사례 ID와 함께 응답

제가 한 일.

배포

  • master 병합 시 Jenkins가 대기 색상 스택을 빌드 → Trivy 보안 스캔 → 기동 → 헬스체크 → nginx 트래픽 전환
  • HIGH·CRITICAL 취약점이 나오거나 헬스체크가 실패하면 전환하지 않음
  • 배포 후 5분간 로그에서 error · exception · fatal 패턴을 찾아 Mattermost 알림에 포함

보안 경계

  • 외부 공개 포트는 80·443만, 나머지 컨테이너 포트는 전부 127.0.0.1 바인딩
  • AI 컨테이너는 외부 포트 없이 backend 내부망에만 둠 — 프론트가 FastAPI를 직접 못 부름
  • SSAFY OAuth: state 검증, 로그인 후 세션 ID 재발급, HttpOnly · Secure · SameSite=Lax 쿠키
  • Client Secret은 Jenkins Credential · 환경변수로만 주입

운영 · 협업

  • 모든 요청에 Request ID, 상태·재고·권한 변경엔 감사 로그 (토큰·개인정보는 로그 미기록)
  • Uptime Kuma로 엔드포인트 감시 + Mattermost 알림, 장애 대응 RUNBOOK 작성
  • CONTRIBUTING.md로 브랜치 · 커밋 · MR 규칙 정리, 인프라 · CI/CD · RAG 흐름 다이어그램 3종
  • 아키텍처 문서에 '미확정 결정 사항' 표를 따로 두고 관리

실제로 이렇게 돌아가요.

사내 GitLab 저장소라 비공개예요. 저장소의 실제 설정 파일에서 판단이 담긴 부분만 옮겼어요.

포트는 안 열고, 컨테이너 안에서 확인

Jenkinsfile · Health Check 단계 발췌

# 서비스 포트는 보안상 127.0.0.1에만 바인딩
# → Jenkins 컨테이너 밖에서는 닿지 않으므로
#   각 컨테이너 안에서 직접 호출
compose exec -T backend curl --fail \
  http://127.0.0.1:8080/api/health || {
  echo 'Backend health check failed'
  compose logs --tail=50; exit 1; }

compose exec -T ai python -c \
  "urllib.request.urlopen(
   'http://127.0.0.1:8000/health', timeout=2)"

외부에 열린 건 80·443뿐

docker-compose.yml 발췌

backend:
  ports:
    - "127.0.0.1:${BACKEND_PORT:-8080}:8080"
frontend:
  ports:
    - "127.0.0.1:${FRONTEND_PORT:-3000}:8080"
ai:
  # 외부 포트 없음 · backend 내부망에서만 호출
  healthcheck:
    test: ["CMD", "python", "-c", "... /health"]

제가 만든 범위

저장소 커밋 기록 기준

FastAPI RAG 서버 (ai/)커밋 전부 본인React 프론트엔드커밋 전부 본인Jenkins · 인프라대부분 본인Spring Boot 백엔드대부분 본인 · 다른 실습코치 일부 참여

문제를 다시 본 순간들.

처음엔

Jenkins 파이프라인이 컨테이너 기동까지는 통과하는데 헬스체크 단계에서만 실패했어요. 처음엔 포트나 방화벽 문제라고 생각했어요.

알고 보니

Jenkins도 컨테이너로 돌고 있었어요. Jenkins 안에서 부른 127.0.0.1은 배포 서버가 아니라 Jenkins 컨테이너 자기 자신이었어요.

바꾼 것

헬스체크를 docker compose exec로 각 컨테이너 안에서 호출하도록 바꿨고, 보안을 위한 127.0.0.1 바인딩은 그대로 유지했어요.

그래서

포트를 더 열지 않고도 배포 파이프라인이 끝까지 돌게 됐어요.

사용한 기술.

Backend
Spring Boot 4Java 21
Frontend
ReactViteTypeScript
AI
FastAPIRAG
Data
PostgreSQLRedis
Infra
Docker ComposenginxJenkinsTrivyUptime Kuma