보안

패스워드 해싱 - bcrypt와 argon2, 뭘 선택해야 할까

jmineekim 2026. 6. 24. 10:00

신입 때 처음 짠 회원가입 코드를 떠올리면 지금도 식은땀이 납니다. 패스워드를 그냥 hashlib.sha256(password) 한 줄로 해시해서 DB에 박아 넣었거든요. 당시엔 "암호화했으니 안전하지 않나?"라고 생각했는데, 사실 SHA-256만 쓰는 건 그냥 빠른 해시일 뿐 패스워드 보호 용도로는 매우 위험합니다. 오늘은 왜 일반 해시가 안 되는지, 그리고 bcrypt와 argon2 중에 뭘 골라야 할지 정리해 보겠습니다.

개념 / 원리

SHA-256은 빠릅니다. 빠른 게 평소엔 장점이지만 패스워드 해싱에선 단점입니다. 공격자가 레인보우 테이블이나 GPU 가지고 초당 수십억 번씩 시도할 수 있거든요. 8자리 패스워드는 GPU 한 대로 몇 시간 안에 다 깨집니다.

그래서 패스워드 해싱 전용 알고리즘이 따로 있습니다. 핵심 아이디어는 두 가지예요.

  • 솔트(Salt): 사용자마다 다른 랜덤 값을 섞어서 해시. 같은 패스워드도 결과가 다 다르게 나옵니다. 레인보우 테이블 무력화.
  • 느리게 만들기(Work Factor): 의도적으로 수천 번 반복해서 한 번 해시하는 데 0.1~1초가 걸리게 합니다. 정상 로그인엔 영향 없지만 무차별 대입은 사실상 불가능해집니다.

bcrypt는 1999년 발표된 알고리즘으로, 지난 20년 넘게 표준처럼 쓰여 왔습니다. argon2는 2015년 패스워드 해싱 경연(PHC) 우승작이고, 메모리 사용량까지 조절할 수 있어서 GPU·ASIC 공격에 더 강합니다. OWASP는 현재 신규 시스템에는 argon2id를 1순위로 권장하고, 환경 제약이 있으면 bcrypt를 차선으로 추천합니다.

실전 예제

Python 기준으로 두 가지 다 써보겠습니다.

pip install bcrypt argon2-cffi

bcrypt부터.

import bcrypt

password = "MySecret123!".encode("utf-8")

# 해싱 (회원가입 시)
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed)
# b'$2b$12$abc...xyz'

# 검증 (로그인 시)
if bcrypt.checkpw(password, hashed):
    print("로그인 성공")
else:
    print("패스워드 불일치")

gensalt(rounds=12)의 12가 work factor입니다. 1 증가할 때마다 처리 시간이 2배씩 늘어납니다. 일반적으로 1012 사이를 권장하지만, 서버 성능과 응답 시간을 보고 조정하세요. 한 번 해시하는 데 0.10.5초 정도가 적당합니다.

argon2도 거의 비슷합니다.

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()  # 기본값으로도 충분히 안전

# 해싱
hashed = ph.hash("MySecret123!")
print(hashed)
# $argon2id$v=19$m=65536,t=3,p=4$...$...

# 검증
try:
    ph.verify(hashed, "MySecret123!")
    print("로그인 성공")
except VerifyMismatchError:
    print("패스워드 불일치")

해시 문자열에 m=65536,t=3,p=4라고 파라미터가 같이 박혀 있는 게 보이실 거예요. 각각 메모리(KB), 반복 횟수, 병렬 처리 스레드 수입니다. 나중에 파라미터를 강화해도 기존 해시 검증이 그대로 가능합니다.

DB에는 해시 결과 문자열 전체를 그냥 저장하면 됩니다. 솔트가 안에 포함되어 있어서 별도 컬럼으로 관리할 필요 없어요.

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

주의할 점

가장 흔한 실수는 솔트를 직접 만들어서 따로 저장하는 겁니다. bcrypt도 argon2도 솔트 생성과 저장을 다 알아서 해주는데, 옛날 글 따라하다가 굳이 손으로 처리하는 경우가 있어요. 라이브러리가 주는 해시 문자열 한 통을 그대로 컬럼에 저장하면 됩니다.

두 번째 실수는 work factor를 너무 낮게 잡는 것입니다. bcrypt rounds 4 같은 값은 거의 효과가 없습니다. 신규 서비스라면 bcrypt는 12 이상, argon2는 기본값 이상으로 잡으세요. 시간이 지나면 하드웨어 발전에 맞춰 올려야 합니다.

세 번째는 기존 시스템에서 알고리즘 변경하는 법입니다. 한꺼번에 다 바꿀 수는 없으니, 사용자가 로그인할 때마다 옛 해시로 검증 → 성공하면 새 알고리즘으로 다시 해시해서 덮어쓰는 방식이 표준입니다. argon2 라이브러리의 ph.check_needs_rehash(hashed)가 이 패턴을 깔끔하게 지원해요.

마지막으로, 패스워드 길이 제한도 함정입니다. bcrypt는 입력을 내부적으로 72바이트까지만 봅니다. 그 이상은 무시되니까 매우 긴 패스워드도 앞부분만 보는 셈입니다. argon2엔 이 제한이 없어요. 다국어(한글 등) 패스워드는 UTF-8 인코딩 시 글자당 3바이트씩 먹기 때문에 bcrypt에선 24자 정도가 한계입니다.

마무리

정리하면 세 줄입니다.

  • 일반 해시(SHA, MD5)는 패스워드 보호용으로 쓰면 안 된다. 너무 빠르다.
  • 신규 시스템은 argon2id, 환경 제약이 있으면 bcrypt를 쓴다.
  • 솔트와 work factor는 라이브러리가 해주는 대로 두되, work factor는 시간 지나면 올려야 한다.
반응형