한 줄 요약: 비밀번호는 bcrypt로 해시, 데이터는 AES로 암호화, 서명과 키 교환은 RSA/ECDSA — 각 도구가 해결하는 문제가 다르다. 상황에 맞는 암호화를 써야 한다. 암호화를 잘 몰라도 개발은 가능하다. 하지만 잘못된 암호화는 아예 안 하는 것보다 더 위험하다. MD5로 비밀번호를 해시하거나 ECB 모드로 AES를 쓰는 것처럼 — 겉으로는 암호화처럼 보이지만 실제로는 무방비 상태다. 이 글은 해시, 대칭키, 비대칭키 암호화를 개발자 관점에서 실용적으로 정리한다.
한 줄 요약: 비밀번호는 bcrypt로 해시, 데이터는 AES로 암호화, 서명과 키 교환은 RSA/ECDSA — 각 도구가 해결하는 문제가 다르다. 상황에 맞는 암호화를 써야 한다.
암호화를 잘 몰라도 개발은 가능하다. 하지만 잘못된 암호화는 아예 안 하는 것보다 더 위험하다. MD5로 비밀번호를 해시하거나 ECB 모드로 AES를 쓰는 것처럼 — 겉으로는 암호화처럼 보이지만 실제로는 무방비 상태다. 이 글은 해시, 대칭키, 비대칭키 암호화를 개발자 관점에서 실용적으로 정리한다.
해시 함수 — 비밀번호는 이렇게 저장한다
해시는 단방향 함수다. 입력에서 고정 길이 출력을 만들고, 출력에서 원래 입력을 복원할 수 없다. 비밀번호 저장에 적합하다. 데이터를 복호화할 필요가 없고, 검증만 하면 되기 때문이다.
해시 알고리즘 선택 기준
알고리즘
용도
비밀번호 저장
비고
MD5
파일 무결성(레거시)
절대 사용 금지
충돌 취약, GPU 초당 수십억 해시
SHA-256
데이터 무결성, JWT 서명
단독 사용 금지
빠름 → 레인보우 테이블 취약
bcrypt
비밀번호 저장
권장
salt 자동 포함, cost factor 조정 가능
Argon2id
비밀번호 저장
최신 권장
메모리 사용 → GPU 공격 어렵게 설계
PBKDF2
비밀번호 저장, 키 유도
허용
반복 횟수 충분히 설정 필요(600,000+)
개발자를 위한 암호화 기초 — 해시, 대칭키, 비대칭키 — 보안 아키텍처 다이어그램 (출처: 공식 문서 및 벤치마크 데이터 기반)
bcrypt와 Argon2 비밀번호 해시 예시 (Node.js)
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
// bcrypt — cost factor 12 권장 (2026년 기준)
async function hashPasswordBcrypt(password) {
const saltRounds = 12; // 값이 클수록 느려짐 (2^12 반복)
return await bcrypt.hash(password, saltRounds);
}
async function verifyPasswordBcrypt(password, hash) {
return await bcrypt.compare(password, hash);
// hash 내부에 salt가 포함되어 있어 별도 저장 불필요
}
// Argon2id — 현재 OWASP 최우선 권장
async function hashPasswordArgon2(password) {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 64 * 1024, // 64MB
timeCost: 3, // 반복 횟수
parallelism: 4
});
}
async function verifyPasswordArgon2(password, hash) {
return await argon2.verify(hash, password);
}
// 사용
const hash = await hashPasswordArgon2(req.body.password);
await db.query('INSERT INTO users (email, password_hash) VALUES ($1, $2)', [email, hash]);
주의: bcrypt는 입력을 72바이트로 자른다. 72자 이상의 비밀번호는 73번째 문자부터 해시에 반영되지 않아 보안 수준이 낮아진다. 72자 이상 비밀번호를 지원해야 한다면 먼저 SHA-256으로 해시한 뒤 bcrypt를 적용하거나(pepper+bcrypt 패턴), Argon2를 사용한다.
대칭키 암호화 — AES로 데이터를 잠근다
대칭키 암호화는 같은 키로 암호화와 복호화를 모두 한다. 속도가 빠르고 대용량 데이터에 적합하다. AES(Advanced Encryption Standard)가 현재 표준이다.
AES 모드 선택
모드
특징
권장 여부
ECB
블록 독립 암호화 — 같은 평문 = 같은 암호문
절대 사용 금지
CBC
IV(초기화 벡터) 필요, 패딩 필요
조건부 허용 (GCM 권장)
GCM
인증 포함 (AEAD), IV 필요
현재 표준 권장
ChaCha20-Poly1305
모바일/임베디드 최적화
권장 (AES 가속 없는 환경)
AES-256-GCM을 쓰면 암호화와 무결성 검증(변조 탐지)을 동시에 처리한다. CBC 모드는 패딩 오라클 공격에 취약하므로 GCM을 우선한다.
개발자를 위한 암호화 기초 — 해시, 대칭키, 비대칭키 — 위협 모델 시각화 (출처: 공식 문서 및 벤치마크 데이터 기반)
AES-256-GCM 암호화/복호화 예시 (Node.js crypto)
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
// 키 생성 (환경 변수에서 로드하거나 KMS 사용)
// const key = crypto.randomBytes(KEY_LENGTH); // 생성 예시
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(plaintext) {
const iv = crypto.randomBytes(12); // GCM에는 12바이트 IV 권장
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// iv + authTag + ciphertext를 함께 저장
return {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
data: encrypted
};
}
function decrypt(encryptedObj) {
const { iv, authTag, data } = encryptedObj;
const decipher = crypto.createDecipheriv(
ALGORITHM,
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8'); // authTag 검증 실패 시 여기서 에러
return decrypted;
}
비대칭키 암호화 — RSA와 ECDSA의 역할
비대칭키 암호화는 공개키(Public Key)와 비밀키(Private Key)를 사용한다. 공개키로 암호화하면 비밀키로만 복호화 가능하고, 비밀키로 서명하면 공개키로 검증 가능하다.
RSA vs ECDSA 비교
항목
RSA
ECDSA
키 길이
2048~4096 bits
256~384 bits (동급 보안)
속도
상대적으로 느림
빠름
키 크기
큼
작음 (모바일 유리)
주요 용도
TLS 키 교환, 암호화
디지털 서명, JWT, TLS
호환성
광범위
최신 환경 권장
실무 사용 시나리오
JWT 서명: 단일 서버라면 HS256(대칭, HMAC-SHA256). 마이크로서비스처럼 여러 서비스가 검증해야 하면 RS256 또는 ES256(비대칭)이 적합 — 검증 서비스에 비밀키를 배포하지 않아도 됨.
데이터 암호화 + 전송: 대용량 데이터는 AES-256-GCM으로 암호화하고, AES 키 자체를 RSA로 암호화하는 하이브리드 방식 사용.
코드 서명: 배포 아티팩트의 무결성 검증에 ECDSA 사용.
TLS — 전송 구간 보안의 기반
TLS(Transport Layer Security)는 앞서 설명한 암호화 기법들을 조합해 네트워크 전송을 보호한다. HTTPS의 보안 계층이 TLS다.
TLS 핸드셰이크 간략 흐름
클라이언트가 지원 가능한 암호화 스위트 목록 전송
서버가 인증서(공개키 포함) 전송 + 암호화 스위트 선택
클라이언트가 인증서 유효성 확인 (CA 서명 검증)
키 교환 — ECDH로 세션 키(대칭키) 협상
이후 통신은 협상된 대칭키(AES-GCM)로 암호화
개발자가 확인해야 할 TLS 설정
TLS 1.2 이상만 허용 — TLS 1.0, 1.1, SSL 비활성화
약한 암호화 스위트 비활성화 — RC4, DES, 3DES, MD5 기반 스위트 제거
인증서 체인 완전성 확인 — 중간 CA 인증서가 누락되면 일부 클라이언트에서 실패
OCSP Stapling 활성화 — 인증서 상태 확인 속도 향상
개발자를 위한 암호화 기초 — 해시, 대칭키, 비대칭키 — 취약점 분석 플로우차트 (출처: 공식 문서 및 벤치마크 데이터 기반)
팁: 암호화 키는 코드나 git 저장소에 절대 저장하지 않는다. 프로덕션에서는 AWS KMS, HashiCorp Vault, GCP Secret Manager 같은 전용 키 관리 서비스를 사용한다. 키 로테이션 일정(90~180일)을 자동화하고, 키 접근 로그를 모니터링하는 것이 운영 보안의 기본이다.
실무 적용 가이드 — 상황별 암호화 선택
어떤 암호화를 써야 하는지 상황별로 정리한다.
상황
권장 방법
비고
비밀번호 저장
Argon2id 또는 bcrypt(cost 12+)
SHA-256 단독 절대 금지
개인정보 DB 저장
AES-256-GCM
키는 KMS 관리
JWT 서명 (단일 서버)
HS256 (HMAC-SHA256)
비밀키 32바이트 이상
JWT 서명 (마이크로서비스)
RS256 또는 ES256
공개키만 배포, 비밀키 중앙 관리
파일 무결성 확인
SHA-256 또는 SHA-3
MD5 사용 금지
API 키 생성
crypto.randomBytes(32)
Math.random() 절대 금지
네트워크 전송 보안
TLS 1.3 + AES-256-GCM
인증서 자동 갱신 설정
암호화 라이브러리는 직접 구현하지 않는다. 검증된 라이브러리(Node.js 기본 crypto 모듈, libsodium, OpenSSL 기반 라이브러리)를 사용한다. 직접 구현한 암호화 알고리즘은 미묘한 버그 하나가 전체 보안을 무너뜨린다.
참고: 양자 컴퓨터 시대를 준비한 Post-Quantum Cryptography(PQC) 표준이 2024년 NIST에서 확정됐다. CRYSTALS-Kyber(키 교환)와 CRYSTALS-Dilithium(서명)이 주요 후보다. 대부분의 서비스는 아직 적용하지 않아도 되지만, 금융/의료/국방 관련 장기 데이터는 지금부터 준비를 시작할 시점이다.
자주 묻는 질문
해시·대칭키·비대칭키는 각각 어떤 상황에 써야 하나요?
선택 기준은 단순합니다. 다시 읽을 필요가 없는 값이면 해시, 다시 읽어야 하면 대칭키, 키를 안전하게 나눠야 하면 비대칭키입니다. 비밀번호는 검증만 하면 되고 복호화할 일이 없으니 Argon2id나 bcrypt(cost 12+) 해시가 맞습니다. 반대로 개인정보처럼 나중에 다시 평문으로 읽어야 하는 데이터는 AES-256-GCM 같은 대칭키 암호화를 씁니다. 단일 서버 JWT 서명도 HS256(대칭)으로 충분합니다. RSA·ECDSA 같은 비대칭키는 여러 서비스가 같은 토큰을 검증해야 하는 마이크로서비스 환경, TLS 키 교환, 코드 서명처럼 비밀키를 공유하지 않고 공개키만 배포해야 하는 상황에 적합합니다. 반대로 단일 서버에 비대칭키를 쓰면 느리고 키 관리만 복잡해져 부적합합니다.
더 깊게 공부하려면 어떤 자료를 보면 좋을까요?
실무 기준점으로는 OWASP의 Password Storage Cheat Sheet와 Cryptographic Storage Cheat Sheet 두 문서가 가장 정확합니다. bcrypt cost factor나 Argon2id 파라미터 권장값이 최신 기준으로 갱신되니 주기적으로 확인하시는 게 좋습니다. AES-GCM이나 키 관리 코드를 직접 다룬다면 Node.js crypto 모듈 공식 문서의 createCipheriv 섹션을 정독하시길 권합니다. IV·authTag 처리를 정확히 이해하지 못하면 미묘한 버그가 생깁니다. 한 단계 더 깊이 가려면 Cryptography Engineering(Ferguson·Schneier) 책에서 왜 ECB를 쓰면 안 되는지 같은 원리를 다지면 됩니다.
개발자를 위한 암호화 기초, 한 줄로 정리하면 어떻게 되나요?
암호화는 만능 도구 하나가 아니라 문제마다 맞는 도구가 따로 있다는 것이 핵심입니다. 비밀번호는 복호화 불가능한 Argon2id·bcrypt로 해시하고, 다시 읽어야 하는 데이터는 AES-256-GCM으로 암호화하며, 키 교환과 서명은 RSA·ECDSA로 처리합니다. 가장 중요한 것은 알고리즘보다 키 관리입니다. AES를 써도 키를 git에 박아두면 무의미하므로 KMS·Vault로 키를 분리하는 것까지가 한 묶음입니다.
실무에서 처음 도입할 때 가장 먼저 확인할 것은 무엇인가요?
가장 먼저 확인할 것은 키를 어디에 둘 것인가입니다. 암호화 코드보다 키 관리가 먼저입니다. 키를 코드나 git 저장소에 박아두면 AES-256-GCM을 써도 무의미합니다. 환경 변수가 최소선이고, 프로덕션이라면 AWS KMS·HashiCorp Vault·GCP Secret Manager 중 하나로 시작하세요. 그다음 용도를 명확히 구분합니다. 비밀번호는 복호화할 필요가 없으니 Argon2id나 bcrypt(cost 12+)로 해시하고, 나중에 다시 읽어야 하는 개인정보는 AES-256-GCM으로 암호화합니다. 이 둘을 섞어 비밀번호를 AES로 암호화하는 실수만 피해도 절반은 성공입니다.
가장 자주 발생하는 실수나 함정은 무엇인가요?
'암호화처럼 보이지만 실제로는 무방비'인 패턴들이 반복됩니다. 비밀번호를 MD5나 SHA-256으로 해시하는 것이 대표적입니다. 이 알고리즘들은 너무 빨라서 GPU로 초당 수십억 개를 대입할 수 있어 사실상 평문과 다름없습니다. AES를 ECB 모드로 쓰는 것도 같은 함정입니다. 같은 평문이 항상 같은 암호문이 되어 패턴이 그대로 드러나니 반드시 GCM 모드를 쓰세요. 또 하나 놓치기 쉬운 것이 bcrypt의 72바이트 제한입니다. 72자가 넘는 비밀번호는 뒷부분이 해시에 반영되지 않습니다. 마지막으로 API 키나 토큰을 Math.random()으로 만드는 실수인데, 반드시 crypto.randomBytes(32)를 써야 예측 불가능한 값이 나옵니다.