프로그래밍

바코드 검증

바코드의 마지막 한 자리는
아무렇게나 정해진 숫자가 아닙니다.

학습 목표

  1. 글자로 들어온 숫자열에서 자리마다 값을 꺼내 다룰 수 있다.
  2. 1부터 센 자리가 홀수 번째인지 짝수 번째인지에 따라 다르게 처리하는 반복을 설계할 수 있다.
  3. 나머지 연산을 써서 한 자리 검사 숫자를 구할 수 있다.
  4. 사람이 세는 순서(1부터)와 컴퓨터의 인덱스(글자나 값의 위치를 0부터 센 번호)를 구별해 옮길 수 있다.

무엇을 만드나

잘못 읽힌 숫자를 어떻게 알아챌 수 있을까요?

주어진 문제

여덟 자리 숫자를 입력받아, 마지막 한 자리가 앞의 일곱 자리와 아귀가 맞는지 확인해 Valid 또는 Invalid를 내놓는 프로그램을 만듭니다.

상품에 붙은 바코드의 마지막 숫자는 앞자리들로부터 계산해서 붙인 것입니다. 기계가 바코드를 잘못 읽거나 사람이 한 자리를 잘못 눌렀을 때, 계산해 본 값과 적힌 값이 어긋나므로 그 자리에서 알아챌 수 있습니다. 이런 숫자를 검사 숫자라고 합니다.

여기서 쓰는 계산법에 대하여

실제 상품 바코드는 자릿수도 더 많고 규격도 따로 있습니다. 이 실습에서는 그 방식의 뼈대만 가져와 여덟 자리로 줄인 것을 씁니다. 여기서 만든 프로그램으로 실제 상품 바코드를 검사할 수는 없습니다. 배우려는 것은 규격이 아니라 「계산해서 맞춰 보는」 생각의 틀입니다.

단계 여기서 답할 질문
1 요구 분석 몇 자리를 받고 무엇을 내놓을 것인가
2 설계 자리마다 다르게 세는 규칙을 어떻게 적을 것인가
3 구현 사람이 세는 순서를 인덱스로 어떻게 옮길 것인가
4 테스트 맞는 바코드와 틀린 바코드를 어떻게 만들 것인가

먼저 개발 문서 양식을 내려받으세요

읽기만 하지 말고 직접 채워 보세요. 단계마다 자기 답을 적어 두면 그 기록이 요구 사항 명세서이자 설계서가 됩니다. 답을 보기 전에 먼저 적어 봅니다.

요구 분석과 설계는 언어와 상관없습니다. 두 양식의 앞부분은 같고, 구현을 적는 자리만 언어에 맞춰 다릅니다. 수업에서 쓰는 언어에 맞는 것을 받으면 됩니다. 이 강의노트의 코드 예시는 파이썬으로 되어 있습니다.

1단계 · 무엇을 만들 것인가

검사 규칙을 못박지 않으면 만들 수가 없습니다.

되물어야 할 것들

  • 몇 자리를 받나요? 여덟 자리가 아니면 어떻게 하나요
  • 숫자가 아닌 글자가 들어오면?
  • 검사 숫자를 어떻게 구하나요? 규칙을 한 줄 한 줄 정해야 합니다
  • 자리는 어디부터 세나요? 왼쪽부터인가요 오른쪽부터인가요
  • 무엇을 내놓나요? 맞다 틀리다만인가요, 올바른 검사 숫자도 알려 주나요

정리한 요구 사항

  1. 여덟 자리 숫자를 공백 없이 한 줄로 입력받는다.
  2. 자리는 왼쪽부터 1번, 2번… 8번으로 센다. 8번이 검사 숫자다.
  3. 홀수 번째(1·3·5·7번) 자리의 합을 구한다.
  4. 짝수 번째(2·4·6번) 자리의 합에 3을 곱한다.
  5. 둘을 더한 값을 10으로 나눈 나머지를 구하고, 10에서 그 나머지를 뺀 값을 검사 숫자로 삼는다. 나머지가 0이면 검사 숫자도 0이다.
  6. 계산한 검사 숫자가 8번 자리와 같으면 Valid, 다르면 Invalid를 내놓는다.
  7. 여덟 자리 숫자만 들어온다고 본다. 자릿수나 글자 검사는 이번 판에서 다루지 않는다.

5번 끝의 한 줄이 없으면 틀립니다

나머지가 0일 때 10에서 0을 빼면 10이 됩니다. 그런데 검사 숫자는 한 자리라 10이 될 수 없습니다. 0이어야 합니다. 이 경우를 안 적어 두면 딱 떨어지는 바코드만 전부 Invalid가 됩니다. 자주 나오는 경우가 아니라서 몇 개 넣어 봐서는 안 잡힙니다.

스스로 확인

요구 사항 4번에서 왜 하필 3을 곱할까요? 만약 곱하지 않고 그냥 더하기만 한다면, 두 자리를 바꿔 적는 실수를 잡아낼 수 있을까요?

2단계 · 자리마다 다르게 센다

사람은 1부터 세고 컴퓨터는 0부터 셉니다.

토막 무엇을 정하는가
입력 여덟 자리 숫자 — 수가 아니라 글자로 받는다
처리 앞 일곱 자리를 홀짝으로 나눠 더하고, 검사 숫자를 계산해 비교한다
출력 Valid 또는 Invalid

왜 수가 아니라 글자로 받을까

자리마다 따로 꺼내 써야 하기 때문입니다. 수로 받아 버리면 88012340은 팔천팔백만…이라는 하나의 값이 되어 셋째 자리만 꺼내기가 번거로워집니다. 00000019처럼 0으로 시작하는 바코드는 수로 바꾸는 순간 19가 되어 앞의 0이 사라집니다. 글자로 두면 인덱스로 지정해 꺼낼 수 있습니다. 더할 때만 하나씩 수로 바꾸면 됩니다.

88012340으로 따라가 보기

자리 숫자 어디에 더하나
1번 (홀수) 8 홀수 합
2번 (짝수) 8 짝수 합
3번 (홀수) 0 홀수 합
4번 (짝수) 1 짝수 합
5번 (홀수) 2 홀수 합
6번 (짝수) 3 짝수 합
7번 (홀수) 4 홀수 합
8번 0 검사 숫자 — 더하지 않고 비교하는 데 쓴다

홀수 합은 8+0+2+4 = 14, 짝수 합은 8+1+3 = 12입니다. 14 + 12×3 = 50이고, 10으로 나눈 나머지는 0입니다. 10에서 0을 빼면 10인데 다시 10으로 나눈 나머지를 취해 0으로 만듭니다. 8번 자리가 0이니 Valid입니다.

1번 자리는 인덱스 0입니다

사람은 1번 자리라고 하고 컴퓨터는 인덱스 0이라고 합니다. 언제나 한 칸 어긋나 있습니다. 그래서 홀수 번째인지 보려면 인덱스가 아니라 「인덱스 + 1」이 홀수인지 봐야 합니다. 그냥 인덱스로 홀짝을 판단하면 홀수와 짝수가 통째로 뒤바뀝니다. 답이 나오긴 나오므로 손으로 계산해 본 값과 맞춰 보기 전에는 모릅니다.

푸는 순서

  1. 여덟 자리를 글자로 입력받는다.
  2. 앞 일곱 자리와 마지막 검사 숫자를 나눈다.
  3. 홀수 합과 짝수 합을 0으로 놓는다.
  4. 앞 일곱 자리를 하나씩 돌며, 인덱스 + 1이 홀수면 홀수 합에, 짝수면 짝수 합에 더한다.
  5. 합계 = 홀수 합 + 짝수 합 × 3
  6. 나머지 = 합계를 10으로 나눈 나머지
  7. 검사 숫자 = (10 − 나머지)를 다시 10으로 나눈 나머지
  8. 계산한 값이 8번 자리와 같으면 Valid, 아니면 Invalid
스스로 확인

7번 줄에서 「다시 10으로 나눈 나머지」를 빼면 88012340은 무엇이 나올까요? Valid일까요 Invalid일까요?

3단계 · 파이썬으로 옮기기

여덟 줄짜리 순서가 코드에서는 어떻게 될까요?

먼저 스스로 적어 본 뒤에 코드를 보세요. 반복 안에서 홀짝을 구분하는 줄을 눈여겨봅니다.

바코드.기본.py

# 여덟 자리 숫자를 글자 그대로 입력받는다
barcode = input()

# 앞 일곱 자리와 마지막 체크 숫자를 나눈다
data = barcode[:7]
check_digit = int(barcode[7])

s1 = 0  # 홀수 번째 자리의 합
s2 = 0  # 짝수 번째 자리의 합

for i in range(7):
    num = int(data[i])
    if (i + 1) % 2 == 1:  # 자리 번호가 홀수면
        s1 += num
    else:
        s2 += num

# 짝수 자리만 세 배로 세는 것이 이 방식의 핵심이다
total_sum = s1 + s2 * 3
remainder = total_sum % 10

# 나머지가 0일 때 10이 아니라 0이 되도록 한 번 더 나눈다
result = (10 - remainder) % 10

if result == check_digit:
    print("Valid")
else:
    print("Invalid")

왜 1을 더해서 홀짝을 보나

반복이 도는 값은 0, 1, 2…인데 우리가 세는 자리는 1, 2, 3…입니다. 그래서 1을 더한 값으로 홀짝을 판단합니다. 이 +1을 빼먹으면 홀수 자리와 짝수 자리가 통째로 뒤바뀌고, 그러면 3을 엉뚱한 쪽에 곱하게 됩니다.

스스로 확인

검사 숫자를 더하는 데도 쓰면 어떻게 될까요? 코드에서 앞 일곱 자리만 도는 이유를 말해 보세요.

4단계 · 넣어 보고 확인하기

맞는 바코드는 직접 만들어서 넣어야 합니다.

이 프로그램은 테스트할 값을 만드는 일부터 어렵습니다. 아무 숫자나 넣으면 거의 다 Invalid가 나오기 때문입니다. 앞 일곱 자리를 정하고 검사 숫자를 손으로 계산해 맞는 바코드를 만들어야 합니다.

넣은 값 나와야 할 결과 무엇을 보려는 값인가
88012340 Valid 나머지가 0인 경우 — 요구 사항 5번의 끝 한 줄
88012345 Invalid 앞자리는 같고 검사 숫자만 틀린 경우
12345678 Valid 나머지가 0이 아닌 일반적인 경우
12345670 Invalid 같은 앞자리에 검사 숫자만 바꾼 짝
00000019 Valid 나머지가 1일 때 검사 숫자가 9로 나오는지
00000000 Valid 전부 0 — 합계가 0인 끝값

결과가 어긋났다면 여기부터 봅니다

  • 88012340과 00000000이 Invalid로 나온다 — 다시 10으로 나누는 줄이 빠졌습니다. 10과 0을 비교하고 있습니다
  • 거의 전부 Invalid로 나온다 — 홀짝이 뒤바뀌었을 수 있습니다. 1을 더해서 판단하고 있는지 봅니다
  • 맨 마지막 자리에서 멈춘다 — 반복이 여덟 자리를 다 돌고 있는지 봅니다. 앞 일곱 자리만 돌아야 합니다
  • 글자끼리 이어 붙는다 — 꺼낸 글자를 수로 바꾸지 않고 더하고 있습니다

Valid가 나왔다고 검사가 통과한 것은 아닙니다

거꾸로 틀린 프로그램도 Valid를 내놓을 수 있습니다. 이를테면 홀짝을 통째로 뒤바꿔도 00000000은 여전히 Valid입니다 — 전부 0이니까요. 그래서 Valid만 여러 개 넣어 보는 것으로는 부족합니다. Invalid가 나와야 하는 값도 반드시 함께 넣어야 합니다.

스스로 확인

13245678은 12345678에서 2번과 3번 자리를 서로 바꾼 것입니다. 손으로 계산해 보면 Valid일까요 Invalid일까요? 짝수 자리에 3을 곱하는 이유와 이어서 생각해 보세요.

더 해 보기

1단계에서 미뤄 둔 것을 데려와 봅시다.

요구 사항 7번에서 자릿수와 글자 검사를 다루지 않기로 했습니다. 일곱 자리를 넣으면 없는 자리를 찾다가 멈추고, 숫자가 아닌 글자를 넣으면 수로 바꾸다가 멈춥니다.

스스로 해 본 뒤에 열어 보세요

더 알아보기: 자릿수를 검사하는 코드

꺼내기 전에 막습니다. 자릿수를 먼저 확인한 뒤에 계산합니다.

# 여덟 자리가 아니면 계산할 수가 없다. 맨 앞에서 막는다
barcode = input()
if len(barcode) != 8:
    print("여덟 자리를 입력하세요")
else:
    ...  # 아래는 기본 코드와 같다

한 걸음 더

  • Invalid일 때 올바른 검사 숫자가 무엇이었는지 함께 알려 주기
  • 앞 일곱 자리만 받아 검사 숫자를 만들어 주는 프로그램으로 바꾸기
  • 한 자리를 잘못 눌렀을 때 정말 다 잡히는지 0~9를 차례로 넣어 확인해 보기
  • 이웃한 두 자리를 서로 바꿔 적은 실수도 언제나 잡히는지 여러 쌍으로 바꿔 넣어 확인해 보기
스스로 확인

「검사 숫자를 만들어 주는 프로그램」은 지금 코드에서 무엇을 빼면 될까요? 여덟 줄 중 어느 줄이 필요 없어지는지 찾아보세요.