Arda 개발 보고서
2026. 09. 29. github.com/Seuk-Team/Arda

목차 › 5. 기술 스택 및 개발 환경

기술 스택 및 개발 환경

무엇을 썼는가보다 왜 그것이어야 했는가를 남긴다 · 기준 시점 저장소 Seuk-Team/Arda 8071fd9 · 최종 갱신 2026. 09. 22.

결정 문서 36편 ADR
API 라우트 107개
테이블 28개 v2.8
alembic 리비전 26개
CI 잡 5개 · push·PR마다
백엔드 테스트 1,165건

1. 기술 스택 선정 및 근거

기술 · 범위 · 윤리 결정 36건을 ADR(docs/03_decision/)로 남겼다. 안 한 것에도 ADR 이 있다 — Kubernetes 제외(0001) · 실시간 공동편집 제외(0005) · SQS 워커 폐기(0036). 개정은 원문을 지우지 않고 절을 덧붙이므로, 뒤집힌 결정도 원래 근거가 그대로 남아 있다. 아래 표는 2026. 09. 22. 기준 현행 스택이고, 이어지는 카드가 갈림길이 있었던 항목의 근거다.

영역 스택 근거 파일
백엔드 Python 3.12 · FastAPI · SQLAlchemy 2.0 · psycopg 3 · PostgreSQL 16 + pgvector(pgvector/pgvector:pg16) · alembic · uv(의존 잠금) · pytest + pytest-timeout(60초) · ruff --select F backend/pyproject.toml
아키텍처 헥사고날 부분 적용 — Bounded Context 4개(hiring · talent · application · interview) + ports/output 포트 · adapter/outbound/{pg,llm,mail} 어댑터 (ADR-0035) backend/app/
프론트엔드 React 19 · Vite 8 · TypeScript 6 · react-router 7 · three.js(아르 3D 캐릭터) · CSS Modules + 디자인 토큰 · oxlint frontend/app/package.json
앱 Flutter(CI 3.44.8 · Dart SDK ^3.12) · http · flutter_secure_storage(토큰은 Android Keystore) · Android APK mobile/pubspec.yaml
AI · 판단 Anthropic Claude(claude-haiku-4-5) — 서류 요약 · 평가 · 추천 3단 체인(ADR-0022) · 아르 도구 호출 에이전트 · 지원자 FAQ. 규칙 의도 라우터가 빈출 요청 · 기본 질문을 LLM 없이 처리($0) backend/app/agent/
AI · 검색 sentence-transformers(ko-sroberta) 임베딩 + pgvector 시맨틱 검색 (ADR-0021) backend/app/agent/embedder.py
AI · 음성 · 표정 OpenAI Whisper API(whisper-1) STT — STT_BACKEND 로 로컬 faster-whisper 전환 가능(ADR-0038) · lie-detection 서비스(Python 3.13 · MediaPipe 얼굴 랜드마크 · ViT 표정 · librosa) — 담당자 화면 참고 지표 전용, 점수 미반영(ADR-0029 · 0032) ai/lie-detection/
AI · R&D 자산 Qwen3-8B QLoRA 어댑터 3갈래(chat v9 · summary · interview) — 동일 채점기로 학습 전 26.1% → v9 73.9%. 심사 서빙엔 쓰지 않고 AGENT_*_BACKEND=ollama 스위치로 교체 가능 보존 ai/qwen-training/
무결성 web3 · Ethereum Sepolia 테스트넷 앵커 · OpenTimestamps — 제출물 SHA-256 해시 사슬을 추가 전용 원장에 쌓고 DB 트리거가 수정 · 삭제를 거부(ADR-0028). 서명 개인키는 서버 밖(GitHub Actions) backend/app/chain.py
메일 n8n 웹훅 + SMTP — n8n 이 죽어도 API 가 5분마다 밀린 메일을 재발송(최대 3회). SES · SQS 는 폐기 폐기 (ADR-0030 · 0031 · 0036) backend/app/shared/mail_smtp.py
인프라 Docker Compose(db · api · caddy · lie-detection · n8n) · AWS EC2 t3.medium(서울 · Elastic IP · EBS 50GB) · S3(presigned URL 직접 업로드) · Caddy(443, 인증서 자동) · Vercel(프론트) · systemd 타이머 자동 CD · CloudWatch + SNS 경보 · 매일 S3 DB 백업(30일 수명주기) infra/ · 07-deploy
CI GitHub Actions 5잡 — ① 백엔드 린트(ruff F) ② 백엔드 테스트(alembic 이행 → 모델↔이행 비교 → pytest) ③ 프론트 빌드·린트 ④ AI 면접 서버 시험 ⑤ 앱 시험 .github/workflows/ci.yml

갈림길이 있었던 것 — 무엇과 비교했고 무엇을 기준으로 잘랐는가

백엔드 — Python · FastAPI

선정 ADR 없음 · 남은 근거는 결과 쪽이다

FastAPI 자체에는 비교 ADR 이 없다. 기획 단계에서 스택 표에 그대로 적혀 시작했고, 갈림길로 기록된 것은 그 위의 프론트(ADR-0006)와 앱(ADR-0010)이다. 지어내지 않고 실제로 값을 한 자리만 적는다.

첫째, 계약의 진실이 코드에서 나온다. Swagger /docs 가 라우트 107개의 요청 · 응답을 자동 생성하고, 02-api.md 는 "무엇이 있는가"만 유지한다. 프론트 · 앱 두 클라이언트가 이 문서 하나로 병렬 개발했고, 배포본 OpenAPI 를 main 과 대조하는 스크립트(check_public_contract.py)가 08/31 실측에서 35/35 · 62/62 일치를 확인했다.

둘째, 에이전트가 같은 런타임 안에 있다. backend/app/agent/ 는 별도 서비스가 아니라 같은 Python 프로세스이고, Claude SDK · sentence-transformers · pypdf 가 API 코드와 한 의존 묶음에 들어간다. 에이전트가 백엔드에 제공하는 요약 생성 함수도 프로세스 안 호출이라 두 오너 간 인터페이스가 HTTP 가 아니라 함수 시그니처다.

셋째, 그 한 덩어리가 커지자 헥사고날 부분 적용으로 Bounded Context 4개와 adapter/outbound/{pg,llm,mail} 을 갈랐다(ADR-0035). 지금 모델 · 메일 · 큐 · 스토리지 공급자를 환경변수 스위치 (AGENT_CHAT_BACKEND · MAIL_DISPATCH · QUEUE_BACKEND · S3_ENDPOINT_URL)로 갈아 끼울 수 있는 것이 그 재편의 결과다.

프론트엔드 — React, 서버 템플릿(Jinja) 기각

ADR-0006 · 확정 2026. 08. 23.

목업이 순수 정적 HTML 이라 두 갈래가 가능했다. Jinja 는 FastAPI 가 HTML 을 완성해 내려주므로 프론트 서버 · 빌드 · API 호출 · CORS 가 통째로 없다. React 는 프로젝트를 따로 세우고 팀원 HTML 을 JSX 로 다시 쓴다.

이식 비용은 쟁점이 아니었다 — HTML → JSX 는 기계적 변환이다. 실제 무게는 딸려오는 층이었다: 개발 서버 두 개 · 빌드 · CORS · 배포 대상 두 곳. 그 층이 막히면 데모 자체가 안 뜬다. 그래서 판단 기준을 하나로 잡았다 — "프론트가 안 뜰 때 혼자 고칠 수 있는가."

React 를 택했다. 그 층들이 이미 손에 익어 있었고, 익숙하지 않은 스택을 새로 배우는 비용이 그 층을 유지하는 비용보다 크다고 봤다. 딸린 결정 하나 — 공통 조각(사이드바 · 테이블 행 · 버튼 · 입력)을 먼저 컴포넌트로 만든 뒤 화면을 이식한다. 안 그러면 화면마다 같은 마크업을 네 번 다시 친다.

앱 — Flutter, Expo 에서 재확정

ADR-0010 · 08. 25. Expo 1차 확정 → 08. 26. Flutter 재확정

학습 비용은 판단 기준으로 삼지 않았다. AI 코딩 보조를 쓰면 새 언어의 문법 학습 비용이 예전만큼 크지 않고, Dart 를 처음 본다는 것이 그 자체로 Flutter 를 기각할 근거가 못 된다. 기준은 "팀 폰에서 돌아가는 데모를 09/30 까지 만들 수 있는가" 하나였다.

08. 25. 에는 이 기준이 Expo 를 가리켰다. 완료 기준이 "팀 전원 기기에서 실행 확인" 이었고 팀 기기가 iOS 1대 · Android 4대라, Mac · Xcode 가 필요한 Flutter 로는 그 1대를 올릴 수 없었기 때문이다. 08. 26. 에 팀이 데모 범위를 좁혀 iOS 1대를 시연 대상에서 뺐고, 그 순간 Expo 를 골랐던 유일한 근거가 사라졌다. 바뀐 것은 기술 비교가 아니라 데모 범위다.

대신 감수한 것을 숨기지 않고 적었다 — 팀에 Dart 사용자가 0명이고 앱은 1인 트랙이라 막히면 혼자 푼다. 팀 배포는 매번 APK 를 만들어 전달한다(Expo Go 의 QR 즉시 반영이 없다). iOS 호환은 끝까지 미검증으로 남는다 — iOS 지원 플러그인만 쓰고 Platform.isAndroid 분기를 넣지 않지만, Mac 이 없어 flutter build ios 를 실행해 본 적이 없다. 그래서 발표 문구까지 고정했다: "iOS 도 됩니다"가 아니라 "포팅 가능한 상태로 뒀고, 빌드 검증은 장비가 없어 못 했습니다."

패키지도 같은 방식으로 골랐다. http — dio 는 인터셉터 · 재시도가 필요할 때 값을 하는데 서버에 리프레시 토큰이 없어 갱신 로직이 없고 처리할 것이 401 하나뿐이다. flutter_secure_storage — shared_preferences 는 평문이라 쓰지 않는다, 12시간짜리 토큰이라도 남의 계정이다. url_launcher — 이력서를 앱 안에서 보려면 PDF 렌더러가 또 필요하고 이미지 · docx 는 각각 다르다, 서버가 준 presigned URL 을 브라우저로 넘긴다.

스키마 이행 — 손 SQL 에서 alembic 으로

2026. 09. 01. 전환 · 사고가 먼저 있었고 도구는 그 뒤에 왔다

전환 이유는 취향이 아니다. create_all 은 없는 테이블을 만들 뿐 기존 테이블에 컬럼을 붙이거나 타입을 넓히지 않는다. 2026. 09. 01. 하루에 그 대가를 두 번 치렀다.

· 로컬 DB 의 users.role 체크 제약이 옛 값이라 테스트 101건이 error
· 운영 applications.ai_summary_model 이 varchar(50) 이라 AI 요약이 전건 실패 — Claude 호출 3번을 다 끝낸 뒤 저장에서 죽어 돈은 쓰고 결과는 버렸고, BackgroundTasks 라 화면에 에러도 안 떴다

두 번 다 "각자 손으로 SQL 을 돌려라"로 수습했다. 코드는 pull 로 따라오지만 DB 는 안 따라온다 — 문서에 손 SQL 을 적는 방식은 사람마다 스키마가 갈리는 것을 막지 못한다. 지금은 리비전 26개가 쌓여 있고 uv run alembic upgrade head 한 줄로 따라온다.

도입 자체도 한 번 뒤집었다. 처음에는 uv run --with alembic 으로 잠금 파일을 건드리지 않았다 — Dockerfile 의 uv 핀과 uv.lock 형식이 어긋나 이미지 빌드가 깨질까 봐 피한, 근거 있는 판단이었다. 뒤집은 이유는 버전이 아무 데도 안 박힌다 는 것이다: --with 는 매번 최신을 받으므로 사람마다 · CI 마다 다른 alembic 으로 같은 리비전을 돌리게 된다. 그리고 걱정했던 위험은 실측해 보니 성립하지 않았다(잠금 파일 형식 그대로, 기존 패키지 버전 이동 0건, 두 줄만 추가). 원래 판단이 틀렸던 게 아니라 전제가 이미 바뀌어 있었다.

이후 alembic 은 dev 묶음에서 운영 의존으로 올라갔다 — 배포가 컨테이너 안에서 이행을 돌리므로(2026. 09. 04. 이슈 #17), dev 그룹에 두면 --no-dev 빌드에서 빠져 이행이 아예 불가능하다.

인프라 — Kubernetes 제외, AWS 8종 → 3종

ADR-0001 · ADR-0031 · ADR-0036

K8s 는 뺐다. 한 달 안에 K8s 를 "굴렸다"고 말하려면 배포 전략 · 오토스케일링 · 헬스체크 튜닝 중 최소 하나는 실제로 다뤄야 하는데, 서비스 기능까지 만들면서는 매니페스트 쓰고 파드 뜨는 수준이 한계다. 그 수준은 면접에서 파고들면 방어가 안 된다. 그래서 Docker + AWS + GitHub Actions CI/CD 까지를 목표로 잡았다 — "못 만들어서가 아니라, 한 달 규모에서 설명 가능한 깊이까지 못 가기 때문에 뺐다."

AWS 는 8종에서 3종(EC2 · S3 · IAM)으로 줄였다. 이유는 기술이 아니라 기한이다 — 예산이 $400 · 2026. 10. 27. 까지인데 그 뒤에도 서비스가 돌아야 한다. 나머지 다섯(SES · SQS · CloudWatch · SNS · CloudFormation)은 self-host 대안으로 갈아 끼울 스위치를 만든 뒤 폐기 대상으로 돌렸고(콘솔에서 지우는 것은 10. 27. 이후다), 메일은 n8n + SMTP 로 이관했다. 주장으로 두지 않고 PC 한 대에 운영과 같은 스택을 통째로 띄워 실증했다(infra/local/, 2절).

모델 — 자체학습 Qwen 과 Claude 를 같은 자로 재고 클라우드 확정

ADR-0024 → 2026. 09. 18. 확정

Qwen3-8B 를 QLoRA 로 직접 학습해 어댑터 3갈래(chat · summary · interview)를 만들었고, 동일 채점기(judge.py) · 동일 23건으로 쟀다 — 학습 전 26.1% → Claude Haiku 69.6% → Qwen v9 73.9%. 정확도는 동급까지 왔다.

그런데도 심사 서빙은 클라우드 + Claude 로 확정했다. 갈린 것은 정확도가 아니라 수치 추론 · 형식 안정성 · 콜드 스타트 78초 · 동시성이다. 실제로 학원 PC 3대로 온프레미스를 구축해 운영까지 해 봤고, 도구 오호출 · 프롬프트 예시 숫자 베끼기 · JSON 따옴표 깨짐이 심사 직전에 누적돼 폐쇄했다. 학습 파이프라인과 어댑터는 버리지 않고 AGENT_*_BACKEND=ollama 스위치로 교체 가능한 R&D 자산으로 보존했다.

2. 개발 환경 구성

로컬 환경의 원칙은 하나다 — 기본값이 곧 기존 동작이고, 로컬 초록과 CI 초록이 갈리지 않게 한다. 로컬 전용 우회를 허용하면 "내 PC 에선 됐는데"가 배포까지 간다. 아래 함정 목록은 전부 실제로 겪은 것이고, 겪을 때마다 docs/00_overview/08-local-setup.md 에 쌓았다.

평소 셋업 — 네 줄

git pull docker compose up -d --build # DB 를 먼저 띄운다 cd backend && uv sync uv run alembic upgrade head # 스키마 최신화

순서를 2026. 09. 02. 에 바꿨다. 전에는 alembic 이 docker compose up 앞에 있었는데, DB 가 떠 있지 않으면 마이그레이션이 붙을 데가 없다. 컨테이너가 하나도 없는 PC 에서 그대로 따라 하면 거기서 막힌다 — 실제로 겪고 고친 순서다. 로컬 compose 가 띄우는 것은 넷이다: db(pgvector/pgvector:pg16, 5432) · api(8000) · minio(9000) · minio-init. DB 이미지는 CI · 운영과 같은 것을 쓴다 — 순정 postgres 에는 vector 확장이 없어 임베딩 경로가 통째로 안 돈다.

백엔드 스위치 — 아무것도 설정하지 않으면 지금까지와 동일하게 돈다

환경변수 기본 로컬로 돌리려면 비고
STT_BACKEND 백엔드 openai (빈 값도) · 실시간 면접 서버는 빈 값이면 로컬 faster_whisper (uv sync --extra local 필요) 같은 변수, 두 해석 — 같은 backend/.env 를 두 컨테이너가 다르게 읽는다. 로컬에서 비워 두면 백엔드는 API 로, 실시간 서버는 CPU 로 받아쓴다 (ADR-0038)
AGENT_CHAT_BACKEND anthropic ollama 어댑터 유닛 테스트는 mock 기반이라 Ollama 없이 돈다. 실제 추론이 필요한 사람은 에이전트 오너뿐
AGENT_SUMMARY_BACKEND anthropic ollama GPU 가 없으면 OLLAMA_HOST 로 GPU 장비 한 대를 가리킨다 — 포트를 열지 말고 SSH 터널

모르는 값을 넣으면 조용히 폴백하지 않고 즉시 예외로 죽는다. 오타 하나로 개인정보가 외부로 나가면 안 되기 때문이다 — 폴백이 친절해 보이지만, 이 경우엔 틀린 설정이 성공처럼 보이는 것이 더 비싸다.

자주 물리는 함정 — 코드가 아니라 내 환경이 원인인 것들

5432 를 네이티브 PostgreSQL 이 선점

컨테이너 DB 가 정상 기동해도 연결이 그쪽으로 간다. 옛 스키마의 다른 arda DB 에 붙어 DB 의존 테스트가 통째로 error 난다. 개인 오버라이드로 다른 포트를 함께 매핑해 쓴다.

pytest 가 backend/.env 를 읽는다

2026. 09. 07. 부터. 전에는 .env 에 다른 포트를 적어도 무시되고 5432 로 붙었다. 단 에이전트 스위치는 여전히 빈 값으로 선점된다 — .env 에 실제 키가 있어도 테스트엔 안 들어간다. 로컬 초록 · CI 빨강이 갈리는 것을 막기 위해서다.

MinIO 버킷 이름

로컬 S3 는 arda-local 버킷을 만든다. S3_BUCKET 을 실 AWS 이름으로 둔 채 업로드하면 전건 404 로 죽는데, 그 404 는 브라우저가 S3 로 직접 PUT 하다 나는 것이라 API 로그에 안 남는다.

요약 · 임베딩은 응답 뒤에 돈다

BackgroundTasks 라 접수 API 가 201 을 줘도 요약은 아직이다. 실패해도 화면에 안 뜬다 — DB 에서 건수를 세서 확인해야 한다.

WSL 에서 vite 가 변경을 못 본다

2026. 09. 08. Windows 쪽에서 고친 파일이 inotify 로 안 올라와 서버를 켤 때의 코드가 계속 나온다. 고약한 것은 틀린 화면이 그럴듯하게 그려진다는 점이다 — 코드를 읽으면 맞는데 화면이 다르다. VITE_DEV_POLL=1 로 폴링을 켜거나 개발 서버를 다시 띄운다.

.env 를 고쳐도 반영이 안 된다

.env 는 프로세스 시작 때 한 번만 읽는다. 그리고 docker compose restart 는 env_file 을 다시 읽지 않는다 — up -d --force-recreate 로 컨테이너를 다시 만들어야 한다. 실제로 이것 때문에 기능 하나가 "스위치가 없어서" 안 뜬 적이 있다.

운영 그대로 로컬 전체 스택 — infra/local/

위 네 줄은 개발용 최소 스택이다. 이와 별개로 운영(EC2)과 같은 스택을 PC 한 대에 그대로 띄우는 절차를 2026. 09. 08. 에 만들었다. 목적이 분명하다 — "AWS 를 떠나도 돈다"(ADR-0031)를 주장이 아니라 사실로 만드는 것이다. db · api · lie-detection · n8n · caddy 가 뜨고, 운영 compose 의 사본이라 차이는 TLS 없음 · 포트뿐이다. 서버에서 .env 두 벌 · pg_dump · n8n 볼륨을 받아 복원한 뒤 smoke.sh 가 한 번에 자동 검증한다 — 컨테이너 상태 · api 직접과 caddy 경유의 /health · 내부 API · 무결성 API 의 인증 게이트(401) · lie-detection · n8n Basic Auth 와 웹훅 · alembic 이 head 인지 · api 로그의 Traceback 0건. 스택 검증은 dev 서버(5173)가 아니라 프로덕션 번들 preview(4173)에서 한다 — dev 모드에는 로그인 우회 · 목데이터 폴백이 켜져 있어 부적합하다. 가져온 것은 운영 데이터 사본이다 — 지원자 실제 이메일이 들어 있고 MAIL_DISPATCH=n8n 이라 단계를 바꾸면 진짜 메일이 나간다. 테스트는 이메일이 팀원 것으로 바뀐 지원자로만 한다.

CI 환경은 로컬 · 운영과 버전을 맞춰 못 박는다

버전이 떠 있으면 남의 커밋에서 CI 가 터진다. 그래서 전부 이유를 달아 고정했다 — 백엔드 Python 3.12(Dockerfile 의 python:3.12-slim 과 맞춘다) · 프론트 Node 22(로컬 실측 v22.23.1 과 맞춘다) · AI 면접 서버 Python 3.13(ai/lie-detection/Dockerfile 과 맞춘다) · Flutter 3.44.8 · ruff 0.16.7(uvx 로 격리 실행 — dev 묶음에 넣으면 uv.lock 이 바뀌어 전원이 uv sync 를 다시 돌려야 한다). CI 의 DB 도 로컬 compose 와 같은 pgvector/pgvector:pg16 이다.

3. 형상 관리 및 브랜치 전략

저장소는 Seuk-Team/Arda(2026. 09. 04. 이관, 구 Team-Seuk/Arda 는 삭제). main 기준 커밋 1,095건 · 머지 커밋 297건(2026. 08. 20. ~ 09. 18., 8071fd9)이다. 브랜치 전략은 두 번 바뀌었고, 두 번 다 사고나 병목이 먼저 있었다.

시기 규칙 왜 바꿨는가
~ 2026. 08. 27. 이슈 → 브랜치 → PR → 사람 리뷰 · 승인 전 PR 을 팀장 한 명이 리뷰해, W1 실측에서 프로젝트 속도를 코딩이 아니라 팀장의 지시서 생산 · 리뷰 속도가 결정한다는 것이 드러났다(ADR-0007). 인터페이스 리뷰 · 머지가 기본 주 1회 일괄이라 선행 PR 이 밀리면 받는 쪽이 일주일을 논다. 도메인 오너제로 전환하면서 이 병목을 없애는 것이 목적이었다
08. 28. ~ 09. 03. 승인 게이트 전면 폐지 — 전원 main 직접 push 근거는 "전원이 Claude 로 작업해 사람 검수가 형식화됐다"였다. 대가가 바로 나왔다 — 09. 01. 하루에 깨진 main 을 두 번 발견했고 둘 다 우연이었다(d7fdd10 · d96749e)
09. 04. ~ 현재 현행 main 직접 push 금지 → 브랜치 → PR → 자체 머지 (CI 초록 필수, 승인 불요) 승인 게이트는 여전히 없다 — 바뀐 것은 "main 에 닿는 경로가 PR 하나" 라는 점뿐이다. GitHub 브랜치 보호가 직접 push · force push 를 실제로 막으므로 규칙을 외울 필요가 없다 (818e693)

전환이 숫자로 보인다. main 의 머지 커밋 297건 중 09. 04. 이전은 8건뿐이고 나머지 289건이 그 이후다. 그 전의 변경은 대부분 main 직접 push 였다는 뜻이다. PR 번호는 #354까지 갔다.

왜 승인 게이트를 되살리지 않았나. 08. 28. 폐지의 근거(사람 검수의 형식화)는 그대로 유효하다고 봤다. 되살린 것은 리뷰어가 아니라 경로다 — 안전망은 사람이 아니라 CI 초록이고, CI 빨간불 = 머지 불가가 오너제의 유일한 강제다. 대신 책임이 옮겨 갔다: main 머지 = 2분 후 프로덕션 배포이므로 main 이 항상 동작하는 상태여야 할 책임이 리뷰어에서 머지하는 사람에게 넘어왔다. 깨진 main 을 발견한 사람은 묻지 않고 고치거나(fix-forward) revert 한다 — main 이 빨간불이면 다른 작업보다 우선이다. 이의는 사후에 말하고, 합의가 안 되면 revert 가 기본값이다.

CI 5잡 — 게이트가 아니라 "깨졌다는 사실만 즉시 보이게"

잡 무엇을 왜 이 잡이 생겼는가
백엔드 린트 ruff check --select F 헥사고날 이동 뒤 미정의 이름 2건이 프로덕션 500 까지 갔다 — 그 줄을 타는 테스트가 없어 pytest 는 초록이었다. --select F 만 켠다: 스타일까지 켜면 기존 파일 수백 줄이 빨개져 아무도 안 보는 빨간 CI 가 된다
백엔드 테스트 alembic upgrade head → 모델↔이행 비교 → pytest 1,165건 (실제 PostgreSQL + pgvector) 모델↔이행 비교는 2026. 09. 17. 에 붙였다 — pytest 의 conftest 가 create_all 로 이행에 빠진 표를 채워 넣어 이행 누락이 CI 에서 가려지기 때문이다. alembic check 는 CHECK 제약의 값 목록을 안 봐서 09. 16.(메일 종류 password_setup 누락) · 09. 17.(#282 file_blobs) 두 번 새어 나갔다. 리비전 번호가 겹치면 test_alembic_revisions.py 가 CI 에서 잡는다. pytest-timeout 60초는 Docker 가 죽어 pytest 가 24분간 조용히 멈춘 사고에서 나왔다 — 멈춤을 침묵이 아니라 실패로
프론트 빌드 · 린트 oxlint + tsc -b + vite build (Node 22) 타입 · 린트 · 빌드가 한 잡에서 다 도는 것이 요점이다 — 빌드가 깨진 채 머지되면 Vercel 배포가 그대로 멈춘다
AI 면접 서버 시험 pytest 3파일 (Python 3.13) — 실시간 전사 · 판정 중계 · 화자 매칭 본체와 파이썬 버전 · 시스템 라이브러리(opencv · mediapipe · soundfile)가 달라 별도 잡으로 뗐다
앱 시험 flutter analyze + flutter test — 테스트 파일 30개 (Flutter 3.44.8) ADR-0010 의 iOS 호환 규율 중 Mac 없이 가능한 검증이 analyze 무경고까지다. 그것만은 CI 가 대신 지킨다

같은 브랜치에 연달아 push 하면 앞의 실행을 취소한다(concurrency · cancel-in-progress) — 무료 러너 분을 아끼기 위해서다.

커밋 · 문서 · 공지 규약

항목 규칙
커밋 메시지 <type>(<기능번호|도메인>): <한국어 요약> — feat fix docs test chore refactor. 예: feat(J7): 더미 지원서 10만 건 생성기 · fix(agent): 무관 질문 거절을 '데이터 근거' 기준으로. 리뷰가 없으므로 이력이 곧 기록이고, PR 본문에 무엇을 왜 바꿨는지 남긴다
검증 자기 파트 실행 · 테스트 결과를 커밋 메시지 한 줄로 남긴다. 테스트를 통과시키려고 주석 처리 · 우회 금지 — 실패하면 실패한 대로 적는다
스키마 변경 코드 + 01-erd.md + alembic 리비전을 같은 커밋에. 문서 갱신 없는 스키마 변경은 금지한다 — ERD 가 전원의 계약이기 때문이다
API 변경 같은 커밋에서 02-api.md 갱신. 요청 · 응답의 진실은 Swagger 이고 02-api.md 는 "무엇이 있는가"만 유지한다 — 이중 관리하지 않는다
사후 공지 (한 줄) 스키마 · API · 공용 문서(docs/00_overview/ · CLAUDE.md · .github/) · 남의 도메인 폴더를 고쳤을 때. 큰 변경이면 직접 고치지 말고 이슈로 오너에게 넘긴다
시크릿 .env(git 제외) + .env.example(키 이름만). 코드 · 커밋 · 로그에 키를 남기지 않는다

머지에서 배포까지 — 요청이 필요 없다

백엔드는 서버의 systemd 타이머(arda-deploy.timer)가 2분마다 main 을 폴링해 새 커밋이면 pull → build → alembic upgrade head → up -d 를 순서대로 돈다(로그 ~/deploy.log). 프론트는 Vercel 이 1~2분 내 자동 배포한다. 스키마 이행은 api 이미지 안에서 돈다 — 2026. 09. 07. 에 호스트 마운트를 없애고 이미지가 alembic/ 을 갖게 했다(PR #20). set -euo pipefail 이라 이행이 실패하면 배포가 거기서 멈추고 기존 컨테이너는 계속 산다. 그래서 "배포해 주세요" 요청이 사라졌고, 동시에 CI 초록인 PR 만 머지해야 할 이유가 생겼다 — 머지가 곧 프로덕션이다.

← 목차로