왜 "프로젝트별 고정 포트 + 런타임 DB 검증"인가
**앱 포트도 DB 포트도 "프로젝트별 고정 할당"으로 관리하고, 앱 부팅 시 `current_database()`로 연결된 DB 이름을 검증한다.** 자동 포트 탐색(find-port)은 AI 에이전트 환경과 맞지 않아 deprecated.
작성일: 2026-04-15 관련 규칙: core/07-db.md, core/08-local-env.md 관련 change log: docs/changes/2026-04-15-db-port-policy.md
한 줄 요약
앱 포트도 DB 포트도 "프로젝트별 고정 할당"으로 관리하고, 앱 부팅 시 current_database()로 연결된 DB 이름을 검증한다. 자동 포트 탐색(find-port)은 AI 에이전트 환경과 맞지 않아 deprecated.
배경 — 2026-04-14 meetflow 데이터 소실 사건
타임라인
| 시점 | 이벤트 |
|---|---|
| 2026-04-09 | meetflow_pg_data 볼륨 최초 생성 + 회원가입 데이터 적재 |
| 이후 재시작 시 | ax-studio-db 컨테이너가 호스트 5432 선점 → meetflow-postgres 컨테이너가 Created 상태로 갇힘 |
| 이 기간 동안 | meetflow 앱이 localhost:5432에 접속 → 실제로는 ax-studio-db 컨테이너에 연결 → 그 안에 meetflow_dev DB가 생성됨 |
| 2026-04-14 | 포트 5436으로 변경 + docker compose down/up → 원래 meetflow_pg_data 볼륨에 붙었으나 테이블이 비어 있음 |
왜 기존 규칙이 못 잡았나
core/07-db.md 의 DB 이름 충돌 방지 규칙은 정상 작동했다. meetflow_dev와 ax_studio_dev는 이름이 달라서 규칙에 안 걸림. 문제는 다른 층위에서 일어났다:
- 포트 충돌: 호스트 5432를 두 컨테이너가 동시에 원함 → 먼저 뜬 컨테이너가 이김
- 조용한 실패:
docker-compose.yml의"5432:5432"고정 매핑은 포트가 사용 중이면 컨테이너를 Created 상태로 대기시키고 에러를 크게 띄우지 않음 - 앱의 잘못된 자신감:
.env의DATABASE_URL=...:5432/meetflow_dev만 보고 "5432에 연결됨"을 성공 로그로 출력. 실제로 어느 PostgreSQL 인스턴스에 붙었는지는 확인하지 않음
결과: 앱이 엉뚱한 인스턴스의 동명 DB에 회원가입 데이터를 적재 → 볼륨 바꾸는 순간 데이터가 사라진 것처럼 보임.
교훈
"DB 이름이 고유하다"만으로는 부족하다. 앱이 붙은 DB 인스턴스가 내가 의도한 인스턴스인지 확인하는 방어선이 추가로 필요하다.
로컬 포트 관리 패턴 비교
패턴 분류
| 패턴 | 설명 | 대표 사례 |
|---|---|---|
| 1-A 단일 포트 고정 | 모든 프로젝트가 같은 포트(예: 3000) 사용, 한 번에 하나만 실행 | 혼자 쓰는 토이 프로젝트 |
| 1-B 프로젝트별 고정 할당 | 프로젝트마다 고유한 포트 번호를 영구 소유 (3000/3010/3020…) | T3 Stack, Vercel 템플릿 |
| 2 자동 포트 탐색 | 실행 시점에 비어 있는 포트를 런타임이 선택 | Nuxt/Next 기본, find-port.mjs |
| 3 Docker 내부 네트워크 | DB는 호스트에 포트 노출 안 함, 앱 컨테이너만 내부 네트워크로 접근 | Netflix/Uber/마이크로서비스 |
평가 축
실제 AI 다중 프로젝트 개발 환경에서 중요한 축:
- 다중 프로젝트 동시 실행 — 여러 프로젝트를 동시에 띄워 비교하면서 개발할 수 있는가?
.env와 런타임 일치 —.env에 적힌 포트 번호와 실제 앱/DB 포트가 항상 같은가?- AI 에이전트 예측 가능성 — 에이전트가
.env만 읽고 정확한 포트를 알 수 있는가? - 외부 도구 접근성 — DBeaver, Postman,
psql등 GUI/CLI 도구가 바로 붙는가? - 조용한 실패 방지 — 포트 충돌이 발생했을 때 즉시 에러로 터지는가?
- 규칙 문서 단순성 — 팀 온보딩 시 설명해야 할 예외 규칙 수가 적은가?
비교표
| 축 | 1-A 단일 고정 | 1-B 프로젝트별 고정 | 2 자동 탐색 | 3 Docker 내부 |
|---|---|---|---|---|
| 다중 프로젝트 동시 실행 | ❌ 불가능 | ✅ 가능 | ✅ 가능 | ✅ 가능 |
.env ↔ 런타임 일치 |
✅ | ✅ | ❌ 틀어질 수 있음 | ✅ (단 내부만) |
| AI 에이전트 예측 가능성 | ✅ | ✅ 최상 | ❌ 런타임 확인 필요 | ⚠️ Docker 컨텍스트 이해 필요 |
| 외부 도구 접근성 | ✅ | ✅ 바로 | ⚠️ 포트 확인 필요 | ❌ exec 경유 필요 |
| 조용한 실패 방지 | ✅ 에러로 즉시 실패 | ✅ 에러로 즉시 실패 | ❌ 조용히 다른 포트로 뜸 | ✅ 충돌 자체가 불가 |
| 규칙 문서 단순성 | ✅ | ✅ "레지스트리 확인" 한 줄 | ❌ 탐색 로직 + DB 예외 | ⚠️ Docker 규칙 추가 필요 |
| DB 포트 충돌 원천 차단 | ❌ | ⚠️ 레지스트리 관리 필요 | ❌ 이번 meetflow 사례 | ✅ 원천 차단 |
| 프로덕션 환경과의 구조 유사도 | ❌ | ⚠️ 중간 | ❌ | ✅ |
왜 1-B가 AI 에이전트 환경에 최적인가
AI 에이전트는 런타임 상태를 직접 관찰하기 어렵다. 에이전트는 주로:
- 파일(
.env,docker-compose.yml,package.json)을 읽고 - 그 안에 적힌 값을 진실로 신뢰하며
- 명령어를 실행해 결과를 로그로 확인
패턴 2(자동 탐색)는 이 신뢰 모델을 깨뜨린다. .env엔 PORT=3000이 적혀 있지만 실제 앱은 3001에 떠 있을 수 있다. 에이전트가 curl localhost:3000으로 디버깅하면 엉뚱한 결과가 나오거나, 다른 프로젝트의 앱에 붙을 수 있다.
패턴 3(Docker 내부 네트워크)도 에이전트에게 과한 인지 부담을 준다. DB 쿼리 하나 돌리려면 docker compose exec db psql 형태로 컨테이너 경유해야 하고, docker-compose.yml의 서비스 이름을 매번 확인해야 한다. 프로젝트마다 서비스 이름이 다르면 더 복잡해진다.
1-B는 파일만 읽으면 모든 게 결정된다. .env의 DATABASE_URL=postgresql://...:5433/meetflow_dev만 봐도 에이전트는 "localhost:5433에 붙으면 meetflow DB"임을 확신할 수 있다. 이게 .env를 진실의 유일한 원천(single source of truth) 으로 만드는 방식이다.
왜 1-B + 런타임 검증(E)이 필요한가
1-B만으로는 여전히 "사람이 레지스트리 업데이트를 빠뜨리면 재발 가능"하다. 그래서 앱 부팅 시점에 추가 방어선을 둔다:
// 앱 부팅 시 1회 실행
const [{ db, port }] = await sql`SELECT current_database() AS db, inet_server_port() AS port`
if (db !== process.env.EXPECTED_DB_NAME) {
throw new Error(`❌ 엉뚱한 DB에 연결됨: ${db}@${port} (예상: ${process.env.EXPECTED_DB_NAME})`)
}
이번 meetflow 사례에서 이 검증이 있었다면:
- 첫 회원가입 요청 전에 앱이 기동 실패로 터짐
- 데이터 소실 0건
- 에이전트/사람이 로그만 보고 즉시 진단 가능
채택한 정책
앱 포트 (Frontend/Backend)
- 1-B 프로젝트별 고정 할당
- 번호는
~/.claude/projects/PORT_REGISTRY.md로 중앙 관리 - 기본 할당 규칙: 프로젝트당 10씩 증가
- Frontend: 3000, 3010, 3020, …
- Backend API: 4000, 4010, 4020, …
- DB: 5432, 5433, 5434, …
- 신규 프로젝트 세팅 시 에이전트가 레지스트리 조회 → 다음 비어 있는 번호 할당 → 레지스트리 업데이트
DB 포트
- 1-B 프로젝트별 고정 할당 (호스트 노출 유지)
- 이유: 에이전트/사람이
psql, DBeaver로 바로 붙을 수 있어야 함 (패턴 3의 exec 경유 부담 회피) docker-compose.yml의ports에 프로젝트 고유 호스트 포트 명시.env의DATABASE_URL도 동일 포트 사용 → 파일 간 일관성 보장
런타임 검증 (E)
- 앱 부팅 시
current_database()확인 → 예상 DB 이름과 다르면 즉시 실패 .env에EXPECTED_DB_NAME추가- 신규 프로젝트 템플릿에 기본 포함
- 기존 프로젝트는 발견 시 소급 적용
find-port 자동 탐색
- deprecated — 신규 프로젝트에서 사용 금지
- 기존 프로젝트에 있으면 점진적으로 제거 (마이그레이션 시)
- 이유:
.env와 런타임 포트가 틀어져 AI 에이전트가 혼란- 외부 도구(Postman/DBeaver) 설정이 매번 꼬임
- 1-B 고정 할당이 있으면 애초에 충돌 자체가 드묾
패턴 3(Docker 내부 네트워크)
- 고급 옵션으로만 언급 — 기본 정책 아님
- 적용 가능 시나리오:
- 프로덕션 환경과 구조를 최대한 맞추고 싶은 백엔드 엔지니어
- 마이크로서비스 다수를 동시 운영해 DB 포트 관리가 한계에 달한 경우
- AI 에이전트가 주 개발자인 환경에서는 부적합
실제 업계가 쓰는 패턴 (참고)
| 조직/상황 | 주로 쓰는 패턴 |
|---|---|
| 1인 개발자, 프로젝트 3~5개 | 1-B |
| 스타트업 5 |
1-B + 3 혼용 |
| 대기업/마이크로서비스 다수 | 3 |
| 오픈소스 풀스택 템플릿 (T3 Stack, create-t3-app, Vercel) | 1-B |
| Nuxt/Next 기본 dev 서버 | 2 (탐색) — 다만 프로덕션엔 부적합 |
흥미로운 사실: AI 에이전트 친화적인 T3 Stack, create-t3-app 등 인기 풀스택 템플릿은 모두 1-B다. "충돌은 세팅 한 번 고민하고 끝낸다"는 철학.
방어선 계층도
세팅 시점 방어
├── 1-B 프로젝트별 고정 포트 (레지스트리 기반)
├── docker-compose.yml ports 명시 (고정 호스트 포트)
└── .env의 DATABASE_URL 포트와 일치 강제
런타임 방어 (E)
└── 앱 부팅 시 current_database() 검증
→ 포트/인스턴스 틀어져도 여기서 터짐
에이전트 행동 규칙
├── 신규 세팅 시 PORT_REGISTRY 조회 + 업데이트
├── 외부 도구 접근은 .env의 포트 그대로 사용
└── "조용히 실패"가 가능한 패턴 (자동 탐색) 회피
트레이드오프 — 이 정책이 포기하는 것
- 패턴 3의 "DB 충돌 원천 차단" — 여전히 레지스트리 관리를 빠뜨리면 충돌 가능. 대신 런타임 검증(E)이 터뜨려 주기 때문에 데이터 소실 위험은 제거됨.
- 패턴 2의 "바로 실행" — 신규 프로젝트 세팅 시 한 번 레지스트리 확인 필요. 대신 설정 한 번으로 이후 영구 예측 가능.
- "프로젝트 50개 이상으로 늘어났을 때의 포트 고갈" — 현재 규모(5~10개)에서는 문제 없음. 필요 시 패턴 3으로 점진 이행 가능.
이 트레이드오프는 "AI 에이전트가 헷갈리지 않는 단순함"을 최우선 가치로 두고 판단한 결과다.
관련 문서
- core/07-db.md — DB 연결 검증 섹션 (E)
- core/08-local-env.md — 포트 고정 정책 + PORT_REGISTRY
- docs/changes/2026-04-15-db-port-policy.md — 정책 도입 배경과 결정 기록