패스워드는 어째서 암호화를 사용하지 않고 해시를 사용하는 걸까?
- 개발
- 2026. 7. 19. 21:48
로그인 기능을 만들기 위해서는 아이디와 패스워드가 필요합니다. 아이디야 평문으로 해도 상관없지만, 패스워드는 보안상 평문으로 저장하면 안 됩니다. 그렇다면 평문을 다른 사람이 알아볼 수 없게 만들어야 합니다. 이를 위해 암호화를 사용하거나, 해시 함수를 이용해 평문을 해시값으로 변경합니다. 그리고 로그인 시 동일한 해시값이라면 로그인이 성공하고, 다르다면 실패합니다. 일반적으로 해시를 많이 이용하지만, 해시에는 치명적인 문제가 있습니다. 바로 중복 문제입니다. 즉, 서로 다른 평문에 해시 함수를 적용한다고 해서 그 결과가 서로 다르다는 보장은 할 수 없습니다. 이렇게 되면 해시 결과값만 같다면 로그인이 된다는 것인데, 보안상 나쁜 게 아닐까요?
그렇다면 어째서 로그인에는 해시를 사용하는 걸까요? 암호화를 사용한다면 이 문제를 해결할 수 있을까요? 암호화 방식은 어째서 로그인에 사용하지 않는 걸까요?
로그인 기능에서 암호화를 사용하면 안 되는 이유
암호화를 하기 위해서는 키가 필요합니다. 즉, 키를 이용해 평문의 내용을 변형하는 것이라고 해석할 수 있습니다. 반면에 해시는 키가 필요하지 않습니다. 입력값과 알고리즘만 있으면 됩니다.

로그인 기능에서 가장 중요한 것은 보안성입니다. 만약 해커에게 키가 탈취된다면 어떻게 될까요? 해커는 탈취한 키로 암호문을 그대로 복호화할 수 있습니다. 전 사용자의 비밀번호가 한순간에 평문으로 복원되는 것입니다. 그렇다면 해시는 어떨까요? 해시는 위에서 말한 것처럼 키가 존재하지 않습니다. 여기서 한 가지 의문이 생깁니다. "해커가 알고리즘까지 알고 있다면, 오히려 보안성이 떨어지는 게 아닐까?"
해커가 알고리즘까지 알고 있다면, 오히려 보안성이 떨어지는 게 아닐까?
결론부터 말씀드리자면, 보안성은 떨어지지 않습니다. 애초에 해시 알고리즘은 비밀이었던 적이 없습니다. 알고리즘이 공개되어 있을 때, 해커가 시도할 수 있는 방법은 두 가지뿐입니다.
1. 탈취한 해시값에서 거꾸로 평문을 계산해내는 방법 (역산)
2. 평문 후보를 하나씩 대입해 같은 해시값이 나오는지 확인하는 방법 (브루트 포스)
탈취한 해시값에서 거꾸로 평문을 계산해내는 방법
1번부터 살펴보겠습니다. 이때 해커에게 필요한 것은 해시값에서 평문으로 바꾸는 방법입니다. 하지만 해시 알고리즘은 단방향으로 설계되어 있습니다. 이는 앞으로 가는 길이 공개되어도, 되돌아오는 길 자체가 존재하지 않는다는 뜻입니다. 어떻게 보면 길을 완벽하게 알고 있어도 일방통행인 길은 거슬러 올 수 없는 것과 같습니다.

예를 들어, "입력을 10으로 나눈 나머지"라는 알고리즘이 전 세계에 공개되었다고 해봅시다.
입력값 73을 넣으면 3이 나옵니다. 하지만 반대로 3에서 출발하면, 입력값이 73이었다고 확신할 수 없습니다. 3인지, 13인지, 23인지, 73인지 알 수 없습니다. 정답이 하나로 정해지지 않는다는 뜻입니다. 73이 3이 되는 순간, 십의 자리 이상이 7이었다는 정보는 사라져 버렸습니다. 무한한 입력이 유한한 출력으로 접히니, 출력 하나에 입력이 무한히 겹쳐 있고, 그중 어느 것이었는지는 아무도 모릅니다.
진짜 해시 함수는 이 과정을 수십 번 반복합니다. SHA-256은 "섞고, 자르고, 겹쳐 접는" 단계를 64라운드 거치는데, 각 라운드가 정보를 조금씩 버립니다. 한 번 버릴 때마다 거꾸로 갈 때의 후보가 곱절로 불어나고, 64라운드가 끝나면 후보가 천문학적으로 폭발합니다. 결국 "거슬러 올라가기"는 사실상 "모든 입력을 다 넣어보기", 즉 브루트 포스와 같은 일이 됩니다.
근데, 로그인이라는건 해시값만 똑같으면 로그인이 성공하는거 아닌가요?
입력값이 73이든 103이든, x % 10 알고리즘이라면 해시값은 같습니다. 그렇다면 패스워드를 무엇으로 만들었든 상관없이, 해시값만 같다면 로그인이 되는 걸까요?
놀랍게도 답은 "그렇습니다."입니다. 서버는 평문을 저장하지 않기 때문에, 로그인 시 비교할 수 있는 것은 해시값뿐입니다. 같은 해시값을 만들어내는 입력이라면, 그것이 진짜 패스워드든 아니든 서버는 구분할 방법이 없습니다. 위의 예시로 보면, 진짜 패스워드가 73이어도 3을 입력한 사람도, 103을 입력한 사람도 전부 로그인에 성공합니다.

그리고 이 걱정은 실제로 현실이 된 적이 있습니다. 2012년 발견된 Flame 악성코드는 MD5 해시 함수의 충돌을 이용해 마이크로소프트의 정품 서명을 위조했습니다. 2017년에는 구글이 내용이 서로 다른 두 PDF 파일로 같은 SHA-1 해시값을 만들어내는 데 성공했습니다. 충돌이 "찾아지는" 순간, 그 해시 함수 위에 세워진 신뢰는 무너집니다.
그렇다면 우리는 어떻게 안심하고 해시를 쓰는 걸까요? 보안용 해시 함수의 자격 요건은 "충돌이 존재하지 않을 것"이 아닙니다. 그것은 애초에 불가능합니다. 진짜 요건은 "충돌을 찾는 비용이 감당할 수 없이 클 것"입니다. x % 10처럼 출력이 10가지뿐이라면 충돌은 몇 번의 시도로 찾을 수 있습니다. 하지만 SHA-256의 출력은 2^256가지입니다. 특정 해시값과 같은 값이 나오는 다른 입력을 찾으려면 평균 2^256번을 시도해야 하는데, 지구상의 모든 컴퓨터를 우주의 나이만큼 돌려도 턱없이 부족한 횟수입니다. 충돌은 존재하지만, 도달할 수 없습니다. 그리고 존재하지만 도달할 수 없다면, 실질적으로는 없는 것과 같습니다.

이 요건을 잃은 함수는 실제로 퇴출됩니다. MD5와 SHA-1이 바로 그렇게 은퇴한 함수들입니다. 즉 "충돌하면 뚫리는 것 아닌가?"라는 걱정은 기우가 아니라, 암호학계가 해시 함수를 심사하고 은퇴시키는 공식 기준 그 자체입니다.
평문 후보를 하나씩 대입해 같은 해시값이 나오는지 확인하는 방법
1번 방법이 왜 불가능한지 살펴보았습니다. 그렇다면 남은 길은 2번, 하나씩 대입해보는 것뿐입니다. 정말 하나씩 넣다 보면 언젠가는 뚫리는 게 아닐까요?
패스워드를 5회 이상 틀렸을 경우 계정을 잠그는 식으로 가능성 자체를 줄이는 방법이 있긴 하지만, 이런 방어 장치는 없다고 가정하겠습니다.
네, 뚫립니다. 계속 공격을 당하다 보면 언젠가는 뚫리게 됩니다. 그렇다면 방법이 없을까요? 두 손 다 들고 가만히 있어야 할까요? 그렇지 않습니다. 해시를 계산하는 속도를 일부러 낮추는 방법으로 이를 완화할 수 있습니다. 그 주인공이 바로 bcrypt 같은 "느린 해시"입니다.
어째서 일부러 속도를 낮추는 방법이 효과가 좋을까요?
느린 해시의 효과는 해시 1회의 비용이 누구에게나 똑같이 부과된다는 데에서 시작합니다. SHA-256은 워낙 빨라서 GPU로 초당 약 100억 회를 계산할 수 있습니다. 영문 소문자 8자리의 후보 2,000억 개를 전부 대입해도 약 20초면 끝난다는 뜻입니다. 반면 느린 해시인 bcrypt는 1회에 약 0.1초가 걸리도록 설계되어 있어서, 같은 2,000억 개를 계산하면 약 660년이 걸립니다.

그런데 이 0.1초는 정상 사용자에게도 똑같이 부과됩니다. 다만 사용자는 로그인할 때 딱 1번만 계산하므로 총비용도 0.1초로 체감조차 되지 않습니다. 같은 요금인데 1번 내는 사람과 2,000억 번 내야 하는 사람의 차이. 이것이 느린 해시가 사용자는 건드리지 않으면서 공격자만 정밀하게 타격하는 이유입니다.
단순하게 생각하면, 이 과정은 평문을 4,096번 뒤섞기만 할 뿐 아무것도 하지 않는 것과 같습니다. 이를 다른 방향으로 생각해봅시다. 요리에 비유하면 평문은 원재료, 해시값은 완성된 요리입니다. SHA-256은 즉석요리입니다. 재료를 넣으면 그 자리에서 바로 나옵니다. 반면 bcrypt는 일부러 4,096번 반죽해야만 완성되도록 레시피를 짜놓은 요리입니다. 그리고 이 조리 시간의 의미는 손님이 누구냐에 따라 완전히 달라집니다.

로그인하는 사용자는 한 그릇만 주문하는 손님입니다. 조리에 0.1초가 걸려도 아무 불편이 없습니다. 하지만 공격자는 다릅니다. 공격자가 원하는 것은 훔친 요리(해시값)와 똑같은 결과가 나오는 원재료(평문)를 찾아내는 것입니다. 레시피는 공개되어 있지만 재료는 조리해보기 전까지 알 수 없으니, 방법은 하나뿐입니다. 후보 재료를 하나씩 조리해서 같은 요리가 나오는지 확인하는 것 최대 2,000억 그릇을 주문하는 일입니다. 그릇당 0.1초짜리 레시피 앞에서 이 주문서는 660년짜리가 될지도 모릅니다.
지금까지 해커의 공격을 어떻게 막는지 이야기했습니다. 역산은 되돌아오는 길 자체가 존재하지 않는 단방향성이 막아주었고, 브루트 포스는 의도적으로 가격을 올려 대량 주문을 파산시키는 방법으로 막았습니다. 그렇다면 패스워드는 bcrypt로 해시값만 만들면 끝일까요? 아쉽게도 아직 해결하지 못한 문제가 하나 남아 있습니다. 바로 중복 문제입니다.
다만 앞에서 본 충돌과는 다릅니다. 서로 다른 평문이 우연히 같은 해시값이 되는 일은 우주적으로 어렵다고 했습니다. 남은 중복은 정반대로, 100% 확실하게 일어납니다. 같은 비밀번호는 언제나 같은 해시값이 되기 때문입니다(결정성: 처음에 배운 그 성질이, 여기서는 부메랑이 되어 돌아옵니다). "1234"를 쓰는 사용자가 1만 명이라면 DB에는 똑같은 해시값이 1만 개 쌓입니다. 이 중복은 왜 위험하고, 어떻게 없앨 수 있을까요?

이와 같은 중복을 제거하기 위해 솔트(salt) 를 사용할 수 있습니다. 솔트는 사용자마다 새로 생성하는 랜덤 값입니다. 회원가입 시 난수를 하나 뽑아(보통 16바이트 이상) 비밀번호에 붙인 뒤 함께 해시합니다. 그렇다면 솔트는 어떤 목적으로 쓰일까요? 같은 비밀번호라도 사용자마다 전혀 다른 해시값이 나오게 만들어서, 유출된 해시값들을 서로 비교하거나 미리 계산해둔 표와 대조하는 일을 무의미하게 만드는 것입니다.
사용자 A와 사용자 B의 비밀번호가 "1234"로 같다고 해봅시다. 솔트가 없다면 두 사람의 해시값이 완전히 같으므로, A의 비밀번호를 알아내는 순간 B도 함께 뚫립니다. 하지만 솔트가 있다면 같은 "1234"라도 두 사람의 해시값이 서로 달라집니다. 즉 하나가 털려도 다른 하나가 자동으로 함께 뚫리는 일은 없으며, 공격자는 계정마다 공격을 처음부터 다시 해야 합니다. 또한 공격자는 흔한 비밀번호 수억 개의 해시값을 미리 계산해 표로 만들어두는 레인보우 테이블 공격을 쓰기도 하는데, 솔트는 이마저 무용지물로 만듭니다. 표에 들어 있는 것은 hash(비밀번호)인데 실제 저장된 값은 hash(비밀번호 + 솔트)라서, 몇 년을 들여 만든 표가 단 한 줄도 일치하지 않기 때문입니다.

마무리
처음 시작은 "해시를 왜 사용하지?"를 학습하면서 시작되었습니다. 해시는 원문에서 해시값을 O(1)로 빠르게 찾을 수 있다는 특징을 가지고 있습니다. 그런데 로그인할 때는 속도가 무지막지하게 빠를 필요가 없다고 여겼습니다. 그렇다면 어째서 패스워드에 해시를 사용하는지 궁금해졌습니다. 찾다 보니 일반적인 쓰임과 달리, 패스워드에 해시를 사용하는 이유는 속도가 아니라 "되돌릴 수 없다"는 보안적 성질 때문이라는 결론에 도달했습니다. 또한 그 보안을 지키기 위해 어떠한 노력(느리게 만들기, 솔트)이 쌓여왔는지도 알게 되었습니다.
이 글에서 다루지는 않았지만, 솔트 말고 페퍼(pepper)라는 것도 존재한다고 합니다. 솔트와 비슷하게 비밀번호에 섞는 값이지만, DB에 함께 저장되는 솔트와 달리 DB 밖에 비밀로 보관된다고 합니다.
긴글 읽어주셔서 감사합니다.
'개발' 카테고리의 다른 글
| 경쟁조건을 해결하거나 완화하기 위한 다양한 기술들 (0) | 2026.06.29 |
|---|---|
| Rate Limiter (intro.) (0) | 2026.06.28 |
| 로그인은 어떤 방식으로 개발해야 안전할까? (0) | 2026.06.14 |
| 배치 작업시 예외가 발생한다면 어떻게 처리할까? (0) | 2026.05.06 |
| 카프카의 설정은 진짜일까?? - offset(3) (0) | 2026.04.26 |