목차

  1. 소개
  2. 개선 목적
  3. 테스트 코드 0%에서 시작하다
  4. 신뢰 기반 이원화 — 기술 얘기인 척하는 조직 문화 얘기
  5. 서비스를 멈추지 않고 테이블을 고치는 법
  6. TL이 되고 시작한 문서화
  7. 마치며

1. 소개

안녕하세요. 쏘카 자산개발(Asset)팀 백엔드 개발자 원스톤입니다.

저는 자산개발팀에서 존 관리 시스템을 포함한 다수의 내부 시스템을 담당하며, 쏘카 존과 차량 도메인을 개발하고 있습니다. 이 글에서는 6년 동안 비즈니스를 멈추지 않으면서 레거시 코드와 테이블 구조를 점진적으로 개선해 온 경험, 그리고 TL이 된 이후 팀 전체의 장애 대응 역량을 끌어올리기 위해 진행한 문서화 작업에 대해 공유하려 합니다.


2. 개선 목적

처음부터 계획이 있었던 건 아닙니다.

비즈니스 요구사항을 처리하다가 테이블 하나 고치고, 클래스 하나 분리하고, 그게 쌓이다 보니 6년이 됐습니다. 6년 동안 20여 개 테이블, 400여 개 컬럼 구조를 바꿨습니다. 시스템의 어디를 건드리면 어디가 흔들리는지 알았기 때문에, 비즈니스를 멈추지 않고 조금씩 안전하게 개선할 수 있었습니다.

같은 시기에 만들어진 다른 시스템들은 별도 고도화 프로젝트 기간이 필요했습니다. 우리 팀의 시스템은 아직 현역입니다.

이 글은 거창한 마이그레이션 후기가 아닙니다. “멈추지 않고 고쳐온 기록”에 가깝습니다. 정리하면, 해결하고자 했던 핵심 문제는 다음과 같습니다.

  • 테이블 구조 노후화: 초기 설계의 한계로 불필요한 컬럼과 비정규화된 구조가 누적
  • 서비스 간 결합도: 하나의 클래스가 너무 많은 책임을 가지고 있어 변경 비용이 높음 (이 주제는 차량재배치 레거시 개선기에서 상세히 다뤘습니다)
  • 빌드 환경 노후화: 프로젝트마다 빌드 설정이 다르고 표준화되지 않아 유지보수 비용이 높음 (이 주제는 레거시 Gradle 빌드 스크립트 개선기에서 상세히 다뤘습니다)
  • 지식의 속인화: 시스템 구조를 아는 사람이 한정되어, 장애 대응과 협업에 병목 발생

이 글에서는 테이블 구조 노후화지식의 속인화 문제를 중심으로 다룹니다.


3. 테스트 코드 0%에서 시작하다

처음 입사했을 때, 테스트 코드 커버리지는 0%였습니다. 팀 내에서도 테스트 추가가 필요하다는 논의가 있었고, 저도 동의했습니다. 그래서 테스트 케이스를 작성하기 시작했습니다.

그런데 문제가 있었습니다. 한 테이블의 컬럼이 70~100개였습니다. 테스트 케이스 하나를 만들려면 entity를 세팅하는 것만으로도 한 시간 이상이 걸렸습니다. 존 관리 시스템은 상당히 큰 프로젝트였고, 이 규모에서 컬럼 70개짜리 entity를 하나하나 세팅하는 건 현실적이지 않았습니다.

이 문제의 원인을 찾기 위해 ERD 구조를 파악하기 시작했습니다. 그 과정에서 현실의 업무 흐름과 코드, ERD 구성이 일치하지 않는다는 것을 알게 됐습니다. ERD를 보고 업무 프로세스를 파악하는 것이 불가능한 상태였습니다.

하나의 테이블에 모든 것이 담겨 있었다

예를 들어, 매매계약서, 근로계약서, 용역계약서가 있다고 가정하겠습니다. 이 세 가지는 서로 다른 정보를 담는 전혀 다른 문서입니다. 그런데 “계약서”라는 이름의 하나의 테이블에 모든 정보가 담겨 있었습니다.

계약서 테이블 (contract) — 컬럼 약 80개

type = '매매계약서'일 경우:
  매매_금액 = 50000000
  매매_대상 = '토지'
  근로_시작일 = NULL      ← 의미 없는 NULL
  근로_급여 = NULL         ← 의미 없는 NULL
  용역_기간 = NULL         ← 의미 없는 NULL
  용역_단가 = NULL         ← 의미 없는 NULL
  ...

type = '근로계약서'일 경우:
  매매_금액 = NULL         ← 의미 없는 NULL
  매매_대상 = NULL         ← 의미 없는 NULL
  근로_시작일 = '2020-01-01'
  근로_급여 = 3000000
  용역_기간 = NULL         ← 의미 없는 NULL
  용역_단가 = NULL         ← 의미 없는 NULL
  ...

type에 따라 나머지 컬럼 대부분이 NULL로 채워지는 구조였습니다. 혹시 성능을 위해 의도적으로 역정규화한 것은 아닌지 검토해 봤습니다. 조회 부하가 역정규화를 해야 할 정도로 심한가? 타당한 이유가 없었습니다. 단지 초기 설계 시점에 하나의 테이블로 만들었고, 이후 요구사항이 늘어나면서 컬럼이 계속 추가된 결과였습니다.

하나의 연결이 여러 의미를 가지고 있었다

비슷한 문제가 테이블 관계에도 있었습니다. A 테이블과 B 테이블이 M:N 관계로 연결되어 있었는데, B 테이블의 type 컬럼 값에 따라 역할이 완전히 달랐습니다.

A 테이블 ──── M:N ──── B 테이블 (type 컬럼)

type = 'PARKING'인 경우:
  → A 테이블의 데이터를 묶어주는 비즈니스 로직으로 사용

type = 'CLEANING'인 경우:
  → A 테이블의 추가 메타 정보로 사용

같은 M:N 관계인데, type에 따라 “데이터를 묶는 것”과 “메타 정보를 붙이는 것”이라는 전혀 다른 역할을 하고 있었습니다. ERD만 보면 하나의 연결이 여러 가지로 해석될 수 있었고, type이 어떻게 사용되는지 아는 사람만 비즈니스를 파악할 수 있었습니다.

이런 구조는 테이블만 보고 비즈니스를 이해하는 것을 불가능하게 만들었습니다. 결국 역할별로 테이블을 분리하여, ERD만으로도 비즈니스 흐름이 읽히는 구조로 개선했습니다.

이미 상당히 커져버린 시스템의 유지보수를 위해서는 ERD 개선과 코드 개선이 함께 이루어져야 한다고 판단했습니다.

저는 비즈니스, 코드, ERD는 일맥상통해야 한다고 생각합니다. ERD를 보면 비즈니스가 읽혀야 하고, 코드를 보면 ERD가 그려져야 합니다. 이 셋이 어긋나는 순간, 구조를 모두 알고 있는 한 사람에게 시스템이 종속됩니다. 그 사람이 빠지면 아무도 시스템을 건드리지 못하게 됩니다.

현재 상태

6년간의 개선을 거쳐, 현재 테스트 케이스는 350개, 테스트 커버리지는 약 10%입니다.

지표 입사 시점 현재
테스트 케이스 0개 350개
테스트 커버리지 0% 약 10%
테이블당 평균 컬럼 수 70~100개 20~30개

숫자만 보면 아직 부족합니다. 하지만 테스트를 작성할 수 있는 구조로 바꾸는 것이 먼저였고, 그 과정이 곧 이 글에서 다루는 ERD 개선과 코드 개선이었습니다.

테스트 코드를 짜려다 보니 ERD가 문제였고, ERD를 고치려다 보니 코드도 함께 고쳐야 했습니다. 테스트 0%가 6년간의 개선을 시작하게 만든 출발점이었습니다.


4. 신뢰 기반 이원화 — 기술 얘기인 척하는 조직 문화 얘기

레거시 개선에서 가장 어려운 건 기술이 아닙니다. 허락입니다.

테이블 구조를 바꾼다는 건, 중간 상태가 존재한다는 뜻입니다. 기존 테이블과 새 테이블이 공존하는 이원화 기간이 반드시 생깁니다. 이 기간 동안 데이터 정합성을 보장해야 하고, 롤백 플랜도 있어야 합니다.

문제는 이 이원화 기간에 담당자가 퇴사하면 ERD가 반쪽짜리로 남는다는 것입니다. 회사 입장에서는 리스크입니다. 그런데도 진행하게 해줬습니다.

이건 두 가지가 전제되어야 가능합니다.

  • 담당자의 신뢰: “이 사람이 시작하면 끝낸다”는 신뢰
  • 조직의 용기: 리스크를 알면서도 장기적 개선을 선택하는 의사결정

이원화를 시작할 때마다 남긴 세 가지 문서

저는 이원화를 시작할 때마다 다음 세 가지를 문서로 남겼습니다.

문서 목적
현재 상태 기존 테이블 구조와 의존 관계
목표 상태 변경 후 구조
롤백 방법 담당자가 없어도 되돌릴 수 있는 절차

이 문서가 있으면, 이원화 도중에 누가 빠져도 다른 사람이 이어받거나 되돌릴 수 있습니다. 조직 입장에서는 리스크가 줄고, 허락의 근거가 됩니다.

레거시 개선은 기술력만으로 되지 않습니다. 조직이 허락해야 하고, 그 허락은 신뢰에서 나옵니다. 신뢰는 “완료”의 반복에서 나옵니다.


5. 서비스를 멈추지 않고 테이블을 고치는 법

“운영 중인 테이블 컬럼을 삭제해도 괜찮나요?”

괜찮았습니다. 단, 순서가 있습니다.

원칙: 코드 먼저, 스키마 나중

컬럼을 삭제하고 싶다면, 삭제하기 전에 그 컬럼을 참조하는 코드를 모두 제거해야 합니다.

Phase 1: 코드에서 해당 컬럼 참조 제거 → 배포
Phase 2: 안전 확인
Phase 3: ALTER TABLE ... DROP COLUMN

Phase 2에서 확인한 것은 다음 세 가지입니다.

  1. SELECT * 제거 확인: JPA와 Exposed에서 해당 컬럼을 포함하는 SELECT * 쿼리가 더 이상 발생하지 않는지 확인
  2. 이원화 데이터 정합성 확인: 기존 컬럼과 새 컬럼(또는 새 테이블)에 데이터가 이중으로 정상 적재되고 있는지 확인
  3. 코드 참조 완전 제거 확인: 코드에서 기존 컬럼을 직접 참조하는 곳이 없는지 전수 확인

이 세 가지가 모두 충족된 시점에 Phase 3을 진행했습니다. Phase 3 시점에는 이미 해당 컬럼을 읽는 쿼리가 없습니다. 그래서 안전합니다.

변경 예시)

-- Phase 1 — 코드 변경
-- 변경 전: 해당 컬럼을 SELECT에 포함
SELECT id, name, legacy_flag FROM zones;

-- 변경 후: 해당 컬럼 참조 제거
SELECT id, name FROM zones;
-- → 배포 후 모니터링

-- Phase 3 — 스키마 변경 (코드에서 더 이상 참조하지 않는 것을 확인한 뒤)
ALTER TABLE zones DROP COLUMN legacy_flag;

MySQL 8.0의 Online DDL

MySQL 8.0에서 컬럼 삭제 시 내부적으로는 다음과 같은 일이 일어납니다.

단계 동작 서비스 영향
MDL 획득 메타데이터 락 (짧게) 수 ms 수준, 체감 불가
테이블 리빌드 데이터를 새 구조로 복사 SELECT, INSERT, UPDATE 모두 가능
MDL 해제 메타데이터 갱신 수 ms 수준, 체감 불가

Table Lock이 아니라 Metadata Lock(MDL) 입니다. 그리고 리빌드 중에도 Online DDL이 지원되므로 읽기/쓰기가 모두 가능합니다.

Tip. MDL(Metadata Lock)이란? MySQL에서 테이블 구조(메타데이터)를 변경할 때 잠깐 거는 락입니다. 데이터 자체를 잠그는 Table Lock과 달리, 구조 변경이 진행 중일 때 다른 DDL이 동시에 실행되지 않도록 보호하는 역할을 합니다. 보통 수 ms 수준으로 매우 짧습니다.

데이터 규모가 크지 않았기 때문에 리빌드 시간도 짧았고, 해당 컬럼을 참조하는 쿼리가 이미 없었기 때문에 MDL이 잠깐 걸려도 서비스 영향은 없었습니다.

정리하면, 컬럼 삭제가 안전했던 이유는 다음과 같습니다.

  1. 코드에서 이미 참조를 제거한 상태
  2. Online DDL로 리빌드 중에도 서비스 정상
  3. 데이터 규모가 작아 리빌드 시간이 짧음
  4. MDL 구간이 짧아 실질적 영향 없음

핵심은 DDL 자체가 안전한 게 아니라, DDL을 실행해도 안전한 상태를 먼저 만든 것입니다.


6. TL이 되고 시작한 문서화

팀 리드가 되고 나서 가장 먼저 느낀 문제가 있었습니다.

가용 인원이 있는데도, 장애 판단이 느리다.

우리 팀은 도메인이 많았습니다. 10개의 시스템을 담당하고 있었고, 각 시스템의 맥락을 전부 아는 사람은 없었습니다. 외부 채널링 시스템에서 동기화 이슈가 터져도, 담당자가 아니면 “이게 뭔지”부터 파악해야 했습니다.

이 문제를 해결하기 위해 두 가지를 진행했습니다.

6-1. Postman 팀 워크스페이스

가장 먼저 한 건 Postman에 팀 워크스페이스를 만든 것입니다.

Postman Team Workspace
├── 시스템 A - Collections
│   ├── 동기화 API
│   ├── 상태 조회 API
│   └── 수동 보정 API
├── 시스템 B - Collections
│   ├── ...
└── Docs
    ├── 장애 시 확인 순서
    ├── 각 API 사용 시나리오
    └── 환경별 변수 설정

단순히 API를 모아둔 게 아닙니다. “이 장애가 발생하면 이 API를 이 순서로 호출하라”는 대응 시나리오를 Docs에 함께 기록했습니다.

변경 예시)

// 변경 전: 장애 발생 시 흐름
장애 인지 → 담당자 찾기 → 담당자에게 상황 설명 → 담당자가 판단 → 대응
(소요 시간: 담당자 가용 여부에 따라 수십 분 ~ 수 시간)

// 변경 후: 장애 발생 시 흐름
장애 인지 → Postman Docs 확인 → 가용 인원이 직접 대응
(소요 시간: 수 분)

효과는 명확했습니다. 외부 시스템 동기화 이슈가 발생했을 때, 담당자가 아닌 가용 인원이 Postman을 열고 Docs를 보고 직접 대응할 수 있게 됐습니다.

장애 대응의 병목이 “사람” 에서 “문서” 로 바뀌었습니다.

6-2. 10개 시스템 ERD 작성

우리 팀이 담당하는 10개 시스템에는 FK가 없었습니다. ERD도 없었습니다.

테이블 간의 관계는 코드를 읽어야만 알 수 있었고, 그마저도 히스토리를 아는 사람에게 물어봐야 정확했습니다. 비즈니스 논의를 할 때 PM이나 사업 담당자에게 데이터 구조를 설명하려면 매번 화이트보드에 그려야 했습니다.

10개 시스템의 ERD를 모두 작성해서 Confluence에 올렸습니다.

Confluence
└── 시스템 ERD
    ├── 시스템 A - ERD (테이블 관계도 + 주요 컬럼 설명)
    ├── 시스템 B - ERD
    ├── ...
    └── 시스템 J - ERD

변경 예시)

// 변경 전: 데이터 구조 확인 방법
"이 데이터 어디에 있어요?" → 담당자에게 질문 → 담당자가 코드를 읽고 답변
(담당자 부재 시 확인 불가)

// 변경 후: 데이터 구조 확인 방법
"이 데이터 어디에 있어요?" → Confluence ERD 링크 공유
(누구나, 언제든 확인 가능)

기대한 건 개발팀 내부 공유였는데, 실제로는 PM과 사업 담당자가 더 많이 활용했습니다. “이 데이터가 어디에 있어요?” 라는 질문에 링크 하나로 답할 수 있게 됐습니다.


7. 마치며

6년 동안 한 일을 한 문장으로 줄이면 이렇습니다.

비즈니스를 멈추지 않으면서, 매일 조금씩 어제보다 나은 구조로 만들었습니다.

고도화 프로젝트를 따로 잡지 않았습니다. 비즈니스 요구사항을 처리하면서 “이왕 건드리는 김에” 조금씩 고쳤습니다. 그게 6년 동안 쌓이니 시스템이 됐습니다.

되돌아보면 중요했던 건 네 가지입니다.

  1. 구조가 먼저: 테스트를 짤 수 있는 구조를 만드는 것이 테스트보다 먼저였다
  2. 조직의 신뢰: 이원화 리스크를 감수하고 진행하게 해준 의사결정
  3. 안전한 순서: 코드 먼저, 스키마 나중. 상태를 안전하게 만든 후 변경
  4. 문서화: 지식을 사람에서 분리해서, 누구나 대응할 수 있는 구조

레거시는 한 번에 고치는 게 아닙니다. 매일 고치는 것입니다.