'쓰기도 하고 읽기도 하는데' 죽은 코드다
단일 진실 공급원(SSOT) 이중화와 흔적 필드
난이도: ★★★☆☆ 연관 노트: 상태 없는 서버로 CRM 지표 만들기
핵심 요약
- dead code = 어디서도 참조되지 않는 코드. 문제의 컬럼은 저장(write)도 되고 편집기가 다시 읽기(read)도 했다 → 실행은 일어남 → dead 아님. (읽기·쓰기는 있는데 하류 소비처가 0인 상태 = 흔적(vestigial) 필드.)
- 그런데도 위험한 건 — dead code는 아무것도 안 하는 무해한 침묵이지만, 이건 대시보드가 그 소스를 근거로 그럴듯한 숫자를 적극 생산한다. 실제 진실(다른 저장소)과 어긋난 “틀린 값을 진짜인 척” 내보내는 것 → 아무도 에러를 못 봐서 dead code보다 나쁘다.
- 이중 소스를 발견하면 결말은 둘: 합칠 수 있으면 하나로(SSOT), 불가피하면(과거 데이터 동결 등) 경계를 명시적으로 선언해 “우연한 드리프트”를 “선언된 분기”로 바꾼다.
왜 헷갈렸나
- 소비처가 없는 컬럼을 보고 처음엔 **“write-only”**라 불렀다 → 편집기가 다시 읽으니 틀림.
- 그럼 “dead code냐?” → 실행은 되니 그것도 틀림.
- 정확한 이름은 “vestigial(흔적) 필드”: read/write는 일어나는데 그 값으로 실제 벌어지는 일이 없음.
- 더 헷갈린 지점: “안 쓰이면 그냥 무해한 거 아냐?” → 아니다. 같은 데이터가 두 곳(하드코딩 정의 vs DB)에 있고, 화면은 그중 stale한 쪽을 읽어 틀린 숫자를 생산하고 있었다.
메커니즘
[설정 UI] ──save──▶ DB 컬럼 ──read──▶ [편집기] 다시 표시
│
└──▶ (발송? 대시보드?) ──▶ 아무도 안 읽음 ← 소비처 0 = 흔적
SSOT 이중화:
발송 경로 ─────▶ DB (진짜 소스)
대시보드 ───────▶ 하드코딩 정의 (다른 소스) ← 둘이 조용히 어긋남
│
▼
"그럴듯하지만 틀린 숫자"를 화면에 생산
dead code vs 이 상황:
- dead code: 실행 안 됨 → 출력 없음 → 무해.
- 이중 소스 드리프트: 실행됨 → 출력 있음 → 근데 틀린 출력. 에러도 안 나서 발견이 늦다.
해결 패턴
두 결말 중 상황에 맞게:
① 합칠 수 있다 → SSOT
발송·대시보드 모두 DB(단일 소스)를 읽게. 하드코딩 정의는 은퇴.
② 못 합친다(과거분 동결 등) → 선언된 분기
버전/기간으로 소스를 명시적으로 가른다:
v_old → 동결된 하드코딩 정의 (의도적 보존)
v_new → DB
"우연한 이중화"가 아니라 "선언된 경계"로 만들어 드리프트를 막는다.
다음에 이 상황을 만나면
“이 값이 왜 두 군데 있지?” 싶으면 → 소비처를 추적해라.
- 이 필드/코드의 하류 소비처가 실제로 있나? (읽기·쓰기만으로 “살아있다” 판단 금지)
- 같은 개념의 데이터가 두 소스에 있나? 화면은 어느 쪽을 읽나? → 드리프트 가능성
- 합칠 수 있나? 없으면 경계를 코드로 선언(버전/기간)했나, 아니면 우연히 갈라진 채인가?
- “안 쓰이니 무해”라고 넘기지 마라 — 틀린 값을 조용히 생산 중일 수 있다
커넥팅 닷
← 선행 개념 (이걸 알아야 이해된다)
- Single Source of Truth (SSOT) — 하나의 사실은 한 곳에서만 관리. 이게 깨진 게 “이중 소스”.
- dead code / 도달 불가능 코드의 정의 — “실행되느냐”로 판단해야 vestigial과 구분된다.
→ 확장 개념 (여기서 더 나아가면)
- expand–contract(확장-수축) 마이그레이션 — 이중 소스를 안전하게 하나로 접는 단계적 방법(코드 먼저 무해하게 → 데이터 이전 → 레거시 제거).
- feature flag / 버전 스코프 — “못 합칠 때 경계를 선언”의 구현.
↔ 같은 원리가 적용되는 곳
- 상태 없는 서버로 CRM 지표 만들기 — 계산·정의를 한 곳(쿼리)에 모아 드리프트를 없애는 같은 지향.
- 캐시 무효화 / 파생 데이터 — 원본과 사본이 어긋나는 모든 곳에 같은 “드리프트” 문제가 있다.
부록
“write-only”라는 말도 상황에 따라 실재한다(로그처럼 쓰기만 하고 사람이 안 읽는 것). 하지만 그건 “감사/디버깅용으로 언젠가 읽힐 값”이라는 명시적 목적이 있다. 목적도 소비처도 없이 read/write만 도는 게 vestigial이다.