회원 가입 폼이나 거래처 관리 프로그램을 만들다 보면 주민등록번호(또는 형식이 같은 외국인등록번호) 입력값을 한 번은 걸러야 할 일이 생깁니다. 자릿수만 보는 것보다 한 단계 더 나가서 "숫자 조합이 말이 되는지"를 보려면 뒷자리 7개가 어떤 규칙으로 만들어졌는지 알아야 합니다.
앞 6자리는 다들 아시는 대로 생년월일(YYMMDD)입니다. 문제는 뒤 7자리인데, 자리마다 역할이 정해져 있습니다.
| 위치 | 의미 |
|---|---|
| 1번째 | 성별 + 출생 세기 |
| 2~5번째 | 최초 등록한 관할 지역 번호 |
| 6번째 | 같은 날 같은 지역에서 신고한 순서 |
| 7번째 | 앞 12자리로 계산한 검증번호 |
첫 자리가 1·2만 있는 게 아닌 이유
첫 자리는 성별을 나타내지만 출생 연도의 세기도 같이 담고 있습니다. 1900년대 출생은 남자 1, 여자 2이고, 2000년 이후 출생은 남자 3, 여자 4입니다. 100년 단위로 번호가 바뀌는 구조라서 1800년대 출생자에게는 남자 9, 여자 0이 쓰였습니다. 그래서 생년월일 계산 로직을 짤 때 앞자리 YY 만 보고 1900을 더하면 2000년생부터 나이가 100살 많게 나옵니다. 첫 자리를 같이 봐서 세기를 정해야 합니다.
참고로 외국인등록번호도 같은 13자리 형식을 쓰는데, 첫 자리에 5~8이 들어갑니다. 입력 검증을 할 때 1~4만 허용하도록 막아 두면 외국인 직원이나 고객이 가입을 못 하는 일이 생기니 요구사항을 먼저 확인하는 게 좋습니다.
2~5번째 네 자리는 처음 출생신고를 한 관할 관청의 지역 코드이고, 6번째는 그날 그 지역에서 신고한 순서입니다. 같은 동네에서 같은 날 신고한 아이가 많지 않으니 이 자리는 대부분 1이나 2 정도의 작은 숫자가 나옵니다. 이사를 가지 않고 같은 곳에서 출생신고를 한 형제라면 2~5번째 네 자리가 같게 나오는 것도 이 때문입니다.
마지막 자리, 검증번호 계산
13번째 숫자는 앞 12자리가 제대로 조합됐는지 확인하는 체크 디지트입니다. 계산 순서는 이렇습니다.
- 앞 12자리 각각에 가중치
2 3 4 5 6 7 8 9 2 3 4 5를 차례로 곱해서 모두 더합니다. - 그 합을 11로 나눈 나머지를 구합니다.
- 11에서 나머지를 뺍니다. 결과가 10이나 11이면 일의 자리만 씁니다(10 → 0, 11 → 1).
가상의 번호 900101-1234568 로 직접 해보면 다음과 같습니다.
숫자 9 0 0 1 0 1 1 2 3 4 5 6
가중치 2 3 4 5 6 7 8 9 2 3 4 5
곱 18 0 0 5 0 7 8 18 6 12 20 30 합계 = 124
124 mod 11 = 3
11 - 3 = 8 → 마지막 자리 8 과 일치
3단계에서 나머지가 0이나 1이면 11 - 나머지가 11, 10이 되어 한 자리 숫자가 아니게 됩니다. 그래서 코드로 옮길 때는 (11 - 나머지) Mod 10 처럼 한 번 더 10으로 나눈 나머지를 쓰는 게 안전합니다. 이 부분을 빼먹으면 정상 번호를 오류로 튕겨내는 경우가 생깁니다.
VBA로 짜면 이 정도입니다. 하이픈이 있든 없든 받아서 숫자만 남긴 뒤 계산합니다.
Public Function IsValidJumin(ByVal s As String) As Boolean
Dim i As Integer, total As Long, chk As Integer
Dim w As Variant
w = Array(2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5)
s = Replace(s, "-", "")
If Len(s) <> 13 Or Not IsNumeric(s) Then Exit Function
For i = 1 To 12
total = total + CInt(Mid(s, i, 1)) * w(i - 1)
Next i
chk = (11 - (total Mod 11)) Mod 10
IsValidJumin = (chk = CInt(Right(s, 1)))
End Function
PHP 쪽에서 서버 검증을 할 때도 구조는 같습니다.
function isValidJumin(string $s): bool
{
$s = str_replace('-', '', $s);
if (!preg_match('/^\d{13}$/', $s)) {
return false;
}
$w = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5];
$total = 0;
for ($i = 0; $i < 12; $i++) {
$total += (int)$s[$i] * $w[$i];
}
$chk = (11 - ($total % 11)) % 10;
return $chk === (int)$s[12];
}
2020년 10월 이후 번호는 다르다
주의할 점이 하나 있습니다. 2020년 10월부터 주민등록번호 부여 방식이 바뀌어서, 이후 새로 발급된 번호는 성별 자리 뒤 여섯 자리가 지역 코드 없이 임의 번호로 채워집니다. 지역을 추정할 수 없게 하려는 목적이라 위에서 설명한 지역번호·일련번호 규칙이 더 이상 맞지 않고, 검증번호 규칙을 따르지 않는 번호도 정상적으로 존재합니다.
그러니 위 체크섬 함수로 걸러내면 최근 출생자나 번호를 새로 받은 사람이 오류로 막힐 수 있습니다. 그래서 저는 자릿수·숫자 여부·생년월일 유효성 정도만 필수로 보고, 체크섬은 경고 수준으로만 쓰는 편이 사고가 적습니다. 그리고 애초에 주민등록번호는 법령 근거가 없으면 수집 자체를 하면 안 되는 정보라서, 꼭 필요한 경우가 아니면 받지 않는 쪽으로 설계하는 게 먼저입니다.
참고: 네이버 지식iN 질문 글