한 매장에 한 브랜드인 줄 알았습니다 — 그 가정이 매장 13곳을 쪼갰다
같은 자리에 새 브랜드가 들어올 때마다 개설일을 다시 입력하고, 멀쩡한 매장 하나를 시스템에서는 두 개로 쪼개 등록하는 일이 반복되고 있었습니다. 프랜차이즈 ERP를 운영하며 마주친 일입니다. 현장이 이상한 게 아니라, 데이터 모델의 가정 하나가 현실과 어긋나 있었습니다.
이 시스템은 "한 매장은 한 브랜드를 가진다"는 전제 위에 서 있었습니다. 그래서 개설일과 교육일 같은 속성이 매장 테이블의 컬럼으로 박혀 있었습니다. 문제는 현실의 매장이 그렇게 단순하지 않았다는 점입니다.
1:1 가정은 어떻게 데이터를 쪼개는가
한 자리에 여러 브랜드가 동시에, 혹은 순차로 들어오는 복합매장이 흔한 도메인이었습니다. 그런데 모델이 매장:브랜드를 1:1로 전제하니, 같은 자리에 두 번째 브랜드가 들어오는 순간 그 사실을 담을 자리가 시스템에 없었습니다.
현장은 두 가지 우회로 버티고 있었습니다. 개설일을 새 브랜드 기준으로 덮어쓰거나, 하나의 자리를 매장 두 개로 쪼개 등록하는 방식입니다. 전자는 기존 브랜드의 개설 이력을 지웠고, 후자는 "한 자리 = 한 매장"이라는 물리적 사실을 깨뜨렸습니다. 이렇게 쪼개져 있던 매장이 13곳이었습니다.

개설일은 매장의 것이 아니었다
건물과 임대 계약을 나란히 놓으면 정리가 빠릅니다. 건물에는 준공일이 있을 뿐이고, 입주일은 건물이 아니라 각 임대 계약의 속성입니다. 세입자가 바뀔 때마다 건물 준공일을 고쳐 쓰는 관리사무소는 없습니다. 매장과 브랜드의 관계가 정확히 이 구도였습니다.
매장은 자리(place)이고, 브랜드·개설일·계약기간은 계약의 속성입니다. 그래서 매장(1):계약(N) 구조로 재모델링하고, 매장 테이블에 있던 개설일·교육일을 계약 테이블 기준으로 옮겼습니다. 개점 집계의 기준도 매장 개설일에서 계약 개설일로 재정의했습니다.
화면도 따라갔습니다. 매장 기본 정보에 계약별 개설일·기간·만료·위치를 한 번에 주는 통합 DTO를 만들고, 매장 단위의 단일 '개설일' 표시를 없애 계약별로 보여주도록 바꿨습니다.

항목 | 기존(1:1) | 재모델링(1:N) |
|---|---|---|
개설일 | 매장 컬럼 | 계약의 속성 |
복합매장 | 매장을 쪼개 등록 | 매장 1 + 계약 N |
해지 | 덮어쓰기·삭제 | 상태 변경 + 종료 사건행 |
개점 집계 | 매장 개설일 기준 | 계약 개설일 기준 |
관계의 카디널리티를 바로잡은 게 이번이 처음은 아니었습니다. 그래프 스키마를 다룰 때도 관계의 한쪽 주체가 사실은 한 타입이 아니었던 적이 있는데, 그때는 주체에 타입 두 개를 허용하는 방식으로 1:N을 풀었습니다. 모델이 담지 못하는 현실이 나타나면 우회가 아니라 관계 정의부터 의심하게 된 계기였습니다.
잘못 쪼갠 13곳을 다시 합치다
구조가 서자 병합이 가능해졌습니다. 쪼개져 있던 13곳의 계약·매핑·이력을 원매장으로 이동하고, 쪼개면서 갈라졌던 채널 매출을 재집계했으며, 남은 껍데기 매장은 비활성화했습니다. "한 자리 = 한 매장" 원칙이 그제서야 회복됐습니다.
병합은 되돌리기 어려운 작업이라 검증 장치를 걸었습니다. 개점 집계가 계약 기준으로 바뀐 덕분에, 매장을 합쳐도 연도별 개점 수는 변하지 않아야 정상입니다. 병합 전후로 이 숫자가 불변인지 확인하는 것으로 정합을 검증했습니다. 처음엔 병합 자체가 목표였지만, 진행해보니 이 불변 지표가 있어야 안심하고 손을 댈 수 있었습니다.
해지는 삭제가 아니라 사건이다
계약이 끝나면 어떻게 처리할 것인가도 이 구조 위에서 다시 정의됐습니다. 해지를 하드 삭제로 처리하면 그 자리에 어떤 브랜드가 있다가 나갔는지, 재계약이 언제 이어졌는지가 통째로 사라집니다.
그래서 해지는 상태 변경으로 남겼습니다. 사용 여부를 끄고 상태를 폐점으로 바꾸고 종료일을 기록한 뒤, 이력 대장에 '종료' 사건행을 추가합니다. 이력이 매장 기준으로 쌓이니 종료와 재계약의 계보가 전부 보존되고, 재계약은 신규 계약으로 이어붙습니다. 생애주기의 끝이 삭제가 아니라 이벤트가 된 셈입니다.

컬럼 하나 지우는 데도 전수조사
마지막 과제는 매장 테이블에 남은 개설일 컬럼을 아예 지울 수 있는가였습니다. 감이 아니라 전수조사로 답을 냈습니다. 계약 없는 활성 매장 0건, 계약 개설일이 비어 있는 건은 단 1건. 매장 단위의 단일 개설일은 파생조차 불필요하고, 읽는 쪽은 계약값으로 재구성하면 된다는 결론이 나왔습니다.
다만 즉시 DROP은 하지 않았습니다. 아직 그 컬럼을 읽는 코드가 남아 있어서, 읽기 이관 → 쓰기 중단 → DROP 순서로 별도 마이그레이션에 예약했습니다. 지워도 된다는 입증과 실제로 지우는 시점은 별개의 문제였습니다.
물론 1:N이 공짜는 아닙니다. 조회와 집계에 조인이 늘고, 정말 항상 1:1인 도메인이라면 쪼갤 이유가 없습니다. 복합매장이라는 1:N 사례가 실재했기 때문에 값어치가 있었던 재모델링입니다.
"A는 B를 하나 가진다"는 가정에 예외 사례가 실재하는지 — 예외가 하나라도 있으면 그 관계는 1:N입니다
속성이 지금 놓인 엔티티의 것인지, 둘 사이 관계(계약)의 것인지 — 주인이 바뀌면 컬럼도 옮길 대상입니다
잘못 쪼갠 엔티티를 병합할 때 불변이어야 할 집계 지표(개점 수 등)가 정의돼 있는지
종료·해지를 삭제로 처리해 재계약·이력 계보를 끊고 있지 않은지
컬럼 제거 전 전수조사로 입증하고, 읽기 이관 → 쓰기 중단 → DROP 순서를 지키는지
모델의 가정이 현실과 어긋나면 그 비용은 현장의 중복 입력과 잘못된 분할로 먼저 청구됩니다. 이번 작업에서 코드보다 먼저 고친 것은 "개설일은 누구의 것인가"라는 질문의 답이었습니다. 속성의 주인을 바로 찾고 나니 병합·해지·컬럼 제거가 전부 그 관계 위에서 자연스럽게 정의됐습니다.