로컬 개발 환경에서 제일 무서운 문장은 이거다.
“어? 데이터가 다 날아갔나?”
오늘도 비슷한 장면으로 시작했다. 분명 어제까지 로그인도 되고, 라이선스도 들어가 있고, 테스트 데이터도 있던 로컬 서버였다. 그런데 다른 워크트리에서 서버를 띄웠더니 화면이 낯설었다. 마치 새로 설치한 제품처럼 보였다.
순간 등골이 서늘해진다.
DB를 날린 걸까? 마이그레이션이 꼬인 걸까? Docker volume이 바뀐 걸까?
결론부터 말하면, 데이터는 날아가지 않았다. 우리는 그냥 “다른 냉장고”를 열고 있었다.
문제는 코드가 아니라 주소였다
QueryPie 같은 큰 제품은 로컬에서도 꽤 많은 부품이 같이 돈다.
- API
- Front
- Engine
- Redis
- MySQL
- Envoy
- 여러 보조 서비스
여기에 패치 작업용 git worktree가 붙으면 상황이 더 재미있어진다. 기본 개발 트리 하나만 쓰는 게 아니라, 여러 워크트리를 오가며 서버를 띄운다.
이때 acpx workspace는 각 워크트리에 offset을 붙인다. 예를 들어 이런 식이다.
- 기본 workspace: MySQL
3306, Redis6379 - 어떤 패치 workspace: MySQL
3706, Redis6779 - 또 다른 패치 workspace: MySQL
3906, Redis6979
이 구조 자체는 나쁘지 않다. 오히려 격리 테스트에는 좋다. 문제는 “항상 격리 DB가 좋은 것은 아니다”라는 점이다.
대부분의 QCP/UI 확인에서는 기존 로컬 데이터가 필요하다. 로그인 계정, 라이선스, 설정, 테스트 fixture가 유지되어야 한다. 그런데 worktree를 바꿀 때마다 다른 offset DB를 보게 되면, 제품은 새로 설치된 것처럼 보인다.
데이터가 사라진 게 아니다.
그냥 냉장고 A에 있던 반찬을 냉장고 B에서 찾고 있었던 것이다.
그래서 기본값을 바꿨다
오늘의 핵심 개선은 단순하다.
기본값은 shared DB.
격리 DB는 명시적으로 켤 때만.
즉 평소에는 어느 worktree에서 서버를 띄우든 DB와 Redis는 canonical local DB를 본다.
# 일반적인 패치/UI 확인
QUERYPIE_ROOT=/path/to/worktree ./dev-scripts/querypie-local-ensure.sh
이 경우 내부적으로는 다음 기준을 따른다.
DB mode: shared
MySQL: 3306
Redis: 6379
DB workspace: querypie-mono
Data policy: preserve
반대로 깨끗한 DB에서 마이그레이션이나 설치 경로를 검증하고 싶다면 이렇게 명시한다.
# schema-sensitive / clean-install 검증
QUERYPIE_DB_MODE=isolated \
QUERYPIE_ROOT=/path/to/worktree \
./dev-scripts/querypie-local-start.sh
이때만 worktree offset 기반 DB를 쓴다.
DB mode: isolated
MySQL: 3306 + offset * 100
Redis: 6379 + offset * 100
Data policy: disposable
이름만 보면 작은 옵션 하나지만, 실제 효과는 크다. 로컬 개발의 기본 감각이 “매번 새 DB일 수도 있음”에서 “기본은 내 메인 DB를 보존함”으로 바뀐다.
shared와 isolated는 성격이 다르다
둘은 우열 관계가 아니다. 용도가 다르다.
shared DB는 사무실 공용 냉장고다.
항상 같은 곳에 있다. 매일 꺼내 먹던 반찬이 그대로 있다. 로그인 계정도 있고, 라이선스도 있고, 익숙한 테스트 데이터도 있다. 제품 기능을 확인하기 좋다.
isolated DB는 실험실 냉장고다.
오늘만 쓰고 버려도 된다. 마이그레이션을 마음껏 돌려도 된다. 새 설치, schema 변화, 파괴적 변경을 검증하기 좋다.
문제는 실험실 냉장고를 사무실 냉장고처럼 쓰기 시작할 때 생긴다. 반찬이 없다고 놀라고, 누가 치웠냐고 의심하고, 사실은 방을 잘못 들어온다.
그래서 도구가 먼저 말해줘야 한다.
“지금 어느 냉장고를 열고 있는지.”
상태 파일에 DB의 정체를 남기기
이번에는 runtime state에도 DB 정체성을 남기도록 했다.
예를 들면 이런 정보다.
db_mode=shared
db_profile=local-main-latest
db_root=/path/to/querypie-mono
db_workspace=querypie-mono
workspace_offset=0
infra_mysql_port=3306
infra_redis_port=6379
schema_relation=db_ahead_of_branch
이건 디버깅할 때 꽤 중요하다.
예전에는 “서버가 떠 있다” 정도만 알 수 있었다. 이제는 “어느 코드에서 떴고, 어느 DB를 보고 있으며, 그 DB schema가 branch와 어떤 관계인지”까지 볼 수 있다.
로컬 런타임에서 좋은 상태 파일은 블랙박스와 같다. 사고가 나지 않았을 때는 조용하지만, 이상한 일이 생기면 바로 단서를 준다.
schema는 버전명이 아니라 Flyway로 본다
여기서 한 가지 함정이 더 있다.
“branch가 11.6.4니까 DB도 11.6.4면 맞겠지?”
그럴 듯하지만 위험하다. 제품 버전명은 힌트일 뿐이고, DB의 실제 상태는 migration history가 말해준다.
그래서 이번에는 DB의 flyway_schema_history와 branch 안의 migration 파일을 비교해 관계를 나눴다.
matching: DB와 branch schema가 맞음db_behind_branch: DB가 branch보다 뒤처짐db_ahead_of_branch: DB가 branch보다 앞섬unknown: 확인 불가
특히 중요한 건 db_ahead_of_branch다.
shared DB가 branch보다 앞서 있다면, 자동으로 되돌리면 안 된다. DB downgrade는 로컬에서도 위험하다. 그래서 이 경우에는 migration을 강행하지 않고 건너뛴다.
로그는 이런 식으로 말한다.
Skipping infra migrate: shared DB schema is ahead of this branch;
never auto-downgrade or re-baseline.
Use QUERYPIE_DB_MODE=isolated for schema-sensitive checks.
이 문장은 꽤 마음에 든다.
도구가 “내가 똑똑하게 알아서 해볼게요”라고 말하지 않는다. 대신 “여기서 자동으로 하면 위험하니, schema 검증은 isolated로 해주세요”라고 말한다.
좋은 자동화는 가끔 멈출 줄 알아야 한다.
라이선스도 엉뚱한 문을 두드리고 있었다
중간에 또 하나의 함정이 있었다.
helper가 API를 고정 포트 8080으로 띄우고 있는데, 라이선스 등록 도구는 workspace offset이 반영된 포트를 바라볼 수 있었다. 그러면 서버는 떠 있는데 라이선스가 들어가지 않는다. 화면에서는 라이선스 입력 페이지가 나온다.
이건 아주 얄미운 버그다.
서버는 살아 있다. DB도 살아 있다. 그런데 제품은 “라이선스 주세요”라고 한다. 원인은 라이선스 등록 요청이 다른 문을 두드리고 있었기 때문이다.
그래서 라이선스 주입은 active root의 helper script를 통해 API 8080으로 직접 들어가게 고정했다.
즉 “제품이 실제로 듣고 있는 문”에 라이선스를 넣는다.
정리도 중요했다
정책을 바꾸는 것만으로는 부족했다. 이미 만들어진 offset DB들이 남아 있으면 다음 디버깅 때 또 헷갈릴 수 있다.
그래서 기존 offset DB 컨테이너와 데이터 디렉터리를 정리했다.
다만 workspace registry와 offset 정보는 남겼다. 이건 중요하다.
- DB 데이터는 정리한다.
- workspace identity는 보존한다.
- 다음에 isolated를 명시하면 같은 offset 규칙으로 다시 만들 수 있다.
청소를 하되, 주소록까지 찢어버리지는 않는 느낌이다.
검증은 이렇게 했다
이번 개선은 말로만 안정화했다고 하지 않았다. 실제로 worktree를 오가며 확인했다.
확인한 흐름은 대략 이렇다.
- 패치 worktree를 기본 shared DB로 기동한다.
- API health, Envoy login, JS bundle을 확인한다.
- canonical DB의 users/certificates가 유지되는지 본다.
- 기본 worktree로 전환해도 같은 shared DB를 보는지 확인한다.
- 다시 패치 worktree로 돌아와도 offset DB를 만들지 않는지 확인한다.
- isolated mode를 명시했을 때만 offset DB가 생성되는지 확인한다.
- 검증 후 offset DB를 다시 정리한다.
- fast ensure가 재시작 없이 healthy를 인식하는지 확인한다.
마지막 상태는 이랬다.
DB mode: shared
DB workspace: querypie-mono
MySQL: 3306
Redis: 6379
Offset DB ports: not reachable
Runtime: healthy
이 정도면 적어도 “worktree를 바꿨더니 DB가 초기화된 것처럼 보이는 문제”는 재현 방지까지 들어갔다고 볼 수 있다.
물론 이것이 전체 제품 regression suite green이라는 뜻은 아니다. 이건 로컬 런타임 helper의 동작 검증이다. 하지만 오늘 해결하려던 문제에는 정확히 맞는 검증이다.
이번 일의 교훈
로컬 개발 환경의 안정성은 대단한 마법에서 나오지 않는다.
대부분은 이런 작은 질문에서 나온다.
- 지금 내가 보는 코드 root가 맞나?
- 지금 내가 보는 DB가 맞나?
- 이 DB는 보존용인가, 실험용인가?
- schema가 branch보다 앞서나, 뒤처지나?
- 자동으로 밀어붙이면 위험한 순간은 어디인가?
- 성공했다고 말하기 전에 사용자가 실제로 보는 입구까지 확인했나?
오늘의 결론은 이렇다.
로컬 DB는 생각보다 쉽게 사라지지 않는다. 대신 개발자가 생각보다 쉽게 다른 DB를 본다.
그러니 좋은 로컬 도구는 서버를 잘 띄우는 것에서 끝나면 안 된다. 지금 보고 있는 세계가 어떤 세계인지 알려줘야 한다.
공용 냉장고인지, 실험실 냉장고인지.
그걸 구분해주는 순간, “데이터 날아갔나?”라는 공포는 “아, 문을 잘못 열었네”라는 해프닝이 된다.
그리고 개발자는 다시 기능을 고칠 수 있다.
그게 오늘의 작은 안정화였다.