NestJS MSA 로컬 개발 환경 구축 트러블슈팅 기록
들어가며
NestJS 기반 마이크로서비스 아키텍처(MSA)의 로컬 개발 환경을 처음부터 구축하면서 겪은 트러블슈팅을 정리합니다.
5개의 NestJS 서비스 + Apollo Federation Gateway + Elasticsearch를 로컬에서 완전히 기동하는 것이 목표였고, Docker Compose로 MySQL, PostgreSQL, Redis, Elasticsearch 컨테이너를 구동하는 구성입니다.
내부 정보(DB 호스트, 계정, 인프라 구조 등)는 모두 제외하고,
다른 개발자에게도 도움이 될 일반적인 트러블슈팅 패턴만 정리했습니다.
1. DB 이름 불일치 — .env와 Docker Compose 설정 확인
문제
Unknown database 'my_app_db'
원인
.env에 설정된 DB 이름과 Docker Compose에서 실제 생성한 DB 이름이 달랐습니다.
Docker Compose의 MYSQL_DATABASE 환경변수로 생성되는 DB 이름과, 애플리케이션의 .env에 적힌 DB 이름이 일치하지 않으면 이 에러가 발생합니다.
해결
Docker Compose 파일에서 실제 생성되는 DB 이름을 확인하고, .env의 접속 정보를 일치시켰습니다.
개선
당연한 것 같지만, MSA 환경에서 서비스마다 각각의 .env와 Docker Compose 설정이 있으면 이런 단순 불일치가 의외로 잘 발생합니다.
서비스별 .env 템플릿을 팀 차원에서 관리하면 온보딩이 훨씬 수월해집니다.
아니면 그냥 로컬 개발 환경 자체를 깃/깃헙으로 전달하는 것도 방법일 수 있을 것 같습니다.
2. Prisma 마이그레이션 FK 제약조건 에러 — 운영 전제 마이그레이션의 함정
문제
npx prisma migrate deploy 실행 시, 특정 마이그레이션 SQL에서 FK(Foreign Key) 에러 발생. 데이터를 INSERT하는 마이그레이션인데, 참조하는 부모 테이블에 데이터가 없어 FK 위반.
원인
해당 마이그레이션은 운영 DB에 이미 데이터가 존재하는 상태를 전제로 작성된 것이었습니다. 로컬은 빈 DB이므로 참조 데이터가 없어 FK 위반이 발생합니다.
한 번 실패한 마이그레이션은 _prisma_migrations 테이블에 "failed" 상태로 기록되어, 수정 없이 재실행도 불가합니다.
해결
- _prisma_migrations 테이블에서 실패 기록 삭제
- 해당 migration.sql의 INSERT문에 WHERE NOT EXISTS 조건 추가 (데이터가 없으면 스킵)
- 재실행하여 전체 마이그레이션 적용 완료
개선
운영 기준으로 작성된 마이그레이션은 빈 DB에서 반드시 실패할 수 있습니다.
이 수정은 로컬 전용이므로 절대 원격에 push하면 안 됩니다.
이상적으로는 팀 차원에서 "초기 시드 스크립트"를 별도로 관리하는 것이 좋습니다.
3. Sequelize sync()의 한계 — 레거시 스키마는 DDL 덤프가 필수
현상
SequelizeDatabaseError: Failed to add the foreign key constraint.
Missing index for constraint '...' in the referenced table '...'
원인
레거시 서비스의 MySQL은 Prisma가 아닌 Sequelize의 sync()로 DDL을 생성하는 구조였습니다. 그런데 레거시 테이블 간 FK 관계가 복잡하여, sync()만으로는 올바른 순서로 테이블을 생성할 수 없었습니다.
실패한 시도
- npm run dev로 그냥 기동 → Sequelize sync에서 FK 에러
- Prisma migrate로는 Sequelize가 관리하는 테이블이 대상이 아님
해결
테스트/스테이징 DB에서 전체 DDL + 데이터를 덤프하고 로컬에 복원했습니다.
# 복원 시 FK 체크를 임시로 비활성화
mysql -h 127.0.0.1 -P <port> -u <user> -p<password> \
--init-command="SET FOREIGN_KEY_CHECKS=0;" \
<database_name> < dump.sql
개선
Sequelize sync()는 운영 스키마를 완벽히 재현하지 못합니다. 특히 레거시 시스템에서 테이블 간 FK 관계가 복잡하면, sync()가 생성 순서를 제어하지 못해 실패합니다. 이런 경우 운영/테스트 DB에서 DDL 포함 전체 덤프 → 로컬 복원이 유일한 방법입니다.
4. Apollo UsageReporting 플러그인 크래시 — 환경 분기의 중요성
문제
Error: You've enabled usage reporting via ApolloServerPluginUsageReporting,
but you also need to provide your Apollo API key and graph ref
원인
app.module.ts에서 ApolloServerPluginUsageReporting()을 무조건 등록하고 있었습니다.
이 플러그인은 APOLLO_KEY + APOLLO_GRAPH_REF 환경변수가 없으면 크래시합니다.
임시 해결 (비추천)
코드에서 해당 플러그인을 주석 처리. 하지만 이 방법은 주석 처리한 채로 push할 위험이 있습니다.
올바른 해결 — 환경변수 기반 조건 분기
// app.module.ts
plugins: [
...(process.env.NODE_ENV === 'production' ||
(process.env.APOLLO_KEY && process.env.APOLLO_GRAPH_REF)
? [require('apollo-server-core').ApolloServerPluginUsageReporting()]
: []),
],
이렇게 하면:
- 운영 (환경변수 존재): Usage Reporting 활성화
- 로컬 (환경변수 없음): 자동 비활성화
- 코드를 수정할 필요 없이 .env만으로 제어 가능
- push해도 운영에 영향 없음
개선
환경 분기는 코드 주석이 아닌 환경변수로 해야 합니다. 주석 처리/해제는 push 사고의 원인이 됩니다. NODE_ENV나 특정 환경변수의 존재 여부로 분기하면, 코드를 건드리지 않고도 로컬/운영 동작을 분리할 수 있습니다. 이런 개선은 팀 전체에 적용해도 안전하므로 PR로 올리는 것을 권장합니다.
5. Apollo Gateway — IPv6 해석 문제와 서비스 기동 순서
문제 1: IPv6 해석
ECONNREFUSED ::1:3023
macOS에서 localhost가 ::1 (IPv6)로 resolve될 수 있는데,
서비스들은 127.0.0.1 (IPv4)에서만 listen하고 있어서 연결이 거부될 수 있습니다..
해결: Gateway 설정의 subgraph URL에서 localhost를 127.0.0.1로 변경.
문제 2: 서비스 기동 순서
Apollo Federation Gateway는 기동 시 모든 subgraph의 스키마를 fetch합니다. subgraph가 아직 떠있지 않으면 스키마를 가져올 수 없어 실패합니다.
해결: 하위 서비스들을 먼저 기동한 후 Gateway를 마지막에 기동.
서비스 A → 서비스 B → 서비스 C → ... → Gateway (마지막)
개선
macOS의 IPv6 resolve 문제는 흔합니다. 로컬 설정에서는 127.0.0.1을 명시하는 것이 안전합니다.
Federation Gateway는 모든 subgraph가 떠있어야 기동 가능하므로, 기동 순서를 문서화해두면 팀 전체가 시간을 절약합니다.
6. Elasticsearch 마이그레이션 — Zero-Downtime Reindex 패턴의 초기 부트스트랩
배경: 버전 인덱스 + Alias 패턴
이 프로젝트는 Zero-Downtime Reindex 패턴을 사용합니다:
애플리케이션 → alias(posts) → 실제 인덱스(posts_v3)
- 애플리케이션은 항상 alias를 통해 인덱스에 접근
- 매핑 변경 시: 새 인덱스(_v4) 생성 → 기존 인덱스에서 reindex → alias 스왑
- 장점: 무중단 매핑 변경, alias 롤백 가능
- 단점: reindex 중 디스크 2배 사용, alias 관리 오류 시 꼬임
문제
ResponseError: index_not_found_exception: no such index [posts]
원인
마이그레이션 스크립트의 reindex 함수가 alias가 이미 존재한다는 전제 하에 설계되어 있었습니다. 로컬 초기 환경에서는 alias도 인덱스도 없으므로 404 에러가 발생합니다.
해결: 2-Phase Bootstrap
Phase 1 — 인덱스 + alias 생성:
reindex 함수 시작 부분에 alias 존재 체크를 추가합니다:
// reindex 함수 시작 부분
const aliasExists = await elastic.indices.existsAlias({ name: aliasName });
if (!aliasExists.body) {
console.log(`[skip reindex] alias "${aliasName}" does not exist (initial bootstrap)`);
await refreshAndRestoreInterval(elastic, newIndexName);
return;
}
1차 마이그레이션 실행: 각 인덱스의 _v1 생성 → reindex 스킵 → alias 연결
Phase 2 — 데이터 벌크 인덱싱:
마이그레이션 테이블의 checksum을 리셋하여 "변경이 있는 것처럼" 만든 뒤 2차 실행:
UPDATE elasticsearch_migrations
SET checksum = CONCAT('reset_', id), mappings_checksum = NULL
WHERE name != 'config';
2차 마이그레이션 실행: checksum 불일치 감지 → bulk index 경로로 진입 → DB에서 데이터를 읽어 ES에 인덱싱
개선
"운영 기준 incremental" 스크립트는 초기 부트스트랩을 고려하지 않는 경우가 많습니다. "기존 인덱스/alias가 있다"는 전제 하에 설계된 스크립트는, 빈 환경에서 반드시 실패합니다. alias 존재 체크 같은 방어 로직을 정식으로 반영하면 모든 신규 팀원의 온보딩이 수월해집니다.
7. 코드 수정 시 주의사항
로컬 환경 구축 과정에서 코드를 수정해야 하는 경우가 있습니다. 이때 반드시 지켜야 할 원칙:
수정 유형별 관리
수정 유형 예시 push 가능 여부
| 환경변수 (.env) | DB 접속 정보, 포트 등 | .gitignore에 포함되어 있으므로 안전 |
| 환경 분기 코드 | NODE_ENV 기반 플러그인 활성화 | ✅ PR로 올려도 안전 (팀 전체 개선) |
| 로컬 전용 패치 | 마이그레이션 SQL 수정, reindex 스킵 | ⚠️ push 전 반드시 원복 |
로컬 전용 패치 관리 팁
- git stash로 로컬 패치를 보관하고, 작업 브랜치 전환 시 git stash pop으로 복원
- 또는 .git/info/exclude에 로컬 전용 파일을 등록하여 실수로 커밋되지 않게 방지
- 가능하면 로컬 전용 패치가 필요 없도록 환경변수 분기로 전환하는 것이 최선
8. 배운 점 정리
1. 레거시 시스템의 로컬 환경은 DDL 덤프가 필수
Sequelize sync()가 운영 스키마를 완벽히 재현하지 못한다면, 운영/테스트 DB에서 DDL을 포함한 전체 덤프가 유일한 방법입니다. 레거시 마이그레이션 도구의 한계를 인정하고 현실적인 방법을 택하는 것이 시간을 절약합니다.
2. "운영 기준 incremental" 스크립트의 초기 부트스트랩 문제
ES 마이그레이션처럼 "기존 데이터/인덱스가 있다"는 전제 하에 설계된 스크립트는, 빈 환경에서 반드시 실패합니다. 이를 방어하는 코드(alias 존재 체크, 조건부 INSERT 등)를 정식으로 반영하면 온보딩이 훨씬 수월해집니다.
3. 환경 분기는 코드 주석이 아닌 환경변수로
NODE_ENV나 특정 환경변수의 존재 여부로 분기하면, 코드를 건드리지 않고도 로컬/운영 동작을 분리할 수 있습니다. 주석 처리/해제를 반복하는 것은 push 사고의 원인이 됩니다.
4. macOS의 IPv6 resolve 문제
localhost → ::1 (IPv6)로 해석되어 IPv4만 listen하는 서비스에 연결이 안 되는 문제는 흔합니다. 로컬 설정에서는 127.0.0.1을 명시하는 것이 안전합니다.
5. MSA 환경의 서비스 기동 순서
Federation Gateway는 모든 subgraph가 떠있어야 기동 가능합니다. 기동 순서를 README나 스크립트로 문서화해두면 팀 전체가 시간을 절약합니다.