안녕하세요. 오늘은 아침부터 병원을 갔다오니 오전이 순식간에 지나가네요. 오늘 반깁스를 풀 줄 알았는데, 3주 더 하라네요..통깁스가 아닌게 다행입니다. 한동안 불편한 생활을 하겠지만 저번 한 주처럼 요양하듯 아무것도 못하고 집에 있는것보다는 평상시처럼 나가는게 좋을 것 같아요.
저번 시간에는 웹 보안 지식에 대해 자세한 내용을 들어가기 앞서 웹 보안 지식이 왜 필요한지에 대하여 학습해보았습니다. 이번 시간에는 해시 알고리즘에 대해서 자세히 알아보도록 하겠습니다. 그럼 시작하겠습니다.
해시(Hash)
해시는 입력 데이터를 고정된 길이의 데이터로 변환된 값입니다. 해시 값, 해시 코드, 체크섬이라고도 합니다.
이러한 해시는 같은 입력 값을 넣으면 항상 같은 해시값이 나오지만, 해시값만 보고 원래 입력값을 되돌리는 것은 매우 어렵습니다.

해시는 데이터를 숨기는 것이 아닌 데이터가 같은지 비교하거나 변조 여부를 확인하기 위해 자주 사용됩니다.
암호화와 해시의 차이
암호화는 다시 복호화할 수 있지만, 해시는 원래 값을 되돌리는 것이 아니라 같은 입력인지 비교하는 방식으로 사용됩니다.
| 구분 | 암호화 | 해시 |
| 목적 | 데이터를 알아볼 수 없게 변환 | 데이터를 고정 길이 값으로 변환 |
| 복호화 가능 여부 | 키가 있으면 복호화 가능 | 원칙적으로 복호화 불가 |
| 사용 예시 | HTTPS 통신, 파일 암호화 | 비밀번호 검증, 무결성 확인 |
| 핵심 | 다시 원문으로 되돌릴 수 있음 | 원문으로 되돌릴 수 없음 |
그렇다면 암호화와 해시 둘 다 데이터를 알아볼 수 없거나 값으로 변환하는데 왜 평문이 아닌 값을 저장하는 걸까요?
비밀번호를 평문으로 저장하면 안되는 이유
회원가입을 할 경우를 생각해봅시다
사용자의 비밀번호를 그대로 DB에 저장하면 데이터베이스가 유출되는 순간 사용자의 비밀번호가 그대로 노출됩니다.

보통의 사용자는 여러 서비스에서 같은 비밀번호를 사용하는 경우가 많기 때문에 한 서비스의 비밀번호가 유출되면 다른 서비스의 계정 탈취로 이어질 수 있습니다.
그렇기에 비밀번호를 원문으로 저장하는 것이 아닌 해시 처리된 값을 저장해야 합니다.

해시의 특징
- 입력 길이와 관계없이 고정 길이의 값을 만든다.
- 해시는 입력 데이터가 짧든 길든 항상 정해진 길이의 해시값을 생성합니다.
- 같은 입력은 항상 같은 해시값을 만든다.
- 같은 데이터를 같은 해시 함수에 넣으면 항상 동일한 결과가 나옵니다. 이 특징을 통해 비밀번호 검증이나 파일 무결성 확인이 가능합니다.
- 해시값으로 원문을 되돌릴 수 없다.
- 해시는 암호화와 달리 복호화 과정이 없습니다. 따라서 해시값만 보고 원래 입력값을 복구하는 것은 매우 어렵습니다.
- 입력값이 조금만 달라져도 결과값이 크게 달라진다.
- 입력 데이터가 한 글자만 바뀌어도 해시값은 완전히 달라집니다. 이 특징을 통해 데이터가 변경되었는지 확인할 수 있습니다.
- 충돌이 있을 수 있다.
- 해시는 고정 길이 값을 만들기 때문에 서로 다른 입력의 경우에도 같은 해시값을 갖는 충돌이 발생할 수 있습니다.
- 계산 속도가 빠르다.
- 일반적인 해시 함수는 빠르게 계산되도록 설계되어 있습니다. 무결성 검증에서는 장점이지만, 비밀번호 저장에서는 공격자가 많은 후보를 빠르게 대입할 수 있다는 단점이 될 수 있습니다.
대표적인 해시 알고리즘 종류
MD5
hello → 5D41402ABC4B2A76B9719D911017C592
MD5는 단방향 암호화 알고리즘으로 입력값을 128비트 해시값으로 변환하는 해시 알고리즘입니다. 과거에는 파일 무결성 확인 등에 사용되었지만, 현재는 충돌 공격에 취약하다는 문제가 알려져 보안 목적의 사용은 권장되지 않습니다.
※충돌 : 서로 다른 입력값이 같은 해시값을 가지는 상황
MD5는 빠른 처리 속도를 가지고 있지만 오래된 알고리즘이기에 비밀번호 저장이나 보안 검증 용도로 사용하기에 좋지 않습니다.
SHA 함수군
hello → aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
SHA(Secure Hash Algorithm)는 단방향 암호화 알고리즘으로 가변 크기 입력에 대해서 고정 크기의 해시값을 생성합니다.
또한 여러 함수군이 존재합니다.
| 알고리즘 | 특징 | |
| SHA-1 | 160비트의 해시값을 생성하지만 충돌 취약점으로 인해 보안 목적으로 사용하는 것을 권장하지 않는다. | |
| SHA-2 | SHA-256 | SHA-2 계열로 256비트의 해시값을 생성한다. 무결성 검증 등에 널리 사용된다. |
| SHA-512 | SHA-2 계열로 512비트의 해시값을 생성한다. | |
| SHA-3 | 설명을 더 하면 좋겠는데 2015년에 발표된 함수군으로 차세대야?뭐야? 어디에 주로 이용된다 이런식으로 | |
SHA-256 알고리즘은 여전히 무결성 검증에 널리 사용되지만, 이것만으로 비밀번호를 저장하기에는 적절하지 않습니다.
SHA만으로 비밀번호를 저장하면 안되는 이유
SHA-256은 안전한 해시 함수로 알려졌지만 비밀번호 저장에는 별도의 고려가 필요합니다. 그 이유는 SHA 계열 해시 함수가 매우 빠르게 계산되도록 설계되었기 때문입니다.

공격자가 DB를 가져 갔을 경우
위 그림과 같이 비밀번호 목록을 이용하여 해시 함수로 빠르게 변환하여 DB에 저장된 값을 빠르게 비교하여 탈취될 수 있기에 비밀번호 저장에서는 해시 함수가 빠른 것이 장점이 아닌 단점이 될 수 있습니다.
단방향 해시 함수 문제점
- 인식 가능성
- 단방향 해시 함수는 같은 입력값에 대해 항상 같은 해시값을 생성합니다. 따라서 두 사용자가 같은 비밀번호를 사용하면 DB에 저장되는 해시값도 동일하게 나타납니다.
- 이 경우에 공격자가 DB를 탈취했을 때, 원문 비밀번호를 바로 알 수는 없더라도 같은 해시값을 가진 사용자들이 같은 비밀번호를 사용하고 있다는 사실을 인식할 수 있습니다.
- 또한 자주 사용되는 비밀번호를 미리 해시해둔 목록과 비교하면 원래 비밀번호를 추측할 수 있기에 Salt와 함께 사용합니다.
- 빠른 속도
- MD5나 SHA 계열 같은 일반적인 해시 함수는 데이터를 빠르게 처리하도록 설계되어 있습니다. 이는 빠르지만 비밀번호 저장에서는 단점이 될 수 있습니다.
- 공격자가 해시값을 탈취한 뒤 흔히 사용되는 비밀번호 후보를 빠르게 해시해 비교할 수 있기 때문에 해시 함수가 빠를수록 공격자는 더 많은 후보를 짧은 시간 안에 대입할 수 있습니다.
문제점을 보완하기 위한 방법
1) 솔트(salt)
salt는 단방향 해시 함수에서 다이제스트를 생성할 때 추가하는 바이트 단위의 임의의 문자열입니다.
Salt를 사용하면 같은 비밀번호를 사용하는 사용자들도 서로 다른 해시값을 가지게 되어 미리 계산된 해시 목록을 이용한 공격을 어렵게 만들 수 있습니다.
2) 키 스트레칭(key Streching)
키 스트레칭은 다이제스트를 해시함수로 생성하고, 이 다이제스트를 입력값으로 해서 또 다시 해시함쑤로 다이제스트를 생성하고 이를 계속 반복하는 방법입니다.
다이제스트를 계속 새롭게 생성할 수 있으면 이렇게 생성할 경우 입력한 패스워드를 동일한 횟수만큼 해시해야만 다이제스트와의 일치여부를 확인할 수 있습니다.
이러한 솔트와 키 스트레칭을 적용하여 KDF라는 해시 함수가 존재합니다.
KDF(Key derivation function) 키 유도 함수
KDF는 주어진 입력에서 안전한 암호화 키를 만드는 함수입니다.
암호화 키를 만들기 위한 설정 값인 CPU 비용, 메모리 비용, 병렬화, Key길이, salt 길이와 같은 특정 매개변수를 필요로 합니다.
KDF 함수의 종류
bcrypt
bcrypt는 비밀번호 저장을 목적으로 설계된 해시 알고리즘입니다. 내부적으로 salt를 사용하고, work factor를 통해 해시 계산 비용을 조절할 수 있습니다. 또한 scrypt에 비해 구현이 단순합니다.

bcrypt를 적용하여 DB에 적용한 예
※work factor: 해시 계산을 얼마나 어렵게 만들지 정하는 값으로 값이 커질수록 해시 계산이 느려져 공격자가 무차별 대입을 시도하기 어려워집니다.
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword = passwordEncoder.encode("password123");
boolean matches = passwordEncoder.matches("password123", encodedPassword);
회원가입 시에는 encode()로 비밀번호를 해시하여 저장하고, 로그인 시에는 matches()로 사용자가 입력한 비밀번호와 저장된 해시값을 비교합니다.
scrypt
scrypt는 비밀번호 기반 키 도출 함수 중 하나로 CPU 연산 뿐 아니라 메모리 사용량까지 크게 요구하도록 설계되었습니다.
많은 메모리를 사용하도록 만들어져 공격자가 GPU나 전용 장비로 대량의 비밀번호를 빠르게 대입하는 것을 어렵게 만드는데 목적을 가지고 있습니다.
bcrypt와 scrypt 비교
bcrypt가 널리 사용되고 안정적인 반면 scrypt는 메모리 비용까지 고려해 공격 비용을 높이는 방식입니다.
| 구분 | bcrypt | scrypt |
| 목적 | 비밀번호 저장 | 비밀번호 저장/키 도출 |
| Salt | 내부적으로 사용 | 사용 |
| 비용 조절 | work factor | CPU 비용 + 메모리 비용 조절 |
| 특징 | 오래 사용되어 검증 사례가 많음 | 메모리 사용량을 높여 공격 비용 증가 |
| 사용성 | Spring Security에서 흔히 사용 | 설정값 이해가 조금 더 필요 |
일반적인 웹 애플리케이션에서는 bcrypt가 많이 사용되며, 더 강한 메모리 기반 방어가 필요한 경우에는 scrypt나 Argon2와 같은 알고리즘도 고려할 수 있습니다.
Spring Boot에서의 사용 예시
일단 사용 예시를 살펴보기 앞서 회원가입/로그인 흐름에 대해 알아볼 필요가 있습니다.
- 회원가입 흐름
- 사용자 비밀번호 입력 → PasswordEncoder.encode() → 해시된 비밀번호 DB 저장
- 로그인 흐름
- 사용자 비밀번호 입력 → DB에서 저장된 해시 비밀번호 조회 → PasswordEncoder.matches(입력값, 저장된 해시값) → 일치할 경우 인증 성공
1. PasswordEncoder 설정
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Spring Security에서는 비밀번호를 직접해시하지 않고 PasswordEncoder를 통해 처리할 수 있습니다.
BCryptPasswordEncoder를 Bean으로 등록하여 애플리케이션 전역에서 bcrypt기반 비밀번호 해시를 사용할 수 있도록 합니다.
2. 회원가입 시 비밀번호 저장
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public void signup(SignupRequest request) {
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = User.builder()
.email(request.getEmail())
.password(encodedPassword)
.build();
userRepository.save(user);
}
}
회원가입 요청에서 전달된 비밀번호는 passwordEncoder.encode()를 통해 bcrypt 해시값으로 변환됩니다.
이후 DB에는 사용자가 입력한 원문 비밀번호가 아니라 해시 처릳된 비밀번호가 저장됩니다.
3. 로그인 시 비밀번호 검증
public void login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
// 이후 JWT 발급 또는 세션 생성
}
로그인 시에는 사용자가 입력한 비밀번호와 DB에 저장된 해시값을 비교해야 합니다.
이때 단순 문자열 비교를 하는 것이 아니라 passwordEncoder.matches()를 사용합니다.
bcrypt는 내부적으로 salt를 사용하기에 같은 비밀번호라도 encode()를 호출할 때마다 다른 해시값이 만들어질 수 있습니다. 따라서 로그인 검증 시에는 입력값을 다시 encode()해서 문자열로 비교하는 방식이 아닌 matches()를 사용해야 합니다.
오늘은 해시 알고리즘에 대해서 학습해보았습니다. 해시 알고리즘에 대해서는 보통 이론적인 부분 따로 직접 적용한 부분 따로라고 생각했는데 평상시에 사용하던 bcrypt도 해시 알고리즘의 일종인줄은 몰랐네요. 몰랐던 부분에 대해 많이 학습된 것 같습니다. 다음 시간에는 HTTPS와 SSL /TLS에 대해서 돌아오도록 하겠습니다. 수고하셨습니다.
'백엔드 공부' 카테고리의 다른 글
| 웹 보안 지식(4) - CORS (0) | 2026.05.11 |
|---|---|
| 웹 보안 지식(3) - HTTPS와 SSL/TLS의 동작 원리 (0) | 2026.05.08 |
| 웹 보안 지식(1) - 웹 보안은 왜 필요한가? (0) | 2026.05.06 |
| 캐시(3) - 클라이언트 사이드 캐시 : 브라우저 캐시와 저장소 (0) | 2026.05.04 |
| 캐시(2) - 서버 사이드 캐시: Redis (0) | 2026.04.28 |