본문으로 건너뛰기

시트 표기의 선언과 강제

「이름 표기 규약」으로 돌아가기


3. Naming — 시트 표기 규약의 선언과 강제

레시피 최상위에 섹션 하나를 신설합니다. 이름은 모델 전체의 자산이므로 소스별이 아니라 전역입니다.

"Naming": {
"Entity": "pascal", // 테이블·enum·상수셋 이름
"Field": "camel", // 필드 이름
"Label": "pascal", // enum 라벨
"Constant": "pascal", // 상수 이름
"OnViolation": "error", // error | warn
"OnSpellingConflict": "warn", // error | warn | ignore (§4.1)
"OnConsecutiveUnderscores": "warn", // error | warn | ignore (§4.2)
"Exempt": [] // 검사에서 제외할 원문 표기 (§3.3)
}
검사 대상기본값
Entity테이블·enum·상수셋 이름없음 — 선언한 종류만 검사합니다
Field필드 이름. 중첩 이름(Slot1.Id)은 마디별로, * 접두 같은 마커는 벗긴 뒤 검사합니다없음
Labelenum 라벨없음
Constant상수 이름없음
OnViolation규약 위반의 심각도error
OnSpellingConflict표기 충돌의 심각도 (§4.1)warn
OnConsecutiveUnderscores연속 밑줄의 심각도 (§4.2)warn
Exempt모든 이름 검사에서 제외할 원문 표기 목록 — 기존 모델의 도입 경로 (§3.3)[]

3.1 값 어휘와 판정

판정은 왕복 동일성입니다 — 선언한 표기로 변환한 결과가 원문과 같아야 합니다.

왕복 기준통과 예
pascalToPascalCase()MaxHitPoints
camelToCamelCase()maxHitPoints
snakeToSnakeCase()max_hit_points
upper-snakeToUpperSnakeCase()MAX_HIT_POINTS

camel 선언에서 Maxhitpoints(첫 글자 대문자)와 max_hit(밑줄)은 위반으로 보고되고, maxhitpoints는 통과합니다 — 한 단어짜리 이름과 경계가 소실된 이름을 표기만으로 구분할 수 없기 때문입니다. 그 사각을 §4.1이 보완합니다.

왕복 판정에는 사각이 하나 더 있습니다. ToSnakeCase()는 이름 안쪽의 밑줄을 그대로 보존하므로, snake 선언에서 a__b가 왕복을 통과합니다. 그 사각은 §4.2가 보완합니다.

upper-snakeNameCase에 없던 표기이므로 StringExtensionsToUpperSnakeCase()를 추가하였습니다. 생성기 8개가 손으로 조립해 쓰던 ToSnakeCase().ToUpperInvariant()를 그대로 쓰지 않은 이유가 있습니다 — 그 조합은 결과를 파일에 그대로 적을 때만 맞고, 판정에는 쓸 수 없습니다. 판정은 「표기한 결과가 원문과 같은가」이므로 표기와 판정이 한 함수를 거쳐야 하고, 두 경로로 나뉘면 두문자어에서 어긋납니다.

그리고 생성기 쪽 호출 10곳도 이 함수로 옮겼습니다. 판정용 함수를 새로 만들고 기존 체이닝을 남겨 두면 같은 답을 내는 두 경로가 생기고, 단어 분리 규칙을 나중에 고칠 때 한쪽만 움직입니다 — 그 규칙이 이미 한 번 회귀하였던 자리이므로(s_f_x_category_type) 가정할 일이 아닙니다. 옮기기 전에 두 경로의 동등성을 NameCaseTests에 고정하였고, 골든은 한 바이트도 바뀌지 않았습니다.

값의 문자열 파싱은 기존 OnXxx 옵션들과 같은 4단 패턴을 따릅니다 — trim → 빈 값은 기본값 → 소문자화 후 switch → 섹션 경로와 허용값 목록을 담은 예외.

3.2 검사 시점과 보고

쿠킹이 끝난 모델을 한 번 순회합니다 — ModelCooker.Naming.csValidateNaming이고, ValidateModel이 나머지 검사보다 앞에 부릅니다. 레이아웃 파서 3종은 수정하지 않았습니다. 파서가 이미 모든 이름을 RawName(원문)과 Name(정규화) 쌍으로 모델에 보존하고, 선언 위치도 Field.NameLocation · Table.Location · Label.Location · Constant.Location으로 남아 있으므로, 검사에 필요한 재료가 모델에 전부 있습니다.

보고는 Diagnostics 경로입니다 — 첫 위반에서 중단하지 않고 전부 모아 셀 위치와 함께 일괄 보고하며, error는 변환을 중단시키고 warnTreatWarningsAsErrors로 승격됩니다.

진단은 판정으로 끝나지 않고 고쳐 쓸 이름으로 끝납니다. 세 검사 모두 그렇습니다.

검사마지막 문장
왕복 (§3)Write it as \maxHitPoints`.`
표기 충돌 (§4.1)Settle on \maxHitPoints` and rewrite the other 3 places.`
연속 밑줄 (§4.2)Write it as \a_b`.`

이름이 틀렸다고만 적으면 읽는 사람이 표기 규칙을 다시 해석해 손으로 적용해야 합니다. 그것은 도구가 보고할 것이 있다고 판단하기 위해 이미 한 계산이므로, 결과를 버리고 사람에게 다시 시키는 것이 됩니다. 충돌 진단은 고칠 자리의 개수까지 적습니다 — 읽는 사람이 다음에 확인하는 것이고, 표기별 집계가 이미 손에 있기 때문입니다.

충돌 진단이 가리키는 셀은 고쳐야 할 표기의 첫 자리입니다. 권장 표기의 셀을 가리키면 편집기가 고칠 필요 없는 자리로 먼저 이동하고, 그 셀이 지목된 것처럼 읽힙니다.

3.3 기존 모델의 도입 경로 — Exempt 목록

규약을 뒤늦게 선언하는 모델은 위반이 수백 건일 수 있습니다 — §1의 실측에서는 규약 밖 표기의 컬럼이 326종이었습니다. 전량을 즉시 오류로 처리하면 도입 자체가 성립하지 않고, 전량을 경고로 낮추면 새 위반이 기존 위반에 묻힙니다. 그래서 둘을 가르는 장치를 둡니다.

Exempt는 원문 표기의 목록이고, 여기 오른 이름은 §3의 왕복 검사와 §4의 방어선 모두에서 제외됩니다. 도입 절차는 다음과 같습니다.

  1. 규약을 선언하고 한 번 변환하여 위반 전체를 얻습니다.
  2. 기존 위반을 Exempt에 옮기고 OnViolation: "error"로 둡니다 — 이 시점부터 신규 유입이 차단됩니다. 새 이름은 규약을 지켜야만 들어옵니다.
  3. 기존 위반은 계열 단위로 개명하고, 개명이 끝난 이름을 목록에서 지웁니다.

목록은 줄어드는 방향으로만 관리합니다 — 이름을 새로 올리는 것은 새 위반의 도입입니다. 그리고 개명은 표기 수정이 아니라 생성 코드의 이름 변경입니다. 시트의 이름을 고치면 그 이름에서 파생되는 멤버·타입·파일 이름이 함께 바뀌므로, 한 계열을 한 번에 옮기고 같은 주기에 소비 코드를 함께 수정합니다. 표기를 섞은 채 일부만 바꾸는 것이 가장 위험합니다.

4. 선언 없이 동작하는 방어선

규약 선언과 무관하게 항상 수행되는 검사 2종입니다. 기본 심각도는 warn이고 — 기존 레시피의 변환 결과는 불변이며, 관용 정책이 반드시 로그를 남긴다는 기존 원칙과 같은 자리입니다 — 각각 OnSpellingConflict · OnConsecutiveUnderscoreserror 승격 또는 ignore 해제를 선택합니다.

4.1 표기 충돌 검사

§1의 세 표기는 모두 유효한 camelCase이므로 규약 선언으로는 검출되지 않습니다. 이것을 검출하는 것이 이 검사이고, 사용자 문제의 직접 해법입니다.

  • 접기 키: 이름을 소문자화하고 _·-를 제거한 문자열. 세 표기는 모두 maxhitpoints로 접힙니다.
  • 그룹화: 같은 종류(엔티티·필드·라벨·상수)의 이름을 모델 전체 범위에서 접기 키로 묶습니다. 필드는 테이블 경계를 넘어 묶습니다 — 같은 개념이 여러 테이블에 나타나는 것이 문제의 사례이기 때문입니다.
  • 보고 조건: 한 그룹 안에 원문 표기(마커를 벗긴 RawName)가 2가지 이상일 때. 그룹의 모든 표기와 위치를 진단 1건으로 묶고, 권장 표기를 함께 제시합니다.
  • 권장 표기의 선택: 해당 종류에 규약이 선언되어 있으면 규약을 지키는 표기를, 없으면 다수 표기를 제시합니다. 다수가 항상 옳지는 않기 때문입니다 — 어긋난 표기는 시트를 베껴 쓰는 과정에서 맞는 표기와 똑같이 번지므로, 세 테이블이 일치한다는 것은 근거가 아닙니다. 규약을 지키는 표기가 여럿이면 그중 다수를, 동수이면 먼저 나온 것을 제시합니다 — 실행마다 답이 달라지지 않게 하기 위해서입니다.

생성물이 갈라지는지에 따라 무게가 다릅니다. myflag · my_flag · myFlag에서 my_flagmyFlag는 같은 MyFlag로 정규화되어 생성물이 같지만, myflagMyflag로 정규화되어 생성 코드가 나뉩니다. 앞의 것은 시트 표기의 불일치이고 뒤의 것은 소비 코드가 비용을 치르는 분기이므로, OnSpellingConflict가 정하는 것은 뒤의 것의 무게이고 앞의 것은 한 단계 낮게 보고됩니다.

설정생성물이 갈라지는 충돌갈라지지 않는 충돌
warn (기본)warninginfo
errorerrorwarning
ignore보고 없음보고 없음

갈라지지 않는 충돌도 보고하는 이유는 방치되면 갈라지는 표기로 이어지기 때문이고, 기본을 info로 두는 이유는 그것이 승격되지 않는 유일한 단계이기 때문입니다 — TreatWarningsAsErrors를 켠 빌드가 생성물에 아무 영향이 없는 시트 표기 차이로 멈추는 것은 과도하고, 생성 코드가 실제로 갈라지는 충돌로 멈추는 것은 과도하지 않습니다.

접기 키까지 같은 서로 다른 개념 — 오검 — 은 철자가 전부 같아야 하므로 드물고, 그 경우 표기도 1가지라 보고되지 않습니다.

4.2 연속 밑줄 검사

a_b · a__b · a___b는 시트에서는 세 이름처럼 보이지만 도구에게는 같은 이름입니다. 정규화는 이름 안쪽의 밑줄을 단어 경계로만 쓰고 개수를 소거하므로 셋 모두 같은 이름으로 정규화됩니다 — doc/sheets.md에 기록되어 있는 붕괴입니다. 즉 밑줄 개수의 차이는 생성물 어디에도 반영되지 않는 구분입니다. 의도라면 전달되지 않고, 오타라면 같은 범위에서는 중복 오류로만(어느 표기가 원인인지 모른 채), 범위가 다르면 아무것도 보고되지 않습니다.

왕복 판정(§3.1)의 사각이기도 합니다 — ToSnakeCase()가 이름 안쪽의 밑줄을 그대로 보존하므로 snake 선언에서 a__b가 왕복을 통과합니다. 그래서 규약 선언과 무관한 전용 검사로 둡니다.

  • 이름 안쪽에 연속 밑줄(__)이 있으면 위치와 함께 보고합니다.
  • 선행·후행 밑줄은 대상이 아닙니다. _reserved는 정규화가 의도적으로 보존하는 표기이고(StringExtensions.cs의 「a name somebody chose」 주석), __name_name은 서로 다른 이름으로 생성물에 반영되므로 모호하지 않습니다.
  • 차단하려는 팀은 OnConsecutiveUnderscoreserror로 둡니다. 기본이 warn인 것은 기존 레시피 불변 원칙 때문이지 권장이어서가 아닙니다.