← 업계·문화

스키마 마이그레이션 도구를 고르다 마지막 한 칸을 조직 상황으로 갈랐다

유료 등급과 트랜잭션 버그와 롤포워드 한계로 후보를 하나씩 떨어뜨린 뒤, 결승에 남은 goose와 tern은 조직 상황으로 갈랐다. 그 선택을 정당하게 만든 것은 앞 단계 기각 사유의 검증 가능성이다

목차
  1. 문제
  2. 후보는 기술로 하나씩 떨어졌다
  3. 마지막 한 칸은 조직 상황이 갈랐다
  4. 이긴 논거만 남기지 않았다
  5. 후보 선정 논거를 갈아치웠다
  6. 기각 사유의 과장을 낮췄다
  7. 결론
  8. 한계·재검토 트리거

데이터베이스 스키마 마이그레이션 도구 하나를 고르는 데 7단계 결정 체인이 들었다.
후보는 유료 등급과 트랜잭션 처리 버그와 설계 한계로 하나씩 떨어졌다.
결승에 남은 둘은 기술 비교로 갈리지 않았다.

문제

도구를 고르기 전에 스키마의 정본 형식부터 정해야 했다.
처음 잡아둔 "데이터베이스 무관 SQL"은 폐기했다.
pgvector와 halfvec, 벡터 검색용 HNSW 인덱스, 스키마를 넘는 외래키가 전부 Postgres 전용이라 이식 가능이라는 전제가 실제로 쓸 스키마와 맞지 않았다.
정본은 Postgres 네이티브 SQL로 확정됐다.

그다음은 SQL을 정본으로 두느냐 애플리케이션 모델을 정본으로 두느냐였다.
SQL-first로 갔다.
Postgres 기능 밀도가 높아 Alembic의 자동 생성이 이 스키마를 따라오지 못한다는 것이 결정적이었다.
두 결정이 끝나고 남은 것은 SQL 마이그레이션을 버전 관리할 도구 하나였다.
여기서부터 후보가 붙었다.

후보는 기술로 하나씩 떨어졌다

첫 탈락은 Atlas였다.
제품 문서를 직접 확인하니 pgvector 인덱스를 읽어 오는 기능과 RLS(행 수준 보안)가 유료 등급이었고 트리거 지원은 로그인이 필요한 베타였다.
위 세 제약과 정면으로 충돌했다.
migra는 저장소가 스스로 폐기를 고지하고 있었다.
남은 후보는 두 사이클에 걸쳐 훑었다.

도구기각 사유
Atlaspgvector 인덱스 조회와 RLS가 유료 등급, 트리거는 로그인 필요한 베타
migra저장소에 폐기가 명시돼 있음
dbmate트랜잭션을 끄는 설정이 HNSW 인덱스와 CONCURRENTLY 생성 경로에서 어긋남
sqitchPerl 런타임 설치 비용, 모든 마이그레이션을 트랜잭션으로 감쌈
Flyway / LiquibaseJVM 요구, 되돌리기 기능이 유료
graphile롤포워드 전용

여섯 줄의 사유는 전부 남이 다시 확인할 수 있다.
가격표에 적혀 있거나, 저장소 첫 화면에 붙어 있거나, 이슈 트래커에 상태로 남아 있다.
결승에 goose와 tern이 남았다.

마지막 한 칸은 조직 상황이 갈랐다

두 도구는 기술로 갈리지 않았다.
tern의 강점은 마이그레이션 재실행 워크플로이고, 그 강점이 값을 하려면 데이터베이스 코드가 자주 바뀌어야 한다.
그런데 이 프로젝트의 데이터베이스 코드는 배관 성격의 조각 3~4개로 한정돼 있었다(결정 시점의 범위 산정 기준).
tern이 잘하는 자리가 아예 생기지 않는 상태였다.

그 국면에서 지배적이었던 것은 인수인계다.
사내 모노레포 전반이 넘기고 받는 작업으로 채워져 있었고, 도구 선택의 값은 배우는 비용과 넘기는 비용에서 났다.
그래서 배우기 쉽고 넘기기 쉬운 쪽을 골랐다.
최종 채택은 goose였다.

여기서 결정을 가른 것은 그 시점의 조직 상황이었다.
그래서 tern은 버리지 않고 탈출구로 남겼다.
데이터베이스 코드 변경량이 폭증하면 이 결정을 승계해 tern으로 간다는 조건을 같은 문서에 적었다.
조직 상황을 근거로 쓴 결정은 조직 상황이 바뀌면 흔들리므로, 흔들릴 조건을 결정 옆에 붙여 두는 것이 짝이다.

이긴 논거만 남기지 않았다

같은 결정 문서 안에서 내 논거를 두 번 폐기했다.

후보 선정 논거를 갈아치웠다

SQL-first를 고르며 처음 든 근거는 여러 스택이 같은 데이터베이스를 쓴다는 것이었다.
그런데 이 프로젝트는 앞선 결정에서 이미 단일 스택으로 못 박혀 있었다.
근거가 앞선 결정과 모순이라 그대로 버리고 세 개로 갈아 끼웠다.
공유 자원의 정본 위치와 소유권, Postgres 기능 밀도가 Alembic 자동 생성을 무력화한다는 점, 그 시점에 이미 나와 있던 스키마 산출물이 SQL이라는 점이다.

기각 사유의 과장을 낮췄다

dbmate의 결함은 처음에 오래 방치됐다는 취지로 적었다.
확인해 보니 부정확했다.
관련 이슈 세 건은 하나가 열려 있고, 하나는 완료됐고, 하나는 계획 없음으로 닫힌 상태였다(당대 이슈 트래커 조회 기준).
표현을 그 상태에 맞게 낮추고, 기각 근거를 방치 여부에서 트랜잭션 처리 동작 자체로 옮겨 다시 세웠다.

결론

결정문의 품질은 기각 사유의 검증 가능성이 정한다.
채택 사유는 쓴 사람 손에 있고, 기각 사유는 읽는 사람이 다시 확인할 수 있다.
기각 여섯 줄이 가격표와 저장소 고지와 이슈 상태로 채워졌기 때문에 마지막 한 칸을 조직 상황으로 채워도 근거가 무너지지 않았다.

조직 상황을 결정 근거로 쓰는 데는 조건이 붙는다.
그 앞이 전부 검증 가능한 사실이어야 하고, 뒤집힐 조건이 같은 문서에 적혀 있어야 한다.
폐기한 논거를 지우지 않고 남긴 것도 같은 계열이다.
이긴 논거만 남기면 읽는 사람은 그 결정이 어디서 한 번 흔들렸는지 볼 수 없다.
7단계 결정 체인에서 조직 상황으로 채운 칸은 마지막 하나뿐이고, 나중에 이 문서를 다시 여는 사람이 먼저 볼 곳은 기각 절이다.

한계·재검토 트리거

2026-09 기준 / 재검토 트리거: 데이터베이스 코드가 배관 수준을 넘어 변경량이 계속 늘거나, goose가 비트랜잭션 실행 경로를 더 지원하지 않게 되면 결승 판정을 다시 연다.