테스트 커버리지 0%에서 시작해 6년간 서비스 멈추지 않고 테이블을 고친 기록
![]()
거창한 마이그레이션 후기가 아니에요. 쏘카 자산개발팀 백엔드 개발자 원스톤이 쓴 이 글은 "멈추지 않고 고쳐온 기록"에 가까워요. 6년 동안 20여 개 테이블, 400여 개 컬럼 구조를 바꿨대요. 별도의 고도화 프로젝트 기간 없이. 비즈니스를 멈추지 않으면서. 비즈니스 요구사항을 처리하다가 테이블 하나 고치고, 클래스 하나 분리하고, 그게 쌓이다 보니 6년이 된 거예요.
같은 시기에 만들어진 다른 시스템들은 별도 고도화 프로젝트 기간이 필요했다고 해요. 쏘카존 관리 시스템은 아직 현역이고요. 처음부터 계획이 있었던 건 아니에요. 시스템의 어디를 건드리면 어디가 흔들리는지 알았기 때문에, 조금씩 안전하게 개선할 수 있었던 거죠.
테스트를 짜려면 ERD를 먼저 고쳐야 했다
입사 시점, 테스트 코드 커버리지 0%. 팀 내에서도 테스트 추가가 필요하다는 논의가 있었고, 그래서 테스트 케이스를 작성하기 시작했어요.
근데 문제가 있었어요. 한 테이블의 컬럼이 70~100개였거든요. 테스트 케이스 하나 만들려면 entity를 세팅하는 것만으로도 한 시간 이상이 걸렸어요. (진지하게요, 컬럼 70개짜리 entity를 일일이 세팅하는 걸 상상해 보세요.)
원인을 파고들었더니 ERD 구조가 문제였어요. 현실의 업무 흐름과 코드, ERD 구성이 일치하지 않았어요. ERD를 보고 업무 프로세스를 파악하는 것 자체가 불가능한 상태.
구체적으로 어떤 식이었냐면요. 매매계약서, 근로계약서, 용역계약서 — 서로 다른 정보를 담는 전혀 다른 문서잖아요. 근데 "계약서"라는 이름의 하나의 테이블에 컬럼 약 80개가 다 들어가 있었어요.
``` type = '매매계약서'일 경우: 매매_금액 = 50000000 매매_대상 = '토지' 근로_시작일 = NULL ← 의미 없는 NULL 근로_급여 = NULL ← 의미 없는 NULL 용역_기간 = NULL ← 의미 없는 NULL ... ```
type에 따라 나머지 컬럼 대부분이 NULL로 채워지는 구조. 성능을 위한 의도적 역정규화인지 검토해봤지만 타당한 이유가 없었어요. 초기 설계 시점에 하나의 테이블로 만들었고, 이후 요구사항이 늘어나면서 컬럼이 계속 추가된 결과였을 뿐이에요.
비슷한 문제가 테이블 관계에도 있었어요. A 테이블과 B 테이블이 M:N으로 연결되어 있는데, B 테이블의 `type` 컬럼 값에 따라 역할이 완전히 달랐어요. `type = 'PARKING'`이면 A 데이터를 묶어주는 비즈니스 로직, `type = 'CLEANING'`이면 A의 추가 메타 정보. 같은 M:N 관계가 두 가지 전혀 다른 의미를 가지고 있었던 거예요. ERD만 보면 하나의 연결이 여러 가지로 해석될 수 있고, `type`이 어떻게 사용되는지 아는 사람만 비즈니스를 파악할 수 있었죠.
결국 역할별로 테이블을 분리해서 ERD만으로도 비즈니스 흐름이 읽히게 만들었어요. 비즈니스, 코드, ERD는 일맥상통해야 한다는 게 원스톤의 생각이에요. 이 셋이 어긋나는 순간, 구조를 모두 알고 있는 한 사람에게 시스템이 종속돼요. 그 사람이 빠지면 아무도 시스템을 건드리지 못하게 되고요.
6년간의 결과. 테스트 케이스 0개 → 350개. 커버리지 0% → 약 10%. 테이블당 평균 컬럼 수 70~100개 → 20~30개. 숫자만 보면 아직 부족하죠. 근데 테스트를 작성할 수 있는 구조로 바꾸는 것이 먼저였고, 그 과정이 곧 ERD 개선과 코드 개선이었어요.
테스트 코드를 짜려다 보니 ERD가 문제였고, ERD를 고치려다 보니 코드도 함께 고쳐야 했다. 테스트 0%가 6년간의 개선을 시작하게 만든 출발점.
레거시 개선에서 가장 어려운 건 기술이 아니라 허락이다
이 파트가 솔직히 가장 와닿았어요.
테이블 구조를 바꾼다는 건 중간 상태가 존재한다는 뜻이에요. 기존 테이블과 새 테이블이 공존하는 이원화 기간이 반드시 생겨요. 이 기간 동안 데이터 정합성을 보장해야 하고, 롤백 플랜도 있어야 하고요. 근데 이원화 기간에 담당자가 퇴사하면? ERD가 반쪽짜리로 남아요. 회사 입장에서는 리스크. 그런데도 진행하게 해줬다는 거예요.
이건 두 가지가 전제되어야 가능하다고 해요. 담당자의 신뢰 — "이 사람이 시작하면 끝낸다"는 믿음. 조직의 용기 — 리스크를 알면서도 장기적 개선을 선택하는 의사결정.
원스톤은 이원화를 시작할 때마다 세 가지 문서를 남겼어요. 기존 테이블 구조와 의존 관계, 변경 후 목표 구조, 담당자가 없어도 되돌릴 수 있는 롤백 절차. 이 문서가 있으면 이원화 도중에 누가 빠져도 다른 사람이 이어받거나 되돌릴 수 있거든요. 조직 입장에서는 리스크가 줄고, 허락의 근거가 되는 거죠.
레거시 개선은 기술력만으로 되지 않는다. 조직이 허락해야 하고, 그 허락은 신뢰에서 나온다. 신뢰는 "완료"의 반복에서 나온다.
코드 먼저, 스키마 나중 — 운영 중인 컬럼을 안전하게 삭제하는 법
"운영 중인 테이블 컬럼을 삭제해도 괜찮나요?" 괜찮았대요. 단, 순서가 있어요.
원칙은 간단해요. 코드 먼저, 스키마 나중.
``` Phase 1: 코드에서 해당 컬럼 참조 제거 → 배포 Phase 2: 안전 확인 Phase 3: ALTER TABLE ... DROP COLUMN ```
Phase 2에서 확인한 건 세 가지예요. JPA와 Exposed에서 해당 컬럼을 포함하는 `SELECT *` 쿼리가 더 이상 발생하지 않는지. 기존 컬럼과 새 컬럼에 데이터가 이중으로 정상 적재되고 있는지. 코드에서 기존 컬럼을 직접 참조하는 곳이 없는지 전수 확인.
이 세 가지가 모두 충족된 시점에 Phase 3을 진행하면, 이미 해당 컬럼을 읽는 쿼리가 없는 상태예요. 그래서 안전하죠.
MySQL 8.0의 Online DDL도 여기서 한몫했어요. 컬럼 삭제 시 MDL(Metadata Lock)이 수 ms 수준으로 짧게 걸리고, 리빌드 중에도 읽기/쓰기가 모두 가능하거든요. Table Lock이 아니라 Metadata Lock이라는 점이 중요해요. 데이터 규모가 크지 않았기 때문에 리빌드 시간도 짧았고, 이미 참조하는 쿼리가 없었기 때문에 MDL이 잠깐 걸려도 서비스 영향은 없었어요.
정리하면 컬럼 삭제가 안전했던 이유는 네 가지예요. 코드에서 이미 참조를 제거한 상태, Online DDL로 리빌드 중에도 서비스 정상, 데이터 규모가 작아 리빌드 시간이 짧음, MDL 구간이 짧아 실질적 영향 없음.
핵심은 DDL 자체가 안전한 게 아니라, DDL을 실행해도 안전한 상태를 먼저 만든 것.
이 한 문장이 전부예요. 기술적으로 DDL이 가능하다고 바로 실행하는 게 아니라, 실행해도 아무 일 안 일어나는 상태를 먼저 만드는 거예요.
TL이 되고 나서 깨달은 것 — 장애 대응의 병목은 사람이었다
팀 리드가 되고 나서 가장 먼저 느낀 문제. 가용 인원이 있는데도 장애 판단이 느리다는 것.
10개의 시스템을 담당하고 있었는데, 각 시스템의 맥락을 전부 아는 사람은 없었어요. 외부 채널링 시스템에서 동기화 이슈가 터져도, 담당자가 아니면 "이게 뭔지"부터 파악해야 했거든요.
첫 번째로 한 건 Postman에 팀 워크스페이스를 만든 거예요. 단순히 API를 모아둔 게 아니에요. "이 장애가 발생하면 이 API를 이 순서로 호출하라"는 대응 시나리오를 Docs에 함께 기록했어요.
``` 변경 전: 장애 인지 → 담당자 찾기 → 설명 → 판단 → 대응 (수십 분~수 시간) 변경 후: 장애 인지 → Postman Docs 확인 → 가용 인원이 직접 대응 (수 분) ```
효과는 명확했어요. 외부 시스템 동기화 이슈가 터졌을 때, 담당자가 아닌 가용 인원이 Postman을 열고 Docs를 보고 직접 대응할 수 있게 됐거든요. 장애 대응의 병목이 "사람"에서 "문서"로 바뀐 거예요.
두 번째는 10개 시스템의 ERD를 전부 작성해서 Confluence에 올린 거예요. FK가 없었어요. ERD도 없었어요. 테이블 간의 관계는 코드를 읽어야만 알 수 있었고, 그마저도 히스토리를 아는 사람에게 물어봐야 정확했거든요.
기대한 건 개발팀 내부 공유였는데, 실제로는 PM과 사업 담당자가 더 많이 활용했대요. "이 데이터 어디에 있어요?"라는 질문에 링크 하나로 답할 수 있게 된 거예요. (예상 못 한 효과가 오히려 더 컸던 거죠.)
매일 조금씩 — 고도화 프로젝트를 따로 잡지 않았다
6년 동안 한 일을 한 문장으로 줄이면 이거예요. "비즈니스를 멈추지 않으면서, 매일 조금씩 어제보다 나은 구조로 만들었다."
고도화 프로젝트를 따로 잡지 않았어요. 비즈니스 요구사항을 처리하면서 "이왕 건드리는 김에" 조금씩 고쳤어요. 그게 6년 동안 쌓이니 시스템이 된 거예요.
되돌아보면 중요했던 건 네 가지라고 해요.
구조가 먼저. 테스트를 짤 수 있는 구조를 만드는 것이 테스트보다 먼저였어요. 조직의 신뢰. 이원화 리스크를 감수하고 진행하게 해준 의사결정. 안전한 순서. 코드 먼저, 스키마 나중. 상태를 안전하게 만든 후 변경. 문서화. 지식을 사람에서 분리해서, 누구나 대응할 수 있게.
레거시는 한 번에 고치는 게 아니에요. 매일 고치는 거예요.