포스트

사업자·법인등록번호는 입력받기 전에 스스로 검증된다 — 체크섬 알고리즘

식별번호 체크디지트 알고리즘을 외부 호출 전 입력 검증으로 끌어올려 무효 요청과 쿼터 낭비를 막는 설계.

사업자·법인등록번호는 입력받기 전에 스스로 검증된다 — 체크섬 알고리즘

그 주엔 사용자가 입력한 식별번호를 외부 API로 보내 확인하는 기능을 다뤘다. 그런데 외부 호출은 비싸다. 네트워크 왕복 시간이 들고, 많은 오픈 API는 일일 호출 쿼터가 있다. 사용자가 오타로 자리수를 틀리거나 명백히 잘못된 번호를 넣었는데 그걸 그대로 외부로 쏘면, 외부는 “그런 번호 없음”을 돌려주고 우리는 왕복 한 번과 쿼터 하나를 날린다. 핵심은 외부에 묻기 전에 우리 스스로 거를 수 있는 무효값은 미리 거르는 것이다. 다행히 사업자등록번호·법인등록번호 같은 식별번호는 자기 자신 안에 검증 장치를 품고 있다.

형식 검증과 의미 검증은 다르다

입력 검증에는 두 층위가 있다.

  • 형식 검증(format) — 길이, 숫자만 포함, 패턴. 사업자등록번호는 10자리 숫자, 법인등록번호는 13자리 숫자. 정규식으로 끝난다.
  • 의미 검증(checksum) — 자리수와 패턴이 맞아도 “유효한 번호”가 아닐 수 있다. 체크디지트가 맞아야 한다.

1234567890은 형식은 통과하지만 체크섬은 틀린다. 의미 검증까지 통과해야 비로소 “외부에 물어볼 가치가 있는 번호”다.

체크디지트의 원리

체크디지트는 앞자리들로부터 마지막 자리를 산술적으로 도출하는 방식이다. 각 자리에 정해진 가중치를 곱하고, 그 곱의 합을 특정 수로 모듈러 연산해 검증 숫자를 만든다. 공개된 표준 공식이라 누구나 구현할 수 있다.

사업자등록번호(10자리)는 앞 9자리에 가중치 [1,3,7,1,3,7,1,3,5]를 곱한다. 단, 9번째 자리는 가중치를 곱한 뒤 그 결과를 10으로 나눈 몫을 합에 더하는 보정이 들어간다. 합을 10으로 나눈 나머지를 10에서 빼고 다시 10으로 나눈 나머지가 마지막(10번째) 검증 숫자다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class RegistrationNumberValidator {

    private static final int[] WEIGHTS =
        {1, 3, 7, 1, 3, 7, 1, 3, 5};

    /** 사업자등록번호 10자리 체크섬 검증 */
    public static boolean isValidBusinessNumber(String raw) {
        if (raw == null) return false;
        String digits = raw.replaceAll("[^0-9]", "");
        // 1) 형식 검증
        if (digits.length() != 10) return false;

        // 2) 의미(체크섬) 검증
        int sum = 0;
        for (int i = 0; i < 9; i++) {
            sum += (digits.charAt(i) - '0') * WEIGHTS[i];
        }
        // 9번째 자리 가중치 곱의 10단위 몫을 보정 가산
        sum += ((digits.charAt(8) - '0') * 5) / 10;

        int check = (10 - (sum % 10)) % 10;
        int last  = digits.charAt(9) - '0';
        return check == last;
    }
}

체크섬 공식 자체는 공개 표준 지식이므로 구현해도 무방하다. 단, 외부 API의 실제 서비스키나 엔드포인트는 코드에 섞지 않는다. 체크섬은 외부 의존 없이 순수 계산으로 끝난다는 점이 중요하다.

검증 위치를 진입점으로 끌어올린다

가장 흔한 실수는 체크섬 검증을 외부 호출 직전, 클라이언트 깊숙한 곳에 두는 것이다. 그러면 잘못된 입력이 여러 계층을 통과한 뒤에야 걸러진다. 검증은 요청이 들어오는 진입점(컨트롤러/입력 DTO) 으로 끌어올린다.

1
2
3
4
5
6
7
8
9
10
11
public class LookupRequest {
    @NotBlank
    private String registrationNumber;

    // 진입점에서 형식+의미 검증을 끝낸다
    public void validate() {
        if (!RegistrationNumberValidator.isValidBusinessNumber(registrationNumber)) {
            throw new IllegalArgumentException("유효하지 않은 등록번호 형식이다");
        }
    }
}

이렇게 하면 무효 입력은 외부 클라이언트 코드에 닿기도 전에 막히고, 외부 호출 횟수는 “체크섬을 통과한, 그럴듯한 번호”로만 제한된다. 쿼터와 왕복 지연을 모두 아낀다.

운영 함정

  • 체크섬 통과 ≠ 실제 존재. 체크섬은 “오타가 아닌, 형식적으로 유효한 번호”임을 보장할 뿐, 실제 등록된 번호인지는 외부만 안다. 체크섬으로 거른 뒤에도 외부 확인은 필요하다. 둘을 혼동해 “체크섬 통과했으니 존재한다”고 단정하면 안 된다.
  • 하이픈·공백 정규화를 먼저. 사용자는 123-45-67890처럼 구분자를 넣는다. 숫자만 추출하는 정규화를 검증 맨 앞에 두지 않으면 길이 검증부터 틀어진다.

핵심 요약

  • 식별번호는 체크디지트(가중치 합 + 모듈러) 로 자기 검증한다 — 외부 의존 없는 순수 계산.
  • 형식 검증과 의미(체크섬) 검증을 구분하고, 둘 다 진입점에서 끝내 무효 요청이 외부 호출 쿼터를 낭비하지 않게 한다.
  • 면접 Q. 외부 검증 API 호출을 줄이려면? A. 외부에 묻기 전 로컬에서 가능한 검증(형식 + 체크섬)을 진입점으로 끌어올려, 명백한 무효 입력을 미리 거른다. 외부 호출은 자기검증을 통과한 입력으로만 제한한다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.