보안 가이드

비밀번호를 안전하게 저장하는 법: 올바른 해싱과 솔트

평문, 암호화, 단순 해시가 모두 실패하는 이유와 사용자별 솔트의 역할, 권장 방식인 느린 해시(Argon2id, bcrypt, scrypt)의 선택, 튜닝, 기존 시스템 이전 방법을 정리합니다.

게시 2026-06-27 업데이트 2026-10-06 7분 읽기
이 페이지에서

"사용자 비밀번호를 데이터베이스에 정확히 어떻게 저장해야 하지?" — 만드는 사람이라면 한 번은 부딪히는 질문입니다. 답은 명확합니다. 여기 안전하게 저장하는 법을 순서대로, 공격 절차 없이 정리합니다.

평문·암호화·생 해시가 모두 실패하는 이유

언젠가 데이터베이스가 샌다고 가정하세요. 그때 피해는 저장 방식에 따라 천차만별입니다.

  • 평문: 새는 순간 모든 비밀번호가 노출됩니다. 더 나쁜 것은, 비밀번호 재사용이 침해를 사용자의 다른 서비스로 연쇄시킵니다. 최악입니다.
  • 암호화(가역): 키가 그것을 되돌립니다 — 그래서 키가 함께 새면 평문으로 돌아옵니다. 비밀번호는 되읽을 필요가 없으니, 가역성은 아무 이득도 주지 않습니다.
  • 생 해시(MD5/SHA-256): 빠른 해시는 공격자가 추측을 빠르게 시험하게 합니다. 흔한 비밀번호는 레인보우 테이블과 무차별 대입에 무너집니다.

안전한 저장까지의 네 단계

약한 방식에 한 번에 하나씩 보강을 더해 가는 것으로 이해하면 가장 빠릅니다.

1. 평문: 새면 모든 비밀번호를 읽을 수 있음
↓ 일방향으로 만든다
2. 생 해시: 레인보우 테이블에 약함
↓ 사용자별 솔트를 더한다
3. 솔트 해시: 테이블은 막았으나 무차별 대입은 여전히 빠름
↓ 의도적으로 느리게 만든다
4. 솔트 + 느린 해시(Argon2id / bcrypt): 이것이 정답
평문 → 생 해시 → 솔트 → 느린 해시. 마지막에 이르러서야 비로소 '안전한 저장'입니다.

핵심 통찰: 솔트와 느린 해시는 서로 다른 일을 합니다. 솔트는 사전 계산(레인보우 테이블)과 대량 재사용 크래킹을 무력화합니다. 느린 해시는 무차별 대입을 비현실적인 속도로 떨어뜨립니다. 안전해지려면 둘 다 필요합니다(→ 해싱이란 무엇인가).

실전: 지금 무엇을 할까

1

Argon2id(또는 bcrypt)를 쓴다

신규 시스템이라면 Argon2id를 1순위로 — 메모리까지 요구할 수 있어 GPU 무차별 대입에 저항합니다. 검증된 구현을 선호한다면 bcrypt도 실전에서 충분합니다. 어느 쪽이든 표준 라이브러리 구현을 그대로 쓰세요.

2

솔트는 라이브러리가 생성하게 한다

bcrypt/Argon2는 사용자별 솔트를 생성해 해시 문자열 안에 삽입합니다. 별도의 솔트 컬럼을 둘 필요가 없습니다. MD5(salt + password)를 손수 짜는 것은 전형적인 하지 말아야 할 일입니다.

3

환경에 맞게 비용을 튜닝한다

bcrypt의 비용 인자, 또는 Argon2의 메모리/반복/병렬도를 정상 로그인이 여전히 즉각적으로 느껴지는 한도에서 최대한 높게 설정하세요. 서버가 빨라지면 공격자도 빨라지므로 — 매년 다시 점검해 올리세요.

4

표준 비교 함수로 검증한다

로그인 시 입력을 저장된 해시와 라이브러리의 검증 함수로 대조하세요(대부분 타이밍 안전한 상수 시간 비교를 씁니다). 직접 문자열 동등 비교를 짜지 마세요.

5

약한 해시는 로그인 시 재해싱으로 이전한다

이미 MD5/SHA-256을 저장 중이라면, 사용자가 로그인에 성공하는 순간 새 방식으로 재해싱해 저장하세요. 로그인하지 않은 사용자는 기존 해시를 bcrypt로 한 번 더 해싱해(레이어드 해시) 그동안 보호를 강화할 수 있습니다.

흔한 실수 vs 올바른 구현

흔한 실수

  • 저장용으로 비밀번호를 암호화(키가 새면 → 끝)
  • MD5(password)나 SHA-256(password)를 그대로 저장
  • 모든 사용자에게 단일 공유 솔트 사용
  • hash(salt + password)를 손수 짜기

올바른 구현

  • Argon2id / bcrypt로 일방향 해시
  • 사용자별 솔트(라이브러리가 자동으로 추가)
  • 괜찮게 느껴지는 한도에서 높은 비용, 주기적으로 상향
  • 표준 해시/검증 함수를 그대로 사용

해싱 vs 암호화 vs 인코딩

이 셋은 자주 혼동되지만, 되돌릴 수 있는지가 전혀 다릅니다. 비밀번호 저장에 쓸 수 있는 것은 해싱뿐입니다.

해싱암호화인코딩(Base64 등)
되돌릴 수 있나없음(일방향)키가 있으면 가능누구나 가능
키없음필요없음
주된 용도비밀번호 저장, 변조 탐지데이터나 통신의 기밀 유지데이터 형식 변환
비밀번호 저장에쓴다(솔트 + 느린 해시)쓰지 않는다쓰지 않는다(평문과 같음)

권장 설정(OWASP)

OWASP Password Storage Cheat Sheet의 최소 설정입니다. 이를 하한으로 보고 서버에 맞춰 올리세요.

알고리즘최소 설정쓰는 경우
Argon2id메모리 19 MiB, 반복 2회, 병렬도 1새 시스템의 1순위
scryptN=2^17(128 MiB), r=8, p=1Argon2id를 쓸 수 없을 때
bcrypt비용 10 이상. 입력은 72바이트까지기존 시스템, 오랜 실적
PBKDF2-HMAC-SHA256반복 600,000회 이상FIPS 140 준수가 필요할 때

플랫폼의 내장 기능도 확인하세요. PHP의 password_hash()는 기본으로 bcrypt(PASSWORD_DEFAULT)를 쓰며, PHP 8.4에서 기본 비용이 10에서 12로 올라갔습니다. Django는 기본으로 PBKDF2를 쓰며, django[argon2]를 설치하고 PASSWORD_HASHERS의 맨 앞에 Argon2를 두면 바뀝니다. Django는 이전 알고리즘으로 저장된 비밀번호를 사용자가 로그인할 때 자동으로 재해싱합니다.

내 서비스를 확인하는 법

저장된 해시 문자열의 앞부분을 보면 대개 방식을 알 수 있습니다. 운영 데이터를 다른 곳에 복사하지 말고, 관리하는 환경 안에서 몇 행만 보세요.

해시의 모양방식조치
$argon2id$v=19$m=…,t=…,p=…$Argon2idm, t, p가 OWASP 최소치를 충족하는지 확인
$2y$12$… / $2b$10$…bcrypt(숫자가 비용)10 미만이면 올린다
pbkdf2_sha256$1000000$…PBKDF2(숫자가 반복 횟수)600,000 이상인지 확인
16진수 32글자MD5일 가능성이 높음로그인 시 재해싱으로 이전
16진수 40 / 64글자SHA-1 / SHA-256일 가능성이 높음위와 같음
로그인 문자열 그대로평문가장 먼저 이전. 유출됐을 때의 영향을 평가

다음도 확인하세요.

  • 입력된 비밀번호가 앱이나 웹 서버의 로그, 오류 보고, 분석 도구에 기록되지 않는다
  • bcrypt를 쓴다면 72바이트를 넘는 입력을 어떻게 다룰지 정해 두었다
  • 백업이나 테스트용으로 만든 데이터베이스 사본에도 같은 종류의 해시만 들어 있다

실제 사례: 평문 저장이 피해를 키웠다

Takufile-bin 사건(2019년)에서는 서버에 대한 무단 접근으로 약 480만 건의 정보가 유출됐고, 그중 로그인 비밀번호는 평문으로 저장되어 있었습니다. 솔트를 쓴 느린 해시로 저장했다면, 유출된 비밀번호가 다른 서비스에서 그대로 쓰일 위험은 훨씬 낮았을 것입니다. 모든 침입을 막을 수는 없더라도, 비밀번호를 저장하는 방식이 유출 피해의 크기를 바꿉니다.

본 사이트의 견해: 비밀번호 저장에는 검증된 표준 구현을 쓰라

비밀번호 저장은 창의력을 발휘할 자리가 아닙니다. Argon2/bcrypt의 널리 검증된 표준 구현을 쓰는 것이 가장 안전하고 운영도 가장 쉽습니다. 우리의 입장: 여기서는 혁신하지 마세요. 그 노력은 대신 비밀번호 의존 자체를 줄이는 데 쓰세요 — 철저한 다단계 인증(MFA), 그리고 결국에는 패스키(패스워드리스)로의 이동입니다. 아무리 강한 해시도 약하고 재사용된 비밀번호를 구할 수는 없습니다.

다음으로 읽기

출처

FAQ

Q저장용으로 비밀번호를 그냥 암호화하면 안 되나요?
A

안 됩니다 — 암호화는 잘못된 도구입니다. 암호화는 키로 되돌릴 수 있어서, 키가 새면 모든 비밀번호가 평문으로 돌아옵니다. 비밀번호는 본인을 포함해 누구도 되읽을 필요가 없으므로, 되돌릴 수 없는 '해시'가 정답입니다. 다만 생 해시만으로는 부족합니다; 사용자별 솔트와 느린 해시(bcrypt/Argon2)를 결합하세요.

Qbcrypt와 Argon2 중 무엇을 써야 하나요?
A

신규 개발이라면 Argon2(특히 Argon2id)가 1순위입니다: 메모리와 연산을 모두 튜닝할 수 있어 GPU 무차별 대입에 잘 저항합니다. 오랜 검증을 거친 구현을 중시한다면 bcrypt도 여전히 충분히 실용적입니다. 어느 쪽이든 핵심은 표준 라이브러리 구현을 그대로 쓰고 직접 만들지 않는 것입니다.

Q이미 MD5/SHA-256으로 비밀번호를 저장했습니다. 어떻게 이전하나요?
A

평문으로 일괄 되돌릴 수 없으므로(그게 핵심입니다) 로그인 시 이전하세요. 사용자가 로그인에 성공하는 순간, 방금 입력한 올바른 비밀번호를 새 방식(예: Argon2id)으로 재해싱해 저장하세요. 로그인하지 않은 사용자는 기존 해시를 bcrypt로 한 번 더 해싱해(레이어드 해시) 그동안 보호를 강화할 수 있습니다.

Q솔트와 페퍼는 무엇이 다른가요?
A

솔트는 사용자마다 다른 무작위 값으로, 해시와 함께 데이터베이스에 저장됩니다. 비밀로 할 필요는 없습니다. 페퍼는 모든 사용자가 공유하는 하나의 비밀 값으로, 데이터베이스가 아닌 곳(시크릿 관리 서비스나 HSM 등)에 둡니다. OWASP는 페퍼를 데이터베이스만 유출됐을 때를 위한 추가 방어로 설명합니다. 솔트는 필수이고, 페퍼는 선택적인 추가 수단입니다.

Qbcrypt의 72바이트 제한이란 무엇인가요?
A

bcrypt는 입력의 앞 72바이트만 사용하며, 72바이트를 넘는 부분은 무시됩니다. 멀티바이트 문자(한글이나 이모지 등)는 한 글자에 3~4바이트 정도를 차지하므로 생각보다 빨리 한도에 이릅니다. 새 시스템이라면 이런 제한이 없는 Argon2id를 고르고, bcrypt를 쓴다면 입력의 최대 길이를 정하세요(OWASP).

Q솔트는 비밀로 해야 하나요?
A

아닙니다. 솔트의 역할은 같은 비밀번호라도 사용자마다 다른 해시가 되게 해서, 사전 계산 테이블과 일괄 크래킹을 무력화하는 것입니다. 비밀일 필요가 없으며, bcrypt와 Argon2는 솔트를 해시 문자열 안에 저장합니다. NIST SP 800-63B-4는 솔트를 32비트 이상으로 요구합니다.