Step 49. ★ 프로젝트: 비밀번호 도구 — 첫 번째 보안 도구를 만든다
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간
전제: Step 41~48을 마쳤다. 함수, 반복, 문자열, 예외처리, 파일 저장을 쓸 수 있다.
- 준비물: 파이썬이 설치된 컴퓨터, 텍스트 에디터. 새로 설치하는 것은 없습니다.
- ⚠️ 안전 고지: 오늘 만드는 도구는 내 파일과 내 계정 전용입니다. 자신의 비밀번호 강도를 재고, 자신이 쓸 비밀번호를 생성하는 도구이지, 타인의 비밀번호를 시험하거나 알아내는 도구가 아닙니다. 다른 사람의 계정에 이 도구를 들이대는 행위는 범죄입니다.
"123456"과 "X9#mQ2$vL8&z"라는 비밀번호는 무엇이 다를까요? 대답은 "공격자가 알아내는 데 걸리는 시간"입니다. 전자는 전 세계 유출 비밀번호 순위 1위라 0.1초, 후자는 조합이 너무 많아 사실상 무한대에 가깝습니다. 오늘은 이 차이의 원리를 손으로 확인하고, 강한 비밀번호를 만드는 기계와 강도를 재는 기계 — 여러분의 첫 번째 보안 도구를 만듭니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 비밀번호 강도가 조합 수(곱셈)로 결정됨을 숫자로 설명한다
- 보안용 난수(secrets)와 게임용 난수(random)의 차이를 설명한다
- string 모듈의 문자 집합으로 비밀번호 생성기를 완성한다
- 길이와 다양성 조건으로 강도 검사기(점수 + 흔한 비밀번호 경고)를 완성한다
- 사용 설명서(README)까지 갖춘 도구를 완성한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 (표준 라이브러리만 사용, 설치 불필요) |
| 오늘의 모듈 | secrets(보안용 난수), string(문자 집합) |
| 복습 문법 | 함수와 기본값 매개변수, for 반복, any(), while + try 입력 루프 |
| 필요한 개념 | 조합 수(강도의 원리), 엔트로피, 예측 가능한 난수의 위험 |
| 오늘의 산출물 | pwtool.py(생성+검사 메뉴 도구)와 README.md |
2-1. 조합의 곱셈 — 강도의 원리
자물쇠를 떠올려 봅시다. 다이얼이 하나(10가지 숫자)면 후보는 10개, 네 개면 10×10×10×10 = 10,000개. 다이얼 수(자릿수)와 눈금 수(글자 종류)가 늘수록 후보는 곱으로 폭발합니다.
비밀번호도 같습니다. 실측으로 확인한 세 비밀번호의 조합 수 (2026-09-09):
123456 -> 1000000 (숫자만 6자리: 10의 6제곱)
qwerasdf -> 208827064576 (소문자만 8자리: 26의 8제곱 ≈ 2천억)
kX#7mQ2$vL8&zP4a -> 37157429083410091685945089785856
세 번째는 대소문자+숫자+특수문자(94종) 16자리로, 후보가 약 3.7×10의 28제곱 개입니다. 공격자가 초당 10억 번 시도해도 우주의 나이보다 오래 걸립니다. 이 "후보의 총수"가 강도의 정체이고, 그 로그값(지수)을 전문 용어로 엔트로피(entropy)라고 부릅니다. 지금은 "조합 수가 곧 강도다"만 기억하면 됩니다.
2-2. random과 secrets — 두 개의 주사위
Step 47에서 배운 random은 사실 진짜 무작위가 아닙니다. "씨앗(seed)에서 시작하는 정해진 계산"으로 무작위처럼 보이는 수를 뽑습니다. 게임에는 충분하지만, 씨앗을 알면 다음 수를 예측할 수 있습니다 — 비밀번호를 이 주사위로 만들면 규칙을 아는 공격자에게 뚫립니다.
그래서 보안용으로는 secrets 모듈을 씁니다. 운영체제가 제공하는 예측 불가능한 무작위를 쓰고, 사용법은 random과 거의 같아 바꾸기도 쉽습니다.
- 게임, 섞기, 시험 데이터:
random - 비밀번호, 열쇠, 토큰:
secrets
이 구분을 설명할 수 있으면 오늘의 절반은 성공입니다.
2-3. 문자 집합 — string 모듈
비밀번호의 재료 목록(어떤 글자들을 쓸지)은 string 모듈에 준비되어 있습니다.
import string
print(string.ascii_lowercase) # abcdefghijklmnopqrstuvwxyz
print(string.ascii_letters) # 소문자 + 대문자
print(string.digits) # 0123456789
print(string.punctuation) # !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
이 목록들을 더하면 재료 바구니가 됩니다. 재료가 많을수록 자물쇠의 눈금이 늘어나고 조합 수가 폭발합니다.
3. 따라 하기
3-1. secrets 첫 경험
입력 (sec1.py)
import secrets
import string
pool = string.ascii_letters + string.digits
print(secrets.choice(pool))
print(secrets.choice(pool))
print(secrets.choice(pool))
실행해 보세요. 여러 번.
읽는 법: 매번 다른 글자가 나옵니다. secrets.choice(바구니)는 바구니에서 하나를 무작위로 집는 것으로, 사용법이 random.choice와 같습니다. 모듈만 다릅니다.
3-2. 생성기 — 자릿수만큼 반복
입력 (pwgen.py)
import secrets
import string
def generate_password(length=16):
pool = string.ascii_letters + string.digits + "!@#$%^&*"
password = ""
for _ in range(length):
password = password + secrets.choice(pool)
return password
print(generate_password())
print(generate_password(12))
출력의 예 (실행할 때마다 다릅니다):
kX#7mQ2$vL8&zP4a
WWo7JS08ZAWL
(2026-09-09 실측에서 12자리 생성으로 얻은 두 번째 줄은 실제 출력입니다. 여러분은 다른 값이 나옵니다 — 그게 정상입니다.)
읽는 법: 밑줄 _은 "번호가 필요 없는 반복"의 관용 표현입니다. for _ in range(16):은 "그냥 16번 돌아라". 매 바퀴 재료를 하나 뽑아 이어 붙입니다. 기본값 매개변수(Step 44)로 16자리를 기본으로 하되, 원하면 12자리도 됩니다.
예측: 같은 코드를 바로 다시 실행하면 같은 비밀번호가 나올까요? 실행해 보세요. 그리고 secrets를 random.choice로 바꿔서도 해 보세요. 결과의 "매번 다름"은 같아 보이지만, 둘의 차이는 예측 가능성에 있습니다 (2-2절).
3-3. 강도 재기 — 조합 수 계산기
입력 (strength.py)
def estimate_pool_size(pw):
pool = 0
if any(c.islower() for c in pw):
pool = pool + 26
if any(c.isupper() for c in pw):
pool = pool + 26
if any(c.isdigit() for c in pw):
pool = pool + 10
if any(not c.isalnum() for c in pw):
pool = pool + 32
return pool
def combinations(pw):
pool = estimate_pool_size(pw)
return pool ** len(pw)
print(combinations("123456"))
print(combinations("qwerasdf"))
print(combinations("kX#7mQ2$vL8&zP4a"))
출력 (2026-09-09 실측):
1000000
208827064576
37157429083410091685945089785856
읽는 법: 새 함수 any(조건 for c in pw)는 "하나라도 조건에 맞는 글자가 있니?"입니다. .islower()는 소문자인지, .isupper()는 대문자인지, .isdigit()은 숫자인지, .isalnum()은 영숫자인지를 묻는 문자열 검사기들입니다. 포함된 글자 종류만큼 재료 수(pool)를 더하고, 자릿수만큼 거듭제곱(**)합니다.
왜: 세 숫자를 비교해 보세요. 자릿수와 재료가 강도를 어떻게 곱으로 키우는지 숫자가 직접 말해 줍니다.
3-4. 점수표 만들기 — 강도 검사기
조합 수는 너무 크니, 사람이 읽기 좋은 점수로 바꿉니다.
입력 (checker.py)
def score_password(pw):
score = 0
if len(pw) >= 8:
score = score + 1
if len(pw) >= 12:
score = score + 1
if any(c.islower() for c in pw) and any(c.isupper() for c in pw):
score = score + 1
if any(c.isdigit() for c in pw):
score = score + 1
if any(not c.isalnum() for c in pw):
score = score + 1
return score
def grade(score):
if score <= 2:
return "약함 — 즉시 바꾸세요"
if score == 3:
return "보통 — 길이를 늘려 보세요"
if score == 4:
return "강함"
return "매우 강함"
pw = input("검사할 비밀번호: ")
s = score_password(pw)
print(f"점수: {s}/5 — {grade(s)}")
123456, Qwer1234, kX#7mQ2$vL8&zP4a를 넣은 실측 (2026-09-09):
123456 -> 점수: 1/5 — 약함 — 즉시 바꾸세요
Qwer1234 -> 점수: 3/5 — 보통 — 길이를 늘려 보세요
kX#7mQ2$vL8&zP4a -> 점수: 5/5 — 매우 강함
읽는 법: 점수 기준은 우리가 정한 것입니다. 길이(8/12자리)와 다양성(대소·숫자·특수)의 다섯 조건입니다. 이런 기준을 정하고 근거를 주석으로 남기는 것도 설계의 일부입니다.
3-5. 흔한 비밀번호 경고
강도 계산과 별개로, 사전에 있는 것은 즉시 걸러야 합니다.
입력 (checker.py에 추가)
COMMON = ["123456", "password", "qwerty", "111111", "abc123", "12345678"]
if pw.lower() in COMMON:
print("경고: 전 세계에서 가장 흔한 비밀번호입니다. 조합 수와 무관하게 1초면 뚫립니다!")
123456을 검사한 실측 (2026-09-09) — 점수 아래에 경고가 함께 뜹니다:
점수: 1/5 — 약함 — 즉시 바꾸세요
경고: 전 세계에서 가장 흔한 비밀번호입니다. 조합 수와 무관하게 1초면 뚫립니다!
읽는 법: 공격자는 무작위 조합보다 유출된 비밀번호 목록부터 시도합니다. 12345678은 8자리라 나쁘지 않아 보여도 사전 상위라 무의미합니다. .lower()는 대소문자를 통일해 비교하는 기술입니다(PASSWORD와 password를 같게 취급).
왜: 강도는 수학이면서 동시에 심리입니다. 사람이 기억하기 쉬운 것은 공격자도 시도하기 쉽습니다.
3-6. 두 도구 합치기 — 메뉴 프로그램 완성
입력 (pwtool.py)
import secrets
import string
COMMON = ["123456", "password", "qwerty", "111111", "abc123", "12345678"]
def generate_password(length=16):
pool = string.ascii_letters + string.digits + "!@#$%^&*"
password = ""
for _ in range(length):
password = password + secrets.choice(pool)
return password
def score_password(pw):
score = 0
if len(pw) >= 8:
score = score + 1
if len(pw) >= 12:
score = score + 1
if any(c.islower() for c in pw) and any(c.isupper() for c in pw):
score = score + 1
if any(c.isdigit() for c in pw):
score = score + 1
if any(not c.isalnum() for c in pw):
score = score + 1
return score
def grade(score):
if score <= 2:
return "약함 — 즉시 바꾸세요"
if score == 3:
return "보통 — 길이를 늘려 보세요"
if score == 4:
return "강함"
return "매우 강함"
while True:
print("=== 비밀번호 도구 ===")
print("1. 생성 2. 강도 검사 0. 종료")
menu = input("선택: ").strip()
if menu == "1":
while True:
try:
n = int(input("자릿수(기본 16, 그냥 엔터 가능): ") or "16")
break
except ValueError:
print("숫자로 입력해 주세요.")
elif menu == "2":
pw = input("검사할 비밀번호: ")
s = score_password(pw)
print(f"점수: {s}/5 — {grade(s)}")
if pw.lower() in COMMON:
print("경고: 전 세계에서 가장 흔한 비밀번호입니다. 조합 수와 무관하게 1초면 뚫립니다!")
elif menu == "0":
print("종료합니다.")
break
else:
print("0, 1, 2 중에서 고르세요.")
실제로 돌린 실행 (2026-09-09 실측 — 자릿수에 일부러 "열두"를 넣고, 메뉴에 없는 9도 넣었습니다):
=== 비밀번호 도구 ===
1. 생성 2. 강도 검사 0. 종료
선택: 자릿수(기본 16, 그냥 엔터 가능): 숫자로 입력해 주세요.
자릿수(기본 16, 그냥 엔터 가능): 생성됨: WWo7JS08ZAWL
=== 비밀번호 도구 ===
1. 생성 2. 강도 검사 0. 종료
선택: 검사할 비밀번호: 점수: 1/5 — 약함 — 즉시 바꾸세요
경고: 전 세계에서 가장 흔한 비밀번호입니다. 조합 수와 무관하게 1초면 뚫립니다!
=== 비밀번호 도구 ===
1. 생성 2. 강도 검사 0. 종료
선택: 0, 1, 2 중에서 고르세요.
=== 비밀번호 도구 ===
1. 생성 2. 강도 검사 0. 종료
선택: 종료합니다.
읽는 법: Step 44의 메뉴 구조에 Step 46의 입력 루프(while + try)를 얹은 것입니다. "열두"라는 글자 입력은 다시 묻기로 처리되고, 메뉴의 9는 안내 문구로 처리되며, 종료(0)까지 프로그램이 죽지 않습니다. or "16"은 빈 입력(그냥 엔터)이면 기본값 16을 쓰는 기술입니다.
4. 미션과 연습문제
미션 — 도구 완성 + README
완성한 pwtool.py에 사용 설명서를 붙이세요:
- 3-6의 완성판이 빠짐없이 동작하는지 확인한다 (생성, 검사, 경고, 나쁜 입력 처리)
README.md파일을 만들고 다음을 적는다: 도구 이름, 만든 사람, 사용법(실행 방법과 메뉴 설명), 만든 날짜- README에 안전 고지도 포함한다: "이 도구는 내 계정의 비밀번호 관리 전용입니다"
README 예시:
# 비밀번호 도구 (pwtool)
- 만든 사람: 나
- 실행: python pwtool.py
- 기능: 1번은 강한 비밀번호 생성, 2번은 내 비밀번호 강도 검사
- 주의: 이 도구는 내 계정 관리 전용. 생성된 비밀번호는 비밀번호 관리자에 보관할 것
도구를 만들었다면 설명서를 쓰는 것까지가 작품입니다. 남이 쓸 수 있어야 도구이니까요.
연습문제
문제 1. secrets와 random의 차이를 설명하고, 각각 어떤 용도에 써야 하는지 말해 보세요.
문제 2. "소문자만 8자리"와 "대소문자+숫자 8자리"의 조합 수는 각각 어느 정도이며, 이 차이가 의미하는 바는 무엇인가요?
문제 3. 12345678은 8자리 숫자라 조합 수가 1억입니다. 그런데도 "1초면 뚫리는" 비밀번호인 이유는 무엇인가요?
문제 4. any(not c.isalnum() for c in pw) 조건은 무엇을 검사하는 것이며, not을 빼면 어떤 오판이 생기나요?
5. 모범 답안과 완료 기준
미션 모범 답안
README 모범 예:
# 비밀번호 도구 (pwtool)
- 만든 사람: 나
- 만든 날짜: 2026-09-09
- 실행: python pwtool.py
- 메뉴:
- 1. 생성 — 자릿수를 정하면 secrets로 추측 불가능한 비밀번호를 만든다
- 2. 강도 검사 — 길이·다양성 5점 만점 + 흔한 비밀번호 경고
- 0. 종료
- 주의: 이 도구는 내 계정의 비밀번호 관리 전용입니다.
생성된 비밀번호는 메신저에 보내지 말고 비밀번호 관리자에 직접 옮겨 적습니다.
검증하는 법: ① 메뉴 1에서 글자("열두")와 빈 칸을 넣어도 다시 묻거나 기본값으로 넘어가는가. ② 메뉴 2에서 123456은 경고가, 16자리 혼합은 "매우 강함"이 뜨는가. ③ 메뉴에 9나 글자를 넣으면 안내가 뜨는가. ④ README만 읽은 사람이 실행부터 메뉴 선택까지 할 수 있는가. 네 가지가 전부 ‘예’이면 완성입니다.
연습문제 해답
문제 1 해답. random은 씨앗(seed)에서 시작하는 정해진 계산으로 무작위를 흉내 내어, 씨앗을 알면 예측 가능합니다. secrets는 운영체제가 제공하는 예측 불가능한 무작위를 씁니다. 게임·섞기·시험 데이터에는 random, 비밀번호·열쇠·토큰에는 secrets를 씁니다.
문제 2 해답. 소문자만 8자리는 26의 8제곱 ≈ 2,088억, 대소문자+숫자 8자리는 62의 8제곱 ≈ 218조입니다 (26의 8제곱은 2026-09-09 실측 208,827,064,576). 재료 종류가 26에서 62로 늘면 같은 자릿수에서도 후보가 약 천 배로 늡니다 — 자릿수뿐 아니라 재료의 다양성도 강도를 곱으로 키웁니다.
문제 3 해답. 공격자는 무작위 조합(무차별 대입)보다 유출된 비밀번호 사전부터 시도하기 때문입니다. 12345678은 사전 최상위라 조합 수 계산과 무관하게 첫 시도 묶음에 들어갑니다. 강도는 수학이면서 동시에 심리입니다 — 사람이 기억하기 쉬운 것은 공격자도 시도하기 쉽습니다.
문제 4 해답. 특수문자가 하나라도 있는지 검사합니다. isalnum()은 "영숫자이면 True"이므로, 특수문자는 "isalnum이 아닌 것"으로 잡습니다. not을 빼면 조건이 "영숫자가 있으면"으로 뒤집혀, 특수문자가 없는 비밀번호에도 특수문자 점수가 올라가는 오판이 생깁니다.
완료 기준 체크리스트
- [ ] secrets와 random의 차이를 설명할 수 있다
- [ ] 조합 수의 곱셈 원리를 실측 숫자로 설명할 수 있다
- [ ] 비밀번호 생성기(자릿수 지정 + 예외처리)를 완성했다
- [ ] 강도 검사기(점수 + 흔한 비밀번호 경고)를 완성했다
- [ ] 메뉴에 없는 입력과 글자 입력에도 도구가 죽지 않는다
- [ ] 미션: README.md까지 갖춘 도구를 완성했다
- [ ] 이 도구가 내 파일/내 계정 전용임을 이해했다
6. 흔한 실수와 해결
벽 1. 생성할 때마다 같은 비밀번호가 나온다
증상: 실행할 때마다 결과가 같습니다.
원인: secrets.choice(pool) 호출이 for 밖에 있어 한 번만 뽑고 복사한 경우가 대부분입니다. 또는 random을 쓰면서 시드를 고정한 경우입니다.
해결: choice 호출이 for 안에 있는지 확인하세요. 매 자리마다 새로 뽑아야 합니다.
벽 2. 점수가 이상하게 낮거나 높다
증상: 괜찮은 비밀번호인데 "약함"이 나옵니다.
원인: 조건의 순서나 연산자 실수. 특히 and와 or를 헷갈린 경우("소문자도 있고 대문자도 있어야"는 and입니다).
해결: 각 조건을 print로 하나씩 확인하세요. print(any(c.isdigit() for c in pw))처럼. 점수가 어디서 안 오르는지 찾는 것이 빠릅니다.
벽 3. 특수문자가 검사에서 빠진다
증상: 특수문자가 있는데 점수가 안 오릅니다.
원인: not c.isalnum() 조건을 잘못 쓴 경우가 많습니다. isalnum()은 "영숫자이면 True"이므로, 특수문자는 "isalnum이 아닌 것"으로 잡아야 합니다.
해결: 해당 조건을 단독으로 시험하세요. print([not c.isalnum() for c in pw])로 글자별 판정을 보면 어디가 틀렸는지 바로 보입니다.
벽 4. 자릿수 입력에서 그냥 엔터를 치면 죽는다
증상: 기본값을 쓰려고 엔터만 쳤는데 ValueError로 다시 묻습니다.
원인: 빈 입력은 빈 문자열 ""이고, int("")는 ValueError입니다.
해결: input(...) or "16" 기술을 쓰세요 — 빈 문자열은 거짓 취급되어 "16"이 대신 쓰입니다 (3-6절 완성판에 포함, 2026-09-09 실측 확인).
벽 5. 생성된 비밀번호를 채팅으로 보낸다
증상: 만든 비밀번호를 메신저로 옮기다가 유출 사고.
원인: 도구는 무죄, 운용이 문제입니다. 비밀번호는 평문으로 메신저나 메일에 남기면 안 됩니다.
해결: 생성된 비밀번호는 비밀번호 관리자(패스워드 매니저)에 직접 옮겨 적는 습관을 들이세요. 도구의 생성물 취급도 보안 수칙의 연장선입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 조합 수 | 글자 종류 수의 자릿수 제곱 — 강도의 정체 |
| 엔트로피 | 조합 수의 로그값(지수)으로 나타낸 강도 |
| secrets | 보안용 난수 — 예측 불가능 (비밀번호·열쇠용) |
| random | 게임용 난수 — 씨앗을 알면 예측 가능 |
| 흔한 비밀번호 사전 | 조합 수와 무관하게 1초면 뚫리는 함정 |
오늘의 문법·부품
| 부품 | 하는 일 |
|---|---|
secrets.choice(바구니) |
바구니에서 예측 불가능하게 하나 선택 |
string.ascii_letters / digits / punctuation |
재료 문자 집합 |
any(조건 for c in pw) |
하나라도 맞는 글자가 있는지 |
islower / isupper / isdigit / isalnum |
글자 종류 검사기 |
pool ** len(pw) |
조합 수 계산 (거듭제곱) |
input(...) or "16" |
빈 입력에 기본값 적용 |
명령어보다 중요한 감각
공격자의 곱셈을 아는 사람은 방어자의 곱셈도 압니다. 오늘 여러분은 "강함의 원리"를 손으로 확인했고, 그 원리가 작동하는 도구를 직접 만들었습니다.
두 가지를 더 기억해 두세요. 첫째, "강한 비밀번호를 사이트마다 다르게"는 인간의 기억력으로 불가능하므로, 실무에서는 비밀번호 관리자 프로그램에 맡깁니다 — 오늘의 도구는 그 관리자의 심장부를 축소 체험한 것입니다. 둘째, 최근 지침은 복잡함보다 길이를 강조합니다. correct horse battery staple처럼 길고 외우기 쉬운 문장형이 짧고 복잡한 것보다 강하면서 기억하기도 좋습니다 — 자릿수가 늘면 지수가 커지는 곱셈을 생각하면 이유가 보입니다.
축하합니다. 오늘 여러분은 첫 번째 "보안 도구"를 만들었습니다. 게임과 달리 이 도구는 당장 실생활에서 쓸 수 있고, 가족의 비밀번호 습관까지 바꿀 수 있습니다.
전부 체크되면 Step 49 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.