ai-rules handbook 왜 "프로젝트별 고정 포트 + 런타임 DB 검증"인가

왜 "프로젝트별 고정 포트 + 런타임 DB 검증"인가

**앱 포트도 DB 포트도 "프로젝트별 고정 할당"으로 관리하고, 앱 부팅 시 `current_database()`로 연결된 DB 이름을 검증한다.** 자동 포트 탐색(find-port)은 AI 에이전트 환경과 맞지 않아 deprecated.

guide 2026-04-15 docs/guide/LOCAL_PORT_POLICY.md

작성일: 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.mdDB 이름 충돌 방지 규칙은 정상 작동했다. meetflow_devax_studio_dev는 이름이 달라서 규칙에 안 걸림. 문제는 다른 층위에서 일어났다:

  • 포트 충돌: 호스트 5432를 두 컨테이너가 동시에 원함 → 먼저 뜬 컨테이너가 이김
  • 조용한 실패: docker-compose.yml"5432:5432" 고정 매핑은 포트가 사용 중이면 컨테이너를 Created 상태로 대기시키고 에러를 크게 띄우지 않음
  • 앱의 잘못된 자신감: .envDATABASE_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 다중 프로젝트 개발 환경에서 중요한 축:

  1. 다중 프로젝트 동시 실행 — 여러 프로젝트를 동시에 띄워 비교하면서 개발할 수 있는가?
  2. .env와 런타임 일치.env에 적힌 포트 번호와 실제 앱/DB 포트가 항상 같은가?
  3. AI 에이전트 예측 가능성 — 에이전트가 .env만 읽고 정확한 포트를 알 수 있는가?
  4. 외부 도구 접근성 — DBeaver, Postman, psql 등 GUI/CLI 도구가 바로 붙는가?
  5. 조용한 실패 방지 — 포트 충돌이 발생했을 때 즉시 에러로 터지는가?
  6. 규칙 문서 단순성 — 팀 온보딩 시 설명해야 할 예외 규칙 수가 적은가?

비교표

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(자동 탐색)는 이 신뢰 모델을 깨뜨린다. .envPORT=3000이 적혀 있지만 실제 앱은 3001에 떠 있을 수 있다. 에이전트가 curl localhost:3000으로 디버깅하면 엉뚱한 결과가 나오거나, 다른 프로젝트의 앱에 붙을 수 있다.

패턴 3(Docker 내부 네트워크)도 에이전트에게 과한 인지 부담을 준다. DB 쿼리 하나 돌리려면 docker compose exec db psql 형태로 컨테이너 경유해야 하고, docker-compose.yml의 서비스 이름을 매번 확인해야 한다. 프로젝트마다 서비스 이름이 다르면 더 복잡해진다.

1-B는 파일만 읽으면 모든 게 결정된다. .envDATABASE_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.ymlports에 프로젝트 고유 호스트 포트 명시
  • .envDATABASE_URL도 동일 포트 사용 → 파일 간 일관성 보장

런타임 검증 (E)

  • 앱 부팅 시 current_database() 확인 → 예상 DB 이름과 다르면 즉시 실패
  • .envEXPECTED_DB_NAME 추가
  • 신규 프로젝트 템플릿에 기본 포함
  • 기존 프로젝트는 발견 시 소급 적용

find-port 자동 탐색

  • deprecated — 신규 프로젝트에서 사용 금지
  • 기존 프로젝트에 있으면 점진적으로 제거 (마이그레이션 시)
  • 이유:
    1. .env와 런타임 포트가 틀어져 AI 에이전트가 혼란
    2. 외부 도구(Postman/DBeaver) 설정이 매번 꼬임
    3. 1-B 고정 할당이 있으면 애초에 충돌 자체가 드묾

패턴 3(Docker 내부 네트워크)

  • 고급 옵션으로만 언급 — 기본 정책 아님
  • 적용 가능 시나리오:
    • 프로덕션 환경과 구조를 최대한 맞추고 싶은 백엔드 엔지니어
    • 마이크로서비스 다수를 동시 운영해 DB 포트 관리가 한계에 달한 경우
  • AI 에이전트가 주 개발자인 환경에서는 부적합

실제 업계가 쓰는 패턴 (참고)

조직/상황 주로 쓰는 패턴
1인 개발자, 프로젝트 3~5개 1-B
스타트업 520명, 프로젝트 515개 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의 포트 그대로 사용
└── "조용히 실패"가 가능한 패턴 (자동 탐색) 회피

트레이드오프 — 이 정책이 포기하는 것

  1. 패턴 3의 "DB 충돌 원천 차단" — 여전히 레지스트리 관리를 빠뜨리면 충돌 가능. 대신 런타임 검증(E)이 터뜨려 주기 때문에 데이터 소실 위험은 제거됨.
  2. 패턴 2의 "바로 실행" — 신규 프로젝트 세팅 시 한 번 레지스트리 확인 필요. 대신 설정 한 번으로 이후 영구 예측 가능.
  3. "프로젝트 50개 이상으로 늘어났을 때의 포트 고갈" — 현재 규모(5~10개)에서는 문제 없음. 필요 시 패턴 3으로 점진 이행 가능.

이 트레이드오프는 "AI 에이전트가 헷갈리지 않는 단순함"을 최우선 가치로 두고 판단한 결과다.


관련 문서