업무 시스템의 코드 설계 ── 상품 코드・고객 코드 정하는 방법과 체크 디지트

· · 코드 설계, 체크 디지트, 업무 시스템, 데이터베이스, C#, .NET, Excel, 판단표, 설계, Windows 개발

‘새 시스템으로 이관하니 상품 코드 체계를 정해 주세요’──업무 시스템 구축이나 Excel 대장 이관에서는 반드시 이 숙제가 나옵니다. 그리고 그 자리의 즉흥적인 발상으로 정한 코드는 평균적으로 시스템 본체보다 오래 살아남습니다. 시스템은 10년이면 교체되어도, 거래처에 배포한 고객 번호와 과거 전표에 인쇄된 상품 코드는 계속 남기 때문입니다.

한편 코드 설계에는 선인들의 지혜가 놀라울 만큼 축적되어 있습니다. JAN 코드나 신용카드 번호, 마이넘버 같은 ‘대규모로 운용되고 있는 번호’는 입력 실수 검출 방법부터 자릿수 배정까지, 설계 판단의 이유까지 포함해 공개되어 있습니다. 이 글에서는 이를 바탕으로 업무 시스템에서 상품 코드・고객 코드・전표 번호 등을 정할 때의 규칙과 사용할 수 있는 구조(체크 디지트)를 정리합니다.

1. 먼저 결론

  • 코드는 식별에만 전념하고, 의미는 속성으로 DB에 갖게 합니다. 부서・분류・연도를 코드 자릿수에 담는 ‘유의미 코드’는 조직 개편・분류 변경으로 반드시 파탄 납니다. 마이넘버는 의미를 갖지 않는 번호로 설계되어 있습니다.1
  • 사람이 입력・전기(轉記)・구두 전달하는 코드에는 체크 디지트를 붙입니다. 입력 실수의 대부분은 ‘한 글자 오타’와 ‘이웃한 자리의 뒤바뀜’이라는 것이 실측으로 알려져 있으며2, 체크 디지트는 바로 이 두 가지를 정조준해 검출합니다.
  • 문자 종류는 운용으로 정합니다. 전화・팩스・손글씨가 관여한다면 숫자만. 자릿수를 절약하고 싶다면 헷갈리기 쉬운 문자(I・L・O 등)를 제외한 영숫자(Crockford Base32 등3).
  • 자릿수는 ‘장래 건수의 10배+1자리’를 기준으로 확보하고, 앞자리 0을 사용한다면 전체 시스템에서 문자열로 다룹니다. Excel은 숫자로 해석하는 순간 앞자리 0을 없애고, 16자리 이후를 0으로 반올림합니다.4
  • 한 번 사용한 코드는 결번이 되어도 재사용하지 않습니다. 과거 전표・로그・거래처 시스템에 남은 번호가 다른 대상을 가리키게 됩니다.
  • 상품에 바코드를 붙여 유통시킨다면 자체 코드가 아니라 JAN 코드(GS1 표준)에 올라탑니다.5

2. 코드에 의미를 담을 것인가 ── 처음이자 가장 큰 갈림길

코드 설계에서 가장 먼저 정해야 할 것은 자릿수도 문자 종류도 아니라 ‘코드에 의미를 담을 것인가’입니다.

흔한 설계는 이렇습니다. ‘상품 코드는 8자리. 앞 2자리가 부서, 다음 3자리가 분류, 나머지 3자리가 일련번호.’ 정한 순간에는 정연해 보이지만, 몇 년만 지나면 이렇게 됩니다.

  • 조직 개편으로 부서가 통합되어 옛 부서 코드를 가진 상품이 붕 뜬다. 다시 부여하면 과거 전표와 대조할 수 없게 되고, 방치하면 ‘코드의 부서와 실제 부서가 다른’ 예외가 마스터에 증식한다.
  • 분류를 넘나드는 상품(식품이면서 잡화이기도 한)이 나타나, 어느 쪽 코드를 붙일지에 대한 사내 규칙이 필요해진다.
  • 특정 분류만 상품이 늘어 일련번호 3자리가 고갈되어, ‘이 분류만 예외적으로 다른 부서의 빈 번호를 빌린다’는 운용이 시작된다.

원인은 명확합니다. 조직이나 분류는 변하는 것인데도, 변하지 않는다는 전제로 코드에 새겨 넣었기 때문입니다. 그러므로 원칙은 그 반대가 됩니다. 코드는 대상을 유일하게 가리키기 위한 기호로만 하고, 부서・분류 등의 속성은 데이터베이스의 열에 갖게 한다. 속성이라면 아무리 바뀌어도 UPDATE 한 번이면 되고, 코드는 무상합니다.

국가의 번호 제도도 이 원칙으로 만들어져 있습니다. 마이넘버는 주민표 코드를 ‘작위가 가해지지 않는 방법’으로 변환하여 생성되는, 의미를 갖지 않는 11자리+검사용 숫자 1자리입니다.1 의미를 갖게 하지 않는 것은 번호로부터 개인의 속성을 추측당하지 않기 위해서이기도 하지만, 속성 변경(이사나 개명)으로 번호가 바뀌지 않게 하기 위해서이기도 합니다.

그렇다고 해도 실무에서는 ‘앞부분만 봐도 고객인지 매입처인지는 알고 싶다’는 요망도 뿌리 깊기 때문에, 판단표로 정리하면 다음과 같습니다.

방식 강점 파탄 지점 적합한 상황
완전 유의미 코드 02-104-317(부서-분류-일련번호) 읽으면 속성을 알 수 있다 조직 개편・분류 변경・부분적 자릿수 고갈로 재부여 대상이 증감하지 않고 분류가 제도상 고정된 경우에 한함
무의미 일련번호 10000317 무엇이 바뀌어도 깨지지 않는다. 채번이 단순 사람이 코드에서 아무것도 읽어낼 수 없다 속성을 화면・전표에 항상 병기할 수 있는 시스템을 전제로 한 운용
하이브리드 C-0317-5(종별 1글자+일련번호+검사 숫자) 종별 혼동만은 막을 수 있다. 파탄 요소가 최소 종별 정의 변경(드묾) 망설여진다면 이것. 종별은 ‘고객/매입처/상품’ 정도의 거칠기로 그친다

권장은 표의 아래 두 가지입니다. 담는 의미는 늘릴수록 파탄의 씨앗이 늘어난다고 생각해 주세요.

3. 문자 종류와 자릿수 ── 정보 효율과 가독성은 트레이드오프

다음으로 정할 것은 문자 종류와 자릿수입니다. 이것은 순수하게 산수의 문제로, 한 자리에 사용할 수 있는 문자 종류가 많을수록 같은 자릿수로 더 많은 대상을 표현할 수 있습니다.

문자 종류 6자리로 표현할 수 있는 수 구두 전달・손글씨 내성
숫자만(10종) 100만 ◎ 전화・팩스・손글씨에 강함
영대문자+숫자에서 헷갈리기 쉬운 문자를 뺀 32종(Crockford Base323) 약 10.7억 ○ 오독 대책이 되어 있지만 구두 전달은 숫자보다 못함
영대문자+숫자(36종) 약 21.8억 0O, 1I의 오독이 반드시 발생

판단 기준은 다음 두 가지입니다.

  • 그 코드는 전화로 구두 전달되는가, 손글씨・팩스로 전달되는가. 하나라도 해당한다면 숫자만 쓰는 것을 권장합니다. 영문자가 섞이는 순간 ‘아이? 원?’ 하는 확인 비용과 오독 위험이 발생합니다.
  • 숫자만으로는 자릿수가 지나치게 길어지는가. 대상이 수천만 건을 넘는 등 자릿수를 압축하고 싶은 경우에 비로소 영숫자를 검토합니다. 그 경우에도 순수한 36종이 아니라, ILO(그리고 우연한 비속어를 피하기 위한 U)를 제외한 Crockford Base32 같은 설계된 알파벳을 사용해야 합니다. 이 사양은 오독 대책이 철저해서, 디코딩 시 소문자도 받아들이고 il1, o0으로 해석합니다.3

자릿수는 ‘현재의 건수’가 아니라 ‘시스템과 전표가 계속 살아 있을 20~30년 동안 도달할 수 있는 건수’로부터 역산하고, 거기에 여유를 1자리 더합니다. 도달할 수 있는 건수의 10배를 표현할 수 있는 자릿수를 확보한다고 바꿔 말해도 같습니다(10만 건이면 6자리). 체크 디지트를 붙인다면 그 1자리는 이 여유와는 별개로 셉니다(4장). 1장과 9장에서 ‘장래 건수의 10배+1자리’라고 쓴 것은 이 ‘본체=10배분’과 ‘검사용 숫자 1자리’의 합계라는 뜻입니다. 10만 건을 예상한다면 본체 6자리+검사용 숫자 1자리로 7자리, 라는 계산법이 됩니다.

자릿수 초과의 무서움은 8장에서 다루지만, 우편번호가 5자리에서 7자리로 바뀌었을 때(1998년)와 같은 전면 개수를 자사에서 일으키지 않기 위한 비용은 겨우 1자리입니다.

또 하나, 은근히 효과가 큰 것이 구분입니다. 신용카드 번호가 4자리씩 인쇄되듯이, 사람은 긴 숫자열을 3~4자리 덩어리로만 정확하게 다룰 수 있습니다. 8자리 이상의 코드를 전표나 화면에 표시한다면 1234-5678처럼 구분해서 표시합니다. 다만 구분 기호는 데이터에 포함시키지 않고 표시할 때 붙이는 것이 원칙입니다(6장).

4. 체크 디지트 ── 입력 실수를 코드 스스로 검출하게 한다

4.1 입력 실수의 정체는 두 종류

체크 디지트(검사용 숫자)는 코드의 끝(또는 앞)에 붙이는 1자리로, 다른 자리로부터 계산해 이끌어낼 수 있도록 해 둠으로써 입력된 코드의 오류를 그 자리에서 검출하는 구조입니다. 마이넘버의 검사용 숫자도 정령(政令)에 ‘전자계산기에 입력할 때 오류가 없음을 확인하는 것을 목적으로 하여’라고 목적이 명기되어 있습니다.1

어떤 계산식이 좋은지는 사람이 어떤 실수를 하는가에 따라 결정됩니다. 네덜란드의 우편대체 시스템 등의 오기 데이터를 분석한 Verhoeff의 고전적 조사에 따르면, 오류의 60~95%는 한 자리만의 오타(single error)로, 이것이 압도적 다수입니다. 그다음으로 두 자리 오류가 10~20%를 차지하고, 그 대부분은 이웃한 두 자리, 특히 abba형 뒤바뀜(전위, transposition)입니다. 그 외의 유형(aabb형이나 한 자리 건너뛴 뒤바뀜 등)은 각각 전체의 0.5~1.5% 정도에 불과합니다.2

즉 체크 디지트 방식의 성능은 사실상 ‘한 자리 오류를 얼마나 검출할 수 있는가‘와 ‘이웃한 자리 뒤바뀜을 얼마나 검출할 수 있는가’ 두 가지로 평가할 수 있습니다.

4.2 실제로 사용되고 있는 방식과 검출 능력(판단표)

표를 읽기 전에 방식 이름을 읽는 법만 정해 둡니다. ‘모듈러스 〇〇’는 ‘〇〇으로 나눈 나머지를 사용하는 방식’이라는 뜻입니다(모듈러스 11이면 11로 나눈 나머지). ‘가중치’는 각 자리에 곱하는 배율로, ‘가중치 3-1’이면 오른쪽 끝부터 3배・1배・3배・1배…로 번갈아 곱합니다. 어느 방식이든 하는 일은 ‘각 자리에 정해진 배율을 곱해 합산하고, 정해진 수로 나눈 나머지로부터 검사용 1자리를 만드는’ 것뿐입니다.

방식 사용되는 곳 한 자리 오류 이웃한 자리 뒤바뀜 특징
모듈러스 10 가중치 3-1 JAN 코드 등 GS1 표준, ISBN-136 전부 검출 차가 5인 조합(0550, 1661 등)을 놓침 바코드 운용이라면 사실상 이것 하나로 결정
Luhn(모듈러스 10) 신용카드 번호7 전부 검출 0990의 한 쌍만 놓침 구현이 가장 간단. 자체 코드의 기본 후보
모듈러스 11(가중) 마이넘버8, 구 ISBN 거의 전부 검출 거의 전부 검출 검출력은 높지만 ‘나머지 처리’ 문제가 있음(후술)
모듈러스 9(가중) 법인번호9 09를 놓침 09의 조합을 놓침 9로 나누는 특성상 0과 9를 구별할 수 없음
Damm / Verhoeff 학술적 방식 전부 검출 전부 검출 수표(數表) 조회(Damm10)나 군 연산(Verhoeff2)으로 구현

몇 가지 보충합니다.

  • JAN 방식(모듈러스 10 가중치 3-1)은 오른쪽 끝 자리부터 순서대로 3배・1배를 번갈아 곱해 합산하고, ‘10 − (합계를 10으로 나눈 나머지)’의 1의 자리를 체크 디지트로 합니다.6 뒤바뀜으로 합계가 변하지 않는 것은 3배와 1배의 차×(자릿수 차)가 10의 배수가 되는 경우, 즉 두 자리의 차가 정확히 5일 때로, 이 패턴만은 검출할 수 없습니다.
  • Luhn은 1954년에 IBM의 H. P. Luhn이 특허 출원한 방식으로7, 한 자리 건너 자리를 2배 하고 9를 넘으면 9를 빼서 합산합니다. 이웃한 자리 뒤바뀜의 놓침은 0990의 단 한 쌍뿐입니다. 구현의 간단함과 검출력의 균형이 좋아서, 자체 코드에 새로 도입한다면 우선 이것으로 문제없습니다.
  • 마이넘버의 모듈러스 11은 11이 소수인 덕분에 이론상의 검출력은 높지만, 나머지가 0 또는 1일 때 체크 디지트를 일률적으로 0으로 하는 접기 처리가 있어서8, 이 두 부류 사이를 이동하는 오류만은 검출할 수 없습니다. 구 ISBN(ISBN-10)은 같은 모듈러스 11로 이 문제를 ‘나머지 10을 X로 쓴다’는 방식으로 해결했지만, 이번에는 ‘숫자여야 할 자리에 X가 나타난다’는 운용상의 번거로움을 안게 되었습니다. 모듈러스 11 계열을 채택한다면 이 ‘11가지 나머지를 10가지 숫자에 어떻게 밀어 넣을 것인가’라는 문제가 반드시 따라온다는 것을 알아 두어야 합니다.
  • 법인번호의 모듈러스 9는 앞자리에 체크 디지트를 두는 드문 설계이지만11, 9로 나눈 나머지를 사용하기 때문에 09가 합동이 되어, 이 두 개의 오타・뒤바뀜은 그냥 통과시켜 버립니다.
  • Damm이나 Verhoeff 방식은 한 자리 오류와 이웃한 자리 뒤바뀜을 모두 완전히 검출합니다. 모든 한 자리 오류와 모든 이웃한 자리 뒤바뀜을 검출하는 10진 코드는 Verhoeff가 처음으로 구성했고2, Damm은 준군(quasigroup)의 연산표를 한 번 조회하는 것뿐인 더 간단한 구성을 제시했습니다.10 검출력을 최우선한다면 이들 방식이지만, 업무 시스템의 입력 실수 대책으로는 Luhn이나 JAN 방식으로 실용상 충분합니다.

이 표가 ‘한 자리 오류’와 ‘이웃한 자리 뒤바뀜’ 두 열로 방식을 비교하고 있는 것은, 4.1에서 본 Verhoeff의 조사──네덜란드의 우편대체 시스템 등, 사람이 실제로 전기(轉記)・입력했던 번호의 오기 기록을 유형별로 집계한 것──에서 이 두 유형이 오류의 대부분을 차지하고 있었기 때문입니다.2 반대로 말하면, 자사의 코드에서 발생하는 오류의 경향이 명백히 다르다면(예를 들어 손글씨의 17을 잘못 읽는 경우가 대부분이라면) 방식 비교보다 문자 종류나 전표 서식을 재검토하는 편이 효과적입니다.

4.3 체크 디지트를 붙여야 할 코드, 필요 없는 코드

판단 기준은 ‘사람의 손과 눈을 거치는가‘입니다. 종이 주문서에서 입력하는 상품 코드, 전화로 전달하는 회원 번호, 현장에서 손으로 쓰는 전표 번호에는 붙일 가치가 있습니다. 반대로 시스템 간 연계에서만 흐르는 내부 ID, 화면에서 선택으로만 입력되는 코드에는 필요하지 않습니다. 실수의 발생원(사람)이 없다면 검출할 의미도 없기 때문입니다.

5. 구현 예 ── C#과 Excel・VBA

주요 방식은 어느 것이든 몇 줄에서 십수 줄로 구현할 수 있습니다. 코드는 항상 문자열로 받는다는 점에만 주의해 주세요(이유는 6장).

C#

먼저 JAN 방식(GTIN-13). 12자리 본체로부터 체크 디지트를 계산합니다.6

public static int Gtin13CheckDigit(string body12)
{
    if (body12.Length != 12 || !body12.All(char.IsAsciiDigit))
        throw new ArgumentException("12자리 숫자를 지정하세요", nameof(body12));

    int sum = 0;
    for (int i = 0; i < 12; i++)
    {
        int digit = body12[11 - i] - '0';        // 오른쪽 끝부터 센다
        sum += (i % 2 == 0) ? digit * 3 : digit; // 오른쪽 끝이 ×3, 이후 교대로
    }
    return (10 - sum % 10) % 10;
}

다음으로 Luhn. 자사의 고객 코드・회원 번호에 붙인다면 이 두 가지로 완결됩니다.

public static int LuhnCheckDigit(string body)
{
    int sum = 0;
    for (int i = 0; i < body.Length; i++)
    {
        int digit = body[body.Length - 1 - i] - '0';
        if (i % 2 == 0)              // 체크 디지트 옆에서부터 한 자리씩 걸러 2배
        {
            digit *= 2;
            if (digit > 9) digit -= 9;
        }
        sum += digit;
    }
    return (10 - sum % 10) % 10;
}

public static bool IsValidLuhn(string code) =>
    code.Length >= 2 && code.All(char.IsAsciiDigit) &&
    LuhnCheckDigit(code[..^1]) == code[^1] - '0';

이 코드가 무엇을 하고 있는지는 한 번쯤 손으로 따라가 보면 머리에 남습니다. 고객 코드의 본체를 1000317로 하고 체크 디지트를 계산해 보겠습니다. 본체의 오른쪽 끝에서 세어 1번째・3번째・5번째… 자리를 2배 하고, 2배 한 결과가 9를 넘으면 9를 뺀다, 그것뿐입니다.

본체의 자리(오른쪽부터) 1 2 3 4 5 6 7
숫자 7 1 3 0 0 0 1
2배 여부      
계산 후 14→5 1 6 0 0 0 2

합계는 5+1+6+0+0+0+2 = 14. 체크 디지트는 ‘10 −(합계를 10으로 나눈 나머지)’의 1의 자리이므로, 10 − 4 = 6. 완성된 코드는 10003176입니다. 위의 LuhnCheckDigit("1000317")이 반환하는 값과 일치합니다.

완성된 코드를 검증할 때는 체크 디지트까지 포함해 오른쪽 끝부터 세어, 오른쪽에서 2번째・4번째…를 2배 하여 합산합니다. 6+5+1+6+0+0+0+2 = 20으로, 10으로 나누어떨어지므로 올바른 코드입니다. 시험 삼아 흔한 오타를 넣어 보겠습니다.

  • 이웃한 자리를 바꿔 10003716으로 입력한 경우: 6+2+7+6+0+0+0+2 = 23. 10으로 나누어떨어지지 않으므로 검출할 수 있습니다.
  • 한 자리만 잘못 입력해 10008176으로 입력한 경우: 6+5+1+7+0+0+0+2 = 21. 이것도 검출할 수 있습니다.

이 ‘합계가 10의 배수가 되는가’만이 판정 기준이므로, 계산기만 있는 현장에서도 검산할 수 있습니다. 4장에서 본 대로, Luhn이 놓치는 것은 0990의 뒤바뀜뿐입니다.

거래처 마스터의 입력 확인에 사용할 수 있는 법인번호 검증도 실어 둡니다. 성령(省令)의 산식9을 그대로 옮긴 것입니다(앞 1자리가 검사용 숫자, 이어지는 12자리가 기초번호).

public static bool IsValidCorporateNumber(string code)
{
    if (code.Length != 13 || !code.All(char.IsAsciiDigit)) return false;

    int sum = 0;
    for (int n = 1; n <= 12; n++)
    {
        int p = code[13 - n] - '0';          // 기초번호의 최하위 자리를 n=1자리로 한다
        sum += p * (n % 2 == 0 ? 2 : 1);     // 홀수 자리×1, 짝수 자리×2
    }
    return 9 - sum % 9 == code[0] - '0';
}

국세청 자료에 있는 예(회사법인등번호 700110005901 → 법인번호 8700110005901)로 검산하면, 홀수 자리의 합 11+짝수 자리의 합 13×2=37, 37을 9로 나눈 나머지 1, 9−1=8로 일치합니다.11

Excel 수식과 VBA

이관 전의 기존 마스터를 Excel로 실사하는 상황이나, 업무 부서에서 배포된 목록을 정보시스템 부서가 점검하는 상황에서는, 개발 환경을 열지 않고 그 자리에서 검증하고 싶을 때가 있습니다. Luhn 검증이라면 수식 하나로도 작성할 수 있습니다(A2에 코드가 들어 있다는 전제입니다. Microsoft 365 또는 Excel 2021 이후의 LETSEQUENCE를 사용합니다).

=LET(s,A2&"", n,LEN(s), i,SEQUENCE(n),
     d,MID(s,i,1)*1,
     x,IF(MOD(n-i,2)=1, d*2, d),
     y,IF(x>9, x-9, x),
     MOD(SUM(y),10)=0)

n-i가 홀수인 자리, 즉 오른쪽에서 2번째・4번째…를 2배 하고 9를 넘으면 9를 빼서 합산하여, 10으로 나누어떨어지는지를 보고 있습니다. 앞서 본 손계산과 같은 절차입니다. 코드가 숫자로 입력된 셀에서는 앞자리 0이 사라져 있다는 점에 주의해 주세요(6장). 열을 문자열로 바꾼 뒤 검증합니다.

오래된 Excel까지 포함해 배포하고 싶은 경우나 여러 방식을 구분해 사용하고 싶은 경우에는, VBA 함수로 만들어 두면 =IsValidLuhn(A2)처럼 워크시트에서 호출할 수 있습니다.

' Luhn 방식 검증. 앞자리 0이 사라지지 않도록 반드시 문자열로 전달할 것.
Public Function IsValidLuhn(ByVal code As String) As Boolean
    Dim i As Long, n As Long, d As Long, total As Long
    Dim c As String

    n = Len(code)
    If n < 2 Then Exit Function          ' 기본값인 False를 반환

    For i = 1 To n
        c = Mid$(code, n - i + 1, 1)     ' 오른쪽 끝부터 센다
        If c < "0" Or c > "9" Then Exit Function
        d = CLng(c)
        If i Mod 2 = 0 Then              ' 오른쪽에서 2번째・4번째…를 2배
            d = d * 2
            If d > 9 Then d = d - 9
        End If
        total = total + d
    Next i

    IsValidLuhn = (total Mod 10 = 0)
End Function

같은 형태로 JAN 방식(오른쪽 끝부터 3배・1배)이나 법인번호(홀수 자리×1・짝수 자리×2)도 작성할 수 있습니다. 배율과 나누는 수가 다를 뿐입니다.

입력 화면에서의 사용법에도 한 가지 궁리할 점이 있습니다. 체크 디지트가 일치하지 않을 때 ‘코드가 올바르지 않습니다’라고만 표시하는 것이 아니라, 마스터를 조회해 명칭을 재확인 표시하는(‘10003176: 주식회사 〇〇 가 맞습니까?’) 데까지 하면, 체크 디지트를 빠져나간 오입력(실재하는 다른 코드를 잘못 입력한 경우)도 사람의 눈으로 포착할 수 있습니다.

6. 현장에서 반드시 밟는 함정

코드 체계 자체가 좋아도 구현과 운용에서 망가지는 정형적인 패턴이 있습니다.

  • Excel의 앞자리 0 소실과 15자리 반올림. Excel은 셀의 내용을 숫자로 해석하면 앞자리 0을 삭제하고, 또한 숫자의 유효 정밀도가 15자리이기 때문에 16자리 이후를 0으로 바꿔 버립니다.4 고객 코드 00123123이 되고, 신용카드 번호급의 긴 번호는 끝자리가 0으로 둔갑한다는 것입니다. CSV 연계가 있는 시스템의 코드는 모든 경로에서 문자열로 취급(Power Query에서의 텍스트 형식 지정, 가져오기 측에서의 열 형식 지정)하는 것을 처음부터 운용 절차에 포함해 주세요.
  • 데이터베이스에 숫자형으로 저장해 버림. 앞자리 0이 사라지는 것은 Excel과 같고, ‘코드 범위로 좁히려는’ 의도의 BETWEEN이 자릿수가 다른 코드를 끌어들이게 됩니다. 코드는 계산 대상이 아니므로 자릿수 고정 문자열형이 원칙입니다. 정렬 순서도 문자열로 설계합니다(자릿수를 고정해 두면 문자열 정렬=숫자 정렬이 됩니다).
  • 하이픈을 데이터에 포함시켜 버림. 1234-567812345678이 별개의 레코드로 섞이기 시작하면 끝입니다. 저장은 순수 코드, 구분은 표시할 때 부여. 입력 시에는 하이픈・공백을 제거한 뒤 검증합니다.
  • 대소문자의 흔들림. 영문자를 사용한다면 저장 전에 대문자로 정규화하고, 정렬 규칙(collation)에 의존하지 않고 직접 통일합니다.
  • 결번의 재사용. ‘고객 코드 1000317은 해지했으니 신규 고객에게 돌리자’는 금물입니다. 과거의 청구서・로그・거래처 시스템에는 옛 대응 관계가 남아 있어, 감사나 장애 조사에서 다른 사람을 가리키게 됩니다. 코드는 영구 결번이 원칙입니다.

7. 직접 정해서는 안 되는 코드 ── 기존 표준에 올라타는 경우

사내에서 닫히는 코드는 자유롭게 설계할 수 있지만, 상품에 바코드를 인쇄해 사외(소매・EC・물류)로 유통시킨다면 자체 코드가 아니라 JAN 코드(GTIN)를 사용합니다. JAN 코드는 GS1 사업자 코드를 대여받아 설정하는 것으로, 자사에서 마음대로 정할 수 없습니다.5 체크 디지트 계산 방법도 표준으로 정해져 있습니다.6

이때 설계상의 요점은 사내 코드와 JAN 코드를 억지로 일원화하지 않는 것입니다. 동일 상품이라도 입수(入数) 차이로 JAN이 나뉘거나, 규격 변경으로 JAN이 바뀌는 등의 사정이 있기 때문에, 상품 마스터에는 ‘사내 상품 코드(기본 키 상당, 자사에서 채번)’와 ‘JAN 코드(속성, 여러 개 보유 가능)’를 별도의 열로 갖게 하는 것이 정석입니다. 여기서도 ‘코드는 식별, 의미(외부 표준과의 대응)는 속성’이라는 원칙이 그대로 적용됩니다.

8. 코드 체계의 수명과 이관

아무리 정성껏 설계해도 코드 체계는 언젠가 수명을 맞이합니다. 전형적인 것이 자릿수 초과입니다. 일련번호의 상한이 보이기 시작했을 때 선택지는 ‘자릿수를 늘린다’뿐이지만, 자릿수는 마스터의 열 정의뿐만 아니라 전표의 레이아웃, 바코드의 인쇄 폭, 거래처와의 연계 파일 사양, 그리고 거래처 측의 시스템에까지 새겨져 있습니다. 우편번호의 7자리화(1998년)와 같은 이관을 자사와 모든 거래처에서 실시하게 되는 셈입니다.

그렇기 때문에 3장의 ‘여유를 1자리’가 효과를 발휘하지만, 그래도 이관이 필요해졌을 때의 원칙은 다음 세 가지입니다.

  • 신구 대조 마스터를 만들어, 이관 기간 중에는 양쪽 코드로 검색할 수 있게 한다. 거래처로부터의 문의는 옛 코드로 옵니다.
  • 내부 키와 코드를 분리해 두었다면 이관은 표시 계층과 마스터의 문제로 닫힙니다. 데이터베이스의 기본 키에 업무 코드 자체를 사용하고 있으면 모든 테이블의 외래 키가 함께 얽히게 됩니다. 신규 설계에서는 내부 키(자동 채번)와 표시용 코드를 분리해 둘 것을 권장합니다.
  • 스키마 변경은 버전 관리된 마이그레이션으로 배포한다. 코드 열의 자릿수 변경을 모든 고객사・모든 환경에 확실히 전달하는 방법은 DB 스키마 마이그레이션 관련 글에서 다룬 대로입니다.

9. 정리

  • 코드 설계의 제1원칙은 ‘코드는 식별, 의미는 속성’. 부서나 분류를 코드에 새겨 넣으면 조직과 분류의 변화가 그대로 코드의 파탄이 됩니다. 마이넘버가 무의미 번호인 것은 우연이 아닙니다.1
  • 문자 종류와 자릿수는 정보 효율과 가독성의 트레이드오프. 구두 전달・손글씨가 있다면 숫자만, 자릿수를 압축하고 싶다면 오독 대책이 되어 있는 알파벳(Crockford Base323). 자릿수는 장래 건수의 10배+1자리.
  • 입력 실수의 실태는 ‘한 자리 오류가 6~9할, 이웃한 자리 뒤바뀜이 나머지 대부분’.2 사람의 손을 거치는 코드에는 체크 디지트를 붙이고, 방식은 망설여진다면 Luhn, 바코드에 올린다면 GS1 표준6. 모듈러스 11 계열의 ‘나머지 처리’ 문제와 법인번호의 모듈러스 9가 0과 9를 구별하지 못한다는 점은 방식 선정 시 알아 두어야 할 성질입니다.
  • 구현은 문자열 일관이 원칙. Excel의 앞자리 0 소실・15자리 반올림4, 숫자형 저장, 하이픈 혼입, 결번 재사용이 현장의 4대 사고입니다.
  • 외부로 흐르는 상품 코드는 JAN(GS1)에 올리고 사내 코드와는 별도의 열로 갖는다. 자릿수 초과 이관에 대비해 내부 키와 표시용 코드는 분리해 둔다.

코드 체계는 한번 배포하면 나중에 고치는 비용이 자릿수가 다를 만큼 커지는 ‘사실상의 외부 사양’입니다. 신규 시스템의 요건 정의에서 코드 이야기가 나오면, 화면이나 기능보다 먼저 이 글의 체크리스트를 한 번 훑어보시기 바랍니다.

관련 글

관련 상담 영역

합동회사 코무라소프트에서는 업무 시스템 신규 구축・리플레이스 시의 코드 체계・마스터 설계, 기존 코드 체계의 자릿수 초과・중복 조사와 이관 계획, 입력 확인(체크 디지트・마스터 대조) 구현을 다루고 있습니다. 이 글의 체크 디지트 방식 비교처럼 ‘어떤 방식이 어떤 실수를 얼마나 검출할 수 있는가’를 수식으로 평가해 설계 판단으로 연결하는 상담은, 수리 컨설팅 영역으로서도 받고 있습니다.

참고 링크

</content>

  1. 행정절차에서 특정 개인을 식별하기 위한 번호 이용 등에 관한 법률 시행령(헤이세이 26년 정령 제155호) 제6조. 개인번호로 해야 할 번호가, 주민표 코드를 변환하여 얻고 작위가 가해지지 않는 방법으로 생성되는 11자리 번호와, 그 뒤에 붙인 1자리 검사용 숫자(개인번호를 전자계산기에 입력할 때 오류가 없음을 확인하는 것을 목적으로 산출되는 0부터 9까지의 정수)로 구성되는 것에 대해.  2 3 4

  2. J. Verhoeff, Error Detecting Decimal Codes, Mathematical Centre Tracts 29, Mathematisch Centrum, Amsterdam. 실제 시스템의 오기 샘플 분석에 기반한 오류 유형의 빈도(한 자리 오류가 60~95%로 최대 유형, 두 자리 오류가 10~20%로 그 대부분이 이웃한 자리의 뒤바뀜, twin error 등 소수 유형이 각각 0.5~1.5%), 그리고 모든 한 자리 오류와 모든 이웃한 자리 뒤바뀜(같은 책의 transposition은 이웃한 자리의 뒤바뀜을 가리킴)을 검출하는 10진 코드를 저자가 구성한 것에 대해.  2 3 4 5 6

  3. Douglas Crockford, Base 32. 32문자의 알파벳에서 1과 헷갈리기 쉬운 I・L, 0과 헷갈리기 쉬운 O(및 우연한 비속어를 피하기 위한 U)를 제외하고 있는 것, 디코딩 시 대소문자를 받아들이고 i・l을 1, o를 0으로 취급하는 것, mod 37에 의한 체크 기호 구조에 대해.  2 3 4

  4. Microsoft 지원, 앞자리 0과 큰 숫자를 유지하는 방법. Excel의 숫자 유효 정밀도가 최대 15자리이며, 신용카드 번호와 같은 16자리 이상의 숫자에서는 15자리를 넘는 부분이 0으로 바뀌는 것, 앞자리 0이 삭제되는 것, 그리고 열을 텍스트로 취급하는 회피책에 대해.  2 3

  5. GS1 Japan, GS1 사업자 코드・GTIN(JAN 코드). JAN 코드 이용에는 GS1 사업자 코드를 대여받는 등록 절차가 필요한 것에 대해.  2

  6. GS1 Japan, 체크 디지트 계산 방법. GTIN-13(JAN 코드 표준 타입)의 체크 디지트가, 오른쪽 끝 자리부터 순서대로 3배・1배를 번갈아 곱한 합계를 사용해 ‘10에서(합계를 10으로 나눈 나머지)를 뺀’ 값으로 산출되는 것에 대해.  2 3 4 5

  7. H. P. Luhn, US Patent 2,950,048 “Computer for Verifying Numbers” (1954년 출원, 1960년 등록). 원래 번호의 오른쪽 끝에 체크 디지트를 추가하고, 대체 숫자(2배 한 숫자 각 자리의 합)를 이용한 교차 가산으로 번호를 검증하는 방식에 대해.  2

  8. 행정절차에서 특정 개인을 식별하기 위한 번호 이용 등에 관한 법률에 규정하는 개인번호, 개인번호카드, 특정개인정보의 제공 등에 관한 명령(헤이세이 26년 총무성령 제85호) 제5조. 검사용 숫자의 산식(검사용 숫자 이외의 11자리 최하위에서 n번째 자리 숫자 Pn에, 1≦n≦6일 때 n+1, 7≦n≦11일 때 n−5의 가중치 Qn을 곱한 합계를 11로 나누고, 11에서 나머지를 뺀다. 나머지가 1 이하인 경우는 0으로 한다)에 대해.  2

  9. 법인번호 지정 등에 관한 성령(헤이세이 26년 재무성령 제70호) 제2조. 법인번호의 검사용 숫자 산식(기초번호의 최하위에서 n번째 자리 숫자 Pn에, n이 홀수일 때 1, 짝수일 때 2의 가중치 Qn을 곱한 합계를 9로 나누고, 9에서 나머지를 뺀다)에 대해.  2

  10. H. M. Damm, Totally anti-symmetric quasigroups for all orders n≠2,6, Discrete Mathematics, Vol. 307, 2007. 모든 한 자리 오류와 이웃한 자리 뒤바뀜을 검출하는 체크 디지트 방식의 기초가 되는 완전 반대칭 준군이 위수 2・6을 제외한 모든 위수에서 존재하는 것에 대해.  2

  11. 국세청, 체크 디지트 계산. 법인번호가 12자리 기초번호와 그 앞에 붙인 1자리 검사용 숫자로 구성되는 것, 그리고 회사법인등번호 700110005901로부터 체크 디지트 8을 산출하는 계산 예에 대해.  2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

체크 디지트는 어떤 코드에 붙여야 하나요?
사람이 입력・전기(轉記)・구두 전달에 관여하는 코드에 붙입니다. 종이 전표에서 입력하는 상품 코드, 전화로 전달하는 회원 번호, 손으로 쓰는 전표 번호 등이 전형적입니다. 반대로 시스템 간에만 흘러다니는 내부 ID(데이터베이스의 기본 키 등)에는 필요하지 않습니다. 사람의 손을 거치지 않는 코드는 오타가 발생하지 않기 때문이며, 체크 디지트의 목적은 입력 오류 검출에 있습니다. 마이넘버(일본의 개인번호)의 검사용 숫자도, 법령상 '전자계산기에 입력할 때 오류가 없음을 확인하는 것'을 목적으로 명기하고 있습니다.
상품 코드에 부서나 분류의 의미를 담아도 되나요?
원칙은 '코드는 식별에만 전념하고, 의미는 속성으로 데이터베이스에 갖는다'입니다. 부서・분류・연도 등을 코드 자릿수에 담으면 조직 개편이나 분류 변경이 있을 때마다 코드를 다시 부여해야 하고, 과거 전표・거래처에 전달한 번호와의 대조가 깨집니다. 아무래도 사람이 보고 구분하고 싶다면, 종별을 나타내는 접두사 한 글자 정도로 그치고 나머지는 일련번호로 하는 것이 실무적인 타협점입니다.
기존 코드 체계에 체크 디지트를 나중에 추가할 수 있나요?
기술적으로는 가능하지만, 자릿수가 하나 늘어나기 때문에 마스터・모든 전표・거래처와의 데이터 연계・인쇄물 전체에 영향을 미칩니다. 사실상 코드 체계 이관 프로젝트가 되므로, 자릿수 초과 대응 등으로 체계를 쇄신하는 시점에 맞추는 것이 현실적입니다. 그때까지의 임시방편으로는, 입력 화면에서 마스터 존재 확인(입력된 코드가 실재하는지 대조)과 명칭 재확인 표시만 해도 오입력의 실질적 피해를 상당히 줄일 수 있습니다.
업무 코드에 UUID나 ULID를 사용해도 되나요?
데이터베이스의 내부 키로서는 문제없지만, 사람이 읽고 전기(轉記)하는 '표시용 코드'에는 적합하지 않습니다. UUID는 36자로 너무 길어 전화나 팩스로 전달하기에 견디지 못하기 때문입니다. 내부 키(UUID나 자동 일련번호)와 사람에게 보여주는 표시용 코드(짧은 일련번호+체크 디지트)를 나누는 2계층 구성으로 하면 양쪽의 요구를 모두 충족할 수 있습니다. 표시용 코드 체계를 나중에 바꾸게 되더라도 내부 키가 안정적이라면 영향 범위를 표시 계층에 가둘 수 있습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기