TechFeedTechFeed
Security

개발자를 위한 암호화 기초 — 해시, 대칭키, 비대칭키

bcrypt, AES, RSA/ECDSA 등 실무에서 쓰는 암호화 방식의 원리와 적용법.

한 줄 요약: 비밀번호는 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블록 독립 암호화 — 같은 평문 = 같은 암호문절대 사용 금지
CBCIV(초기화 벡터) 필요, 패딩 필요조건부 허용 (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 비교

항목RSAECDSA
키 길이2048~4096 bits256~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 핸드셰이크 간략 흐름

  1. 클라이언트가 지원 가능한 암호화 스위트 목록 전송
  2. 서버가 인증서(공개키 포함) 전송 + 암호화 스위트 선택
  3. 클라이언트가 인증서 유효성 확인 (CA 서명 검증)
  4. 키 교환 — ECDH로 세션 키(대칭키) 협상
  5. 이후 통신은 협상된 대칭키(AES-GCM)로 암호화

개발자가 확인해야 할 TLS 설정

  • TLS 1.2 이상만 허용 — TLS 1.0, 1.1, SSL 비활성화
  • 약한 암호화 스위트 비활성화 — RC4, DES, 3DES, MD5 기반 스위트 제거
  • 인증서 체인 완전성 확인 — 중간 CA 인증서가 누락되면 일부 클라이언트에서 실패
  • OCSP Stapling 활성화 — 인증서 상태 확인 속도 향상
Node.js HTTPS 서버 설정 + TLS 버전 제한
const https = require('https'); const fs = require('fs'); const tls = require('tls'); const options = { key: fs.readFileSync('/path/to/privkey.pem'), cert: fs.readFileSync('/path/to/fullchain.pem'), minVersion: 'TLSv1.2', // TLS 1.2 이상만 허용 ciphers: [ 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', 'TLS_AES_128_GCM_SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-GCM-SHA256' ].join(':'), honorCipherOrder: true }; https.createServer(options, app).listen(443);
팁: 암호화 키는 코드나 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-3MD5 사용 금지
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(서명)이 주요 후보다. 대부분의 서비스는 아직 적용하지 않아도 되지만, 금융/의료/국방 관련 장기 데이터는 지금부터 준비를 시작할 시점이다.
암호화보안해시AESRSATLS

관련 포스트

OWASP Top 10 2026 — 웹 보안 필수 체크리스트2026-02-18인증 구현 가이드 2026 — JWT, OAuth, Passkey2026-02-20API 보안 체크리스트 20262026-03-06JWT vs 세션 인증 — 무엇을 선택할 것인가2026-03-07