해시 함수란 무엇인가: 단방향 변환의 쓰임새
비밀번호를 다루거나, 파일을 다운로드한 뒤 무결성을 검사하거나, JWT 서명을 살펴볼 때 어김없이 등장하는 친구가 있습니다. 바로 해시 함수입니다. SHA-256이나 bcrypt 같은 이름은 익숙한데, 막상 “해시는 정확히 무엇이고 어디까지 보장하나요”라고 물으면 답이 잘 안 나오는 경우가 많죠.
이번 글에서는 해시 함수가 어떤 약속을 지키는지, 흔히 쓰이는 알고리즘들이 어떻게 다른지, 그리고 실무에서 자주 만나는 쓰임새를 한 번에 정리해 보겠습니다.
해시 함수가 지키는 약속
해시 함수는 임의의 길이를 가진 데이터를 입력으로 받아, 정해진 길이의 짧은 출력으로 변환하는 함수입니다. 이 출력을 해시값(hash value), 다이제스트(digest), 또는 줄여서 해시라고 부릅니다. 얼핏 보면 그냥 압축처럼 들리지만, 암호학적 해시 함수는 몇 가지 약속을 더 지켜야 합니다.
첫째는 결정성입니다. 같은 입력에는 항상 같은 출력을 돌려준다는 뜻이라, 한 시간 뒤에 같은 파일을 해시해도 결과가 동일하죠. 둘째는 단방향성입니다. 출력값만 보고 원래 입력을 역으로 알아낼 수 없도록 설계되어 있습니다. 셋째는 충돌 저항성입니다. 서로 다른 두 입력이 같은 출력을 내는 일을 의도적으로 만들기가 매우 어려워야 합니다.
"hello world"
│
▼ SHA-256
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
이 셋이 깨지면 해시 함수가 약속하는 모든 보안적 쓸모도 같이 무너집니다. 그래서 새 해시 알고리즘이 등장할 때마다 학계는 이 세 성질이 충분히 강한지를 검증하고, 약점이 발견되면 알고리즘이 빠르게 권장 목록에서 빠지게 됩니다.
또 하나 자주 언급되는 성질이 눈사태 효과(avalanche effect)입니다. 입력의 한 비트만 바뀌어도 출력이 거의 절반 가까이 바뀌도록 만드는 성질인데, 이 효과가 충돌 저항성과 단방향성을 떠받치는 토대가 됩니다. 원본 메시지의 작은 변화가 해시에 즉각 반영되니, 해시값을 비교하는 것만으로도 위변조를 잡아낼 수 있는 셈이죠.
암호학적 해시와 일반 해시는 다르다
비슷해 보여서 자주 헷갈리는 것이 자료구조에서 쓰는 해시 함수입니다. HashMap이나 Set 같은 자료구조에도 해시 함수가 들어가지만, 이쪽은 빠른 분포가 목적이라 단방향성이나 충돌 저항성이 강하지 않습니다. 오히려 빠르고 분포만 적당히 고르면 충분하다는 다른 기준이 적용되죠.
자바스크립트의 Object.hashCode나 자바 컬렉션의 해시 코드 같은 것들이 그런 예입니다.
이런 해시는 같은 키를 같은 슬롯에 넣고 다른 키는 다른 슬롯에 흩뿌리는 데에 초점이 있어, 출력이 짧고(보통 32비트 정수) 계산이 매우 빠릅니다.
보안에 쓸 수는 없지만 본래 목적인 빠른 검색에서는 훌륭한 도구이니, “해시”라는 단어가 같다고 해서 같은 자리에 쓸 수 있다고 오해하지 않는 것이 첫걸음입니다.
자주 만나는 알고리즘들
가장 자주 등장하는 이름이 SHA-256입니다. SHA-2 계열의 한 멤버로, 출력 길이가 256비트(64자 16진수)인 알고리즘입니다. 파일 무결성 검사부터 PKCE의 code challenge, 블록체인의 블록 해시까지 광범위하게 쓰이죠. 같은 계열의 SHA-384, SHA-512도 출력 길이만 다르고 비슷한 구조라 보안 요구가 더 강한 자리에 골라 쓰입니다.
조금 더 새로운 알고리즘이 SHA-3입니다. SHA-2와 다른 내부 구조(Keccak)로 만들어져, 한쪽 계열에 약점이 발견되어도 다른 쪽이 살아남도록 다양화 차원에서 표준화되었습니다. 실무에서는 SHA-2가 여전히 압도적으로 많이 쓰이지만, 미래를 대비해 SHA-3를 선택지로 두는 경우도 늘고 있습니다.
옛 친구인 MD5와 SHA-1은 보안 목적으로는 더 이상 권장되지 않습니다. MD5는 충돌이 흔하게 만들어질 정도로 깨졌고, SHA-1도 실제 충돌 공격이 발견되어 서명용으로는 쓰지 않는 게 표준이죠. 다만 두 알고리즘 모두 깨지지 않는 영역(예: Git 객체 식별, 단순 체크섬)에서는 여전히 보일 수 있어, 보안적 약속이 필요한 자리에서만 멀리하면 됩니다.
무결성 검사부터 식별자까지
가장 흔한 쓰임새가 무결성 검사입니다. 파일을 다운로드한 뒤 해시값이 제공된 값과 일치하는지 비교하면, 중간에 변조되지 않았다는 사실을 확인할 수 있죠. 오픈 소스 배포물이나 컨테이너 이미지가 거의 빠짐없이 SHA-256 해시를 함께 공개하는 이유입니다.
shasum -a 256 downloaded-file.tar.gz
# 출력된 값과 배포처가 공개한 값을 비교
식별자로도 자주 쓰입니다. 큰 자산에 대해 짧은 해시를 만들어 두면, 같은 내용인지 비교하는 일이 본문 전체를 비교하는 일보다 훨씬 빠르거든요. Git이 커밋과 객체를 SHA-1 해시로 식별하는 것도 같은 이유고, 캐시 키나 정적 자산 파일명에 콘텐츠 해시를 박아두는 패턴도 흔합니다. Cache-Control 글에서 봤듯, 파일명 해시는 캐시 무효화 전략의 핵심 도구로도 쓰입니다.
디지털 서명에서도 해시가 핵심 역할을 합니다. 큰 메시지에 직접 서명하기는 비싸기 때문에, 메시지의 해시를 먼저 만들고 그 짧은 해시에 서명하는 식인데요. 받는 쪽도 같은 방식으로 해시를 만들어 검증하니, 메시지가 한 글자만 바뀌어도 해시가 완전히 달라져 서명 검증이 실패합니다.
웹에서 자주 보이는 또 다른 응용이 SRI(Subresource Integrity)입니다.
HTML이 외부 CDN에서 자바스크립트나 CSS를 불러올 때, integrity 속성에 해시를 적어 두면 브라우저가 받은 파일의 해시를 비교해 변조 여부를 검증합니다.
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-abc123..."
crossorigin="anonymous"
></script>
CDN 자체가 침해되어도 해시가 안 맞으면 브라우저가 스크립트를 실행하지 않아, 공급망 공격에 대한 한 겹의 방어막이 되어 줍니다.
비밀번호 저장은 다른 이야기
해시 함수의 흔한 오해가 “비밀번호도 SHA-256으로 해시해서 저장하면 된다”는 것입니다. 이게 위험한 이유는 일반 해시 함수가 너무 빠르기 때문이죠. GPU를 동원하면 초당 수십억 개의 SHA-256을 계산할 수 있어서, 흔한 비밀번호의 해시 데이터베이스만 있으면 무차별 대입으로 원본 비밀번호를 빠르게 알아낼 수 있습니다.
이런 공격을 막기 위해 등장한 것이 비밀번호 전용 해시 함수입니다. bcrypt, scrypt, argon2 같은 이름이 여기에 해당하는데요. 의도적으로 계산을 느리게 만들고 메모리도 많이 쓰도록 설계되어 있어, 공격자가 단위 시간당 시도할 수 있는 횟수를 크게 떨어뜨립니다. 또한 비용(cost) 매개변수를 조정할 수 있어, 하드웨어가 빨라져도 같은 알고리즘을 그대로 쓰면서 비용만 올려 보안을 유지할 수 있죠.
소금(salt)도 함께 따라옵니다. 같은 비밀번호라도 사용자마다 다른 소금을 붙여 해시하면, 미리 계산해 둔 무지개 테이블을 무력화할 수 있는데요. 요즘 비밀번호 라이브러리들은 소금 생성과 검증을 모두 알아서 처리해 주니, 그 라이브러리들을 그대로 쓰고 직접 손대지 않는 것이 가장 안전합니다.
비슷한 맥락에서 페퍼(pepper)라는 개념도 가끔 등장합니다. 소금이 데이터베이스에 함께 저장되는 공개 값이라면, 페퍼는 애플리케이션 코드나 별도 저장소에 두는 비밀 값인데요. 데이터베이스만 유출되어도 비밀번호를 풀기 어렵게 만들어 주는 추가 안전망이 되지만, 운영이 까다로워 모든 환경에서 도입하지는 않습니다.
콘텐츠 주소 지정과 머클 트리
해시의 결정성을 응용하는 흥미로운 패턴이 콘텐츠 주소 지정(content addressing)입니다. 파일 자체의 해시를 그 파일의 주소로 쓰는 방식인데, 같은 내용이라면 어디에 있어도 같은 주소가 되는 셈이죠. 중복 저장을 자연스럽게 막아 주고, 주소만 알면 어느 노드에서 받든 위변조를 검증할 수 있어서 IPFS, Git, Docker 이미지 레지스트리 같은 시스템이 이 패턴을 쓰고 있습니다.
여러 데이터의 무결성을 효율적으로 묶는 도구가 머클 트리(Merkle tree)입니다. 데이터를 잘게 나눠 각 조각의 해시를 만들고, 그 해시들을 둘씩 묶어 다시 해시하는 식으로 트리를 쌓아 올리는데요. 트리의 꼭대기에 있는 루트 해시 하나만 있으면, 어느 한 조각이 바뀌어도 루트가 달라져 즉시 알아챌 수 있습니다. 블록체인의 트랜잭션 검증, 분산 파일 시스템의 동기화, Git의 트리 객체 같은 자리에 머클 트리가 자리 잡고 있죠.
HMAC: 메시지 인증
해시 함수에 비밀 키를 결합해 만드는 도구가 HMAC(Hash-based Message Authentication Code)입니다. 같은 메시지라도 비밀 키가 다르면 다른 출력을 내고, 받는 쪽이 같은 키로 같은 결과를 만들 수 있는지를 확인해 메시지가 변조되지 않았는지를 검증하죠.
HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))
JWT의 HS256 알고리즘이 정확히 이 방식이고, 쿠키에 서명을 붙이거나 API 요청에 신원을 증명할 때도 자주 등장합니다. 공개 키 기반 서명보다 가볍고 빠르지만, 양쪽이 같은 비밀 키를 공유해야 하므로 키 관리가 다른 이슈가 됩니다. 이 키 관리 문제는 다음에 다룰 대칭키 암호화 글에서 깊이 살펴보게 됩니다.
어떤 해시를 골라야 할까
선택지가 많아 처음에는 헷갈리지만, 자리가 정해지면 후보가 거의 한두 개로 좁혀집니다.
일반적인 무결성 검사, 식별자, 캐시 키, 머클 트리에는 SHA-256이 기본 선택지입니다. 출력 길이가 충분하고 거의 모든 언어와 라이브러리가 표준으로 제공하니, 특별한 이유가 없다면 SHA-256을 그대로 쓰는 것이 안전한 출발점이죠. 더 강한 보안이 요구되거나 출력 길이가 더 필요한 경우에만 SHA-384 / SHA-512를 골라 갑니다.
비밀번호 저장에는 일반 해시가 아니라 bcrypt, scrypt, argon2 가운데 하나를 골라야 합니다. 새로 시작한다면 argon2id가 현재 가장 강한 선택지로 추천되고, 기존 시스템과의 호환성을 고려해야 한다면 bcrypt가 무난합니다. 어느 쪽을 고르든 비용 매개변수를 의식하고, 시간이 흐르면서 하드웨어가 빨라지면 비용을 한 단계씩 올려가는 운영이 함께 가야 합니다.
메시지 인증에는 HMAC-SHA-256이 사실상 표준입니다. JWT의 HS256, AWS의 요청 서명, OAuth의 일부 흐름이 모두 이 조합을 쓰니, 한 번 익히면 여러 자리에서 그대로 활용할 수 있습니다.
마치며
해시 함수는 단순해 보이지만 무결성 검사, 식별, 디지털 서명, 비밀번호 저장까지 굉장히 넓은 자리에 자리잡고 있습니다. “같은 입력은 같은 출력, 출력에서 입력은 못 찾는다”는 두 줄짜리 약속만 손에 익으면, 보안 관련 코드를 읽을 때 어떤 자리에 어떤 해시가 어울리는지를 가늠할 수 있게 되는데요. 다만 비밀번호처럼 특별한 조건이 있는 자리에서는 일반 해시 대신 전용 알고리즘을 쓰는 것이 출발점이라는 점만 기억해두면 좋습니다.
대칭키와 비대칭키 같은 다른 암호 도구가 궁금하다면 이어지는 글에서 함께 다룰 예정입니다. 해시 함수의 더 깊은 이론이 궁금하다면 NIST의 해시 함수 페이지를 출발점으로 추천합니다.
This work is licensed under
CC BY 4.0