주문 유형 컬럼에 값이 열두 가지 있었다. 무엇이 무엇인지 아는 사람이 없었다.
Table of contents
Open Table of contents
상황 — 값만 있고 뜻이 없었다
어떤 값이 얼마나 있는지부터 셌다.
SELECT order_type, COUNT(*) FROM orders GROUP BY order_type;
01 842,110
02 412,004
03 88,220
05 41,882
07 12,004
09 8,220
11 4,100
...
04 와 06 과 08 과 10 은 아예 없었다. 있다가 없어진 것인지 처음부터 없었는지 모른다.
코드를 뒤져도 사정은 같았다.
if ($order->order_type === '01') { ... }
elseif ($order->order_type === '03') { ... }
숫자만 있고 이름이 없어서 이 분기가 무엇을 가르는지 읽히지 않는다.
이 상태에서 조건을 짜려면 값을 넣어 보고 결과로 짐작해야 한다. 사람마다 다르게 짐작하고 그 짐작이 맞는지 확인할 자리도 없었다.
order_type 을 조건에 넣은 화면이 이미 여럿 있었는데 그것들이 같은 뜻으로 넣은 것인지도 알 수 없었다. 값의 뜻을 모르면 기존 코드가 맞는지조차 판정할 근거가 없다.
하나씩 추적했다
각 값이 어디서 들어가는지부터 찾았다.
$ grep -rn "order_type.*=.*'0[0-9]'\|order_type.*=.*\"[01][0-9]\"" --include=*.php src/
넣는 자리의 앞뒤를 읽으면 어떤 상황에서 그 값이 되는지가 나온다.
01 일반 주문
02 정기 배송
03 선물 주문
05 대량 구매
07 기업 구매
09 체험단
11 임직원 구매
04 와 06 과 08 과 10 은 넣는 자리가 아예 없었다.
두 자리 코드에 홀수만 쓴 이유는 아무도 몰랐다. 나중에 사이에 끼워 넣으려고 그랬을 수도 있지만 그 의도를 적어 둔 곳은 없었다.
문맥으로 확정된 것과 짐작으로 채운 것은 따로 표시해 뒀다. 나중에 보는 사람이 둘을 같은 무게로 읽으면 안 된다.
grep 에 안 잡힌 값은 넣는 코드가 지워졌다는 뜻이어서 옛 자료를 직접 열어 봤다. 그 시기의 주문이 어떤 성격이었는지로 짐작하는 것 말고 다른 방법이 없었다.
조치 — 상수와 코드 표
먼저 코드 쪽에 상수를 만들었다.
final class OrderType {
public const NORMAL = '01';
public const SUBSCRIPTION = '02';
public const GIFT = '03';
public const BULK = '05';
public const CORPORATE = '07';
public const TRIAL = '09';
public const EMPLOYEE = '11';
public const LABELS = [
self::NORMAL => '일반 주문',
...
];
}
OrderType::GIFT 로 쓰면 그 분기가 선물 주문을 가른다는 것이 읽힌다.
상수만 두면 화면이나 조회에서 쓸 수 없어서 표로도 만들었다.
CREATE TABLE code_order_type (
code char(2) NOT NULL,
name varchar(50) NOT NULL,
use_yn char(1) NOT NULL DEFAULT 'Y',
sort_no int NOT NULL,
memo varchar(200),
PRIMARY KEY (code)
);
memo 를 둔 것은 그 값이 왜 있는지를 적기 위해서였다.
INSERT INTO code_order_type VALUES
('01','일반 주문','Y',1,''),
('09','체험단','N',6,'2016년 종료. 옛 데이터에만 있음');
안 쓰는 값도 지우지 않고 use_yn 을 N 으로 뒀다. 옛 자료에 그 값이 남아 있으므로 이름은 계속 필요하다.
지우면 그 값이 다시 뜻을 모르는 숫자가 되고 이번 일을 다음 사람이 되풀이한다. 쓰지 않는 것과 없는 것을 use_yn 한 칸으로 갈라 두는 편이 쌌다.
화면에서 쓰게 했다
선택 상자와 목록은 필요한 값이 서로 달랐다.
$types = $this->code->get('order_type'); // use_yn='Y' 인 것만
새로 고를 수 있는 것은 지금 쓰는 값뿐이고 지나간 주문을 보여줄 때는 전부 필요하다.
$label = $this->code->label('order_type', $row['order_type']) ?? $row['order_type'];
표에 없는 값이 오면 코드를 그대로 보여 준다. 이름이 없다고 빈칸을 내보내면 무슨 값인지조차 모르게 된다.
그리고 그런 값이 나오면 기록에 남겼다.
if (!$label) { log_message('error', "정의 없는 order_type: {$row['order_type']}"); }
새 값이 생겼다는 것을 이 줄로 알게 된다. 화면은 안 깨지고 사람은 알게 되는 쪽으로 갈랐다.
label 이 비었을 때 예외를 던지는 안도 검토했지만 목록 한 줄 때문에 화면 전체가 안 뜨는 것은 과했다. 보여줄 수는 있는데 이름만 없는 상태라 화면을 멈출 이유가 없었다.
검증 — 상수와 표의 대조
같은 것이 두 곳에 있으니 한쪽만 고치면 어긋난다.
public function testCodeTableMatchesConstants(): void {
$inDb = $this->db->column("SELECT code FROM code_order_type");
$inCode = array_keys(OrderType::LABELS);
$this->assertEmpty(array_diff($inDb, $inCode), 'DB 에만 있는 코드');
$this->assertEmpty(array_diff($inCode, $inDb), '상수에만 있는 코드');
}
array_diff 를 양방향으로 둔 것은 어느 쪽에만 있는지까지 갈리게 하려는 것이었다.
한쪽에만 넣고 잊는 것이 이런 구조에서 가장 흔한 실수다. 그 실수가 자료에 값으로 남기 전에 여기서 걸린다.
표 쪽에만 넣으면 화면에는 나오는데 코드가 그 값을 모르고 상수에만 넣으면 반대가 된다. 어긋난 방향에 따라 증상이 달라서 array_diff 를 양쪽으로 둔 것이 실제로 도움이 됐다.
설정 — 새 값을 넣는 순서
새 유형이 생길 때의 순서를 정해 뒀다.
1. 코드 표에 넣는다 (뜻과 메모 포함)
2. 상수에 넣는다
3. 쓰는 코드를 넣는다
표를 먼저 두게 한 것은 뜻을 정하지 않고 값부터 쓰는 것을 막기 위해서였다.
무엇에 쓸 값인지 한 줄로 못 적으면 아직 넣을 때가 아니다. 순서를 반대로 하면 이번에 겪은 상황이 얼마 뒤에 그대로 되풀이된다.
같은 문제가 다른 컬럼에도 있는지 스키마에서 훑었다.
SELECT table_name, column_name FROM information_schema.columns
WHERE table_schema = 'order' AND (column_type LIKE 'char(2)%' OR column_type LIKE 'char(1)%');
스물세 개가 나왔고 그중 뜻이 어디에도 안 적힌 것이 열넷이었다.
한 번에 다 하지 못해서 코드에서 참조 횟수가 많은 것부터 손댔다. 어디까지 했는지를 목록에 표시해 뒀는데 그 표시가 없으면 다음 사람이 처음부터 다시 센다.
열넷 중 넷을 이번에 정리했고 나머지는 char(1) 짜리 여부 표시가 많아 급하지 않았다. 급한 것과 아닌 것을 목록에서 갈라 두면 남은 일이 얼마인지가 그 자리에서 보인다.
정리
- 코드 값만 있고 뜻이 없으면 매번 추측하게 된다
- 사람마다 다르게 짐작하고 맞는지 확인할 자리가 없다
- 값이 어디서 들어가는지 찾아 문맥으로 뜻을 알아낸다
- 문맥으로 확정한 것과 짐작한 것을 갈라 표시한다
- 상수로 만들면 분기가 무엇을 가르는지 읽힌다
- 화면과 조회에서 쓰려면 코드 표도 필요하다
- 안 쓰는 값도 지우지 않고
use_yn으로 가른다 - 표에 없는 값이 오면 그대로 보여 주고 기록에 남긴다
- 상수와 표를
array_diff로 양방향 대조한다 - 새 값은 표에 먼저 넣어 뜻을 정하고 나서 쓴다