웹 서비스를 개발하며 회원 시스템을 구축할 때 사용자의 비밀번호를 md5나 sha256으로 단순 해싱해서 DB에 저장해 본 경험이 있을까? 다만 대다수 개발자가 솔트(Salt)의 정확한 개념이나 Bcrypt 알고리즘의 필요성을 모른 채 인터넷 블로그의 구형 코드를 그대로 복사해 붙이는 경우가 많다. 이번에는 PHP에서 제공하는 password_hash()와 password_verify()가 정확히 무엇이고, 왜 필수적인지, 그리고 실무 회원가입 및 로그인에 어떻게 안전하게 적용하는지 완벽하게 정리해서 소개하겠다.

 

1. 왜 md5나 sha256을 단독으로 사용하면 안 될까?

과거에는 md5나 sha1, sha256 같은 단방향 해시 함수를 이용해 비밀번호를 암호화하곤 했다. 하지만 현대 컴퓨팅 환경에서 이러한 전통적인 해시 함수는 비밀번호 보안용으로 절대 안전하지 않다. GPU를 활용하면 초당 수십억 개의 해시를 비교할 수 있어 레인보우 테이블(Rainbow Table, 미리 계산된 해시 값 데이터베이스) 공격에 무기력하게 무너진다.

이를 방어하기 위해서는 입력할 때마다 매번 달라지는 솔트(Salt)를 자동으로 생성하고, 해싱 연산을 여러 번 반복하여 의도적으로 속도를 늦추는 키 스트레칭(Key Stretching) 기법이 필요하다. PHP의 password_hash()는 이러한 처리를 내부적으로 알아서 수행해 준다.

 

구분md5 / sha256 (단독)password_hash() (Bcrypt / Argon2)
솔트(Salt) 처리개발자가 직접 구현하거나 누락함함수가 난수 솔트를 자동 생성해 해시에 포함함
연산 속도매우 빠름 (brute-force 공격에 취약)의도적으로 연산 비용(Cost)을 부여하여 속도를 제어함
동일 비밀번호 결과항상 동일한 해시 값 출력매번 서로 다른 해시 값 출력 (레인보우 테이블 무력화)
권장 여부절대 사용 금지 (보안 취약)권장 (PHP 표준 보안 함수)

 

2. password_hash()와 password_verify() 함수 기초

PHP 5.5부터 기본 탑재된 이 함수들은 복잡한 암호화 알고리즘의 세부 구현을 모르는 개발자도 안전하게 비밀번호를 처리할 수 있게 해준다.

password_hash() 함수는 비밀번호 문자열을 전달받아 암호화된 해시 문자열을 반환한다. 두 번째 인자에는 사용할 알고리즘을 지정하는데, 기본적으로 PASSWORD_DEFAULT(현재 기준 Bcrypt)를 사용한다.

password_verify() 함수는 사용자가 입력한 평문 비밀번호와 DB에 저장된 해시 문자열을 비교하여 일치 여부를 boolean(true/false) 값으로 반환한다.

 

3. 실전 회원가입 및 로그인 구현 예제

실제 회원가입 처리 로직과 로그인 검증 로직에서 기존의 잘못된 구현과 올바른 표준 구현을 각각 비교해 보자.

 

회원가입 시 비밀번호 암호화

✗ 잘못된 코드: md5 함수를 사용하여 비밀번호를 저장하는 경우

<?php
// 잘못된 예시: md5는 솔트가 없어 동일한 비밀번호는 동일한 해시가 됨
$userPassword = $_POST['password'];
$hashedPassword = md5($userPassword);

$stmt = $pdo->prepare("INSERT INTO users (email, password) VALUES (?, ?)");
$stmt->execute([$email, $hashedPassword]);
?>

✓ 올바른 코드: password_hash()를 사용하여 자동 솔팅 및 Bcrypt 적용

<?php
// 올바른 예시: PASSWORD_DEFAULT를 통해 안전하게 암호화
$userPassword = $_POST['password'];
$hashedPassword = password_hash($userPassword, PASSWORD_DEFAULT);

if ($hashedPassword === false) {
    throw new Exception("비밀번호 암호화 실패");
}

$stmt = $pdo->prepare("INSERT INTO users (email, password) VALUES (?, ?)");
$stmt->execute([$email, $hashedPassword]);
?>

결과 해시 예시:

$2y$10$e8R3zK4mQ9X1Y2Z3A4B5C6D7E8F9G0H1I2J3K4L5M6N7O8P9Q0R1S

출력된 해시 문자열을 보면 알고리즘 식별자($2y$), 연산 비용($10$), 그리고 난수 솔트와 암호화된 비밀번호가 결합되어 있음을 볼 수 있다.

 

로그인 시 비밀번호 검증

✗ 잘못된 코드: 단순 문자열 비교나 md5 재해싱 후 비교

<?php
// 잘못된 예시: 해시값을 직접 비교하려 함
$userPassword = $_POST['password'];
$stmt = $pdo->prepare("SELECT password FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();

if ($user && $user['password'] === md5($userPassword)) {
    echo "로그인 성공";
} else {
    echo "비밀번호가 일치하지 않습니다.";
}
?>

✓ 올바른 코드: password_verify() 함수를 사용한 안전한 검증

<?php
// 올바른 예시: 저장된 해시 내부에 포함된 솔트를 파싱하여 안전하게 검증
$userPassword = $_POST['password'];
$stmt = $pdo->prepare("SELECT id, password FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();

if ($user && password_verify($userPassword, $user['password'])) {
    // 로그인 성공 처리
    $_SESSION['user_id'] = $user['id'];
    echo "로그인 성공";
} else {
    // 로그인 실패 처리 (보안상 아이디가 틀렸는지 비밀번호가 틀렸는지 숨김)
    echo "이메일 또는 비밀번호가 올바르지 않습니다.";
}
?>

 

4. 실무에서 자주 하는 실수와 주의사항

password_hash()를 도입할 때 가장 많이 범하는 실무적 에러와 주의사항 2가지를 체크해야 한다.

첫째, 데이터베이스 컬럼의 가변 길이(VARCHAR) 설정 오류다. password_hash()가 생성하는 해시 값은 최소 60자 이상이며, 향후 PHP 버전이 올라가면서 더 강력한 알고리즘(Argon2id 등)이 기본값으로 지정될 경우 길이 문자열이 더 늘어날 수 있다. 따라서 DB 테이블 설계 시 password 컬럼의 길이는 최소 VARCHAR(255)로 여유 있게 생성해야 해시 문자열이 잘려서 로그인에 실패하는 사고를 방지할 수 있다.

둘째, password_needs_rehash() 함수의 존재를 활용하는 것이다. 보안 정책에 따라 연산 비용(Cost)을 올리거나 기본 알고리즘이 변경되었을 때, 사용자가 로그인하는 시점에 구형 해시를 최신 해시로 자동 재암호화해 주는 유용한 함수다.

<?php
// 로그인 성공 후 실행하는 해시 업데이트 로직
if (password_verify($userPassword, $user['password'])) {
    // 현재 설정된 알고리즘이나 cost 기준에 맞지 않는지 확인
    if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
        $newHash = password_hash($userPassword, PASSWORD_DEFAULT);
        $updateStmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
        $updateStmt->execute([$newHash, $user['id']]);
    }
    // 세션 생성 등 정상 진행
}
?>

 

5. 마무리하며

회원 정보 보안은 백엔드 개발자가 가장 기본적으로 갖춰야 할 중요 책임이다. 검증되지 않은 오래된 해시 함수나 직접 구현한 허술한 암호화 알고리즘 대신, PHP 표준 함수인 password_hash()와 password_verify()를 사용하는 것이 가장 안전한 길이다. 이 글의 회원가입과 로그인 코드를 참고해 기존 암호화 방식을 점검하고 수정하면, 서비스의 회원 데이터 보안을 한 단계 끌어올릴 수 있을 것이다.