목차
레거시 모놀리스에 RS256을 도입했다가 토큰 발급 지연이 평균 3배로 늘어났고, 결국 HS256으로 롤백한 적이 있다. JWT RS256 HS256 차이를 "보안 강도"로만 비교한 자료가 너무 많아서 생긴 오판이었다.
실제로, 이 글은 그 오판을 되풀이하지 않기 위해 정리하는 비교 노트다. ES256까지 포함해서, "어디서 검증되는가"라는 한 가지 질문을 중심으로 답을 다시 짜본다.
"무조건 RS256이 답"이라는 통념의 함정
실제로, JWT 관련 글 대부분이 비슷한 결론으로 끝난다. "HS256은 대칭키라서 위험하니 가능하면 RS256을 써라." 이 문장 자체는 틀리지 않다. 다만 맥락이 빠져 있다.
대칭키가 위험한 이유는 키를 공유해야 검증할 수 있어서다. 그런데 단일 서버 환경에서는 공유할 곳이 없다. 발급도 그 서버, 검증도 그 서버다. 이 경우 RS256은 보안 이득 없이 비용만 늘린다.
그런데, :::tip "비대칭 = 더 안전"은 키 분배 시나리오가 존재할 때만 성립한다. 키가 한 프로세스 안에서만 존재한다면, 대칭/비대칭 구분 자체가 의미를 잃는다. :::
실제로 RFC 7518에서도 알고리즘 선택은 배포 모델에 따라 결정하라고 명시한다 (출처: RFC 7518 §3.1). "강한 알고리즘"이라는 단어 자체가 없다. 컨텍스트에 따라 적절한 알고리즘이 다르다는 의미다.
세 알고리즘이 실제로 다른 지점
HS256, RS256, ES256은 "보안 강도"가 아니라 "키 모델"과 "연산 비용"에서 갈린다. 추상적으로 설명하면 와닿지 않으니, 세 가지 축으로 본다.
서명 vs 검증 비용 비대칭
또한, HS256은 대칭이라 서명과 검증 비용이 거의 같다. HMAC-SHA256 한 번이다. 반면 RS256은 서명이 무겁고 검증이 가볍다. ES256은 그 반대에 가깝다. 서명은 비교적 가볍고 검증은 RS256보다 느리다.
| 알고리즘 | 서명 비용 | 검증 비용 | 토큰 서명 크기 |
|---|---|---|---|
| HS256 | 매우 낮음 | 매우 낮음 | 32 bytes |
| RS256 (2048비트) | 높음 | 낮음 | 256 bytes |
| ES256 (P-256) | 중간 | 중간 | 64 bytes |
따라서, (출처: JWT.io 알고리즘 비교, 2026-06 기준)
여기서 자주 빠지는 함정이 있다. 검증이 발급보다 압도적으로 자주 일어나는 시스템에서는 RS256이 합리적이다. 모든 요청마다 토큰을 검증해야 하니까. 발급 시점에 한 번 무거운 게 검증 시점에 가벼운 것보다 낫다.
다만 발급도 자주 일어나는 시스템 — 예를 들어 토큰 수명이 5분인 API — 에서는 이 계산이 뒤집힌다. 발급 부담이 그대로 누적된다.
토큰 크기와 네트워크 비용
한편, RS256 토큰은 HS256보다 평균 200바이트 이상 크다. 한 요청에서는 무시할 수 있지만, IoT 디바이스나 모바일 셀룰러 환경에서는 누적 효과가 보인다. 1초에 100req를 처리하는 게이트웨이에서 헤더 200바이트 차이는 시간당 약 72MB의 추가 트래픽이다.
예를 들어, ES256은 RS256과 동급의 비대칭 보안을 제공하면서 토큰 크기가 1/4 수준이다. 이게 최근 클라우드 IdP들이 ES256으로 옮겨가는 이유다 (Auth0, Firebase 모두 ES256 옵션이 기본 후보로 들어와 있다).
키 관리 복잡도
게다가, 이게 실무에서 가장 큰 차이다. HS256은 비밀 한 개를 어디에 둘 것인가의 문제다. RS256은 비밀 키 보관 + 공개 키 배포 + 키 회전 + 검증자 캐시 무효화의 문제다.
실제로 RS256을 처음 도입한 팀이 JWKS 엔드포인트의 캐시 TTL을 잘못 설정해서 키 회전 직후 1시간 동안 모든 인증이 깨지는 사고가 흔하다. 이건 알고리즘 문제가 아니라 운영 모델 문제다. 알고리즘을 바꾸기 전에 이 운영 비용을 감당할 인력이 있는지 먼저 봐야 한다.
환경별로 답이 갈리는 지점
위 세 축을 합치면 환경별 답이 자연스럽게 나온다.
단일 서버 모놀리스
발급자와 검증자가 같은 프로세스다. 키 분배 문제가 없다. 이 경우 HS256이 거의 무조건 합리적이다. 비밀 키는 환경변수나 시크릿 매니저에서 한 번 읽으면 끝이고, 회전도 단순한 재배포다.
그럼에도 RS256을 권하는 글이 많은데, 이건 "나중에 마이크로서비스로 갈 수 있으니까"라는 추측에 기반한 추천이다. YAGNI 관점에서 보면 그 시점에 가서 바꾸면 된다. 알고리즘 교체는 토큰 수명만큼만 기다리면 되는 작업이다 (refresh 토큰을 쓴다면 더 짧다).
마이크로서비스 내부 통신
그래서, 여러 서비스가 같은 토큰을 검증해야 한다. HS256을 쓰려면 비밀 키를 모든 서비스에 뿌려야 한다. 한 서비스만 침해당해도 전체 시스템의 토큰을 위조할 수 있다.
이 경우 RS256 또는 ES256이 합리적이다. 인증 서버만 비밀 키를 갖고, 나머지 서비스는 공개 키만 받는다. 침해 범위가 좁아진다.
물론, 토큰 크기가 신경 쓰이는 환경(서비스 간 RPC 트래픽이 많은 경우)이면 ES256을 우선 검토한다. 그렇지 않다면 라이브러리 지원이 더 넓은 RS256이 무난하다.
외부 공개 API
특히, OAuth 2.0 / OpenID Connect를 외부에 노출하는 경우다. JWKS 엔드포인트를 통해 공개 키를 배포해야 한다. 이 시점에 HS256은 선택지에서 제외된다. 외부 클라이언트한테 비밀 키를 줄 수는 없다.
RS256과 ES256 중에서는 ES256이 점점 표준이 되는 추세다. OpenID Connect Core 1.0이 RS256을 기본으로 요구하지만, FAPI 2.0 같은 최신 프로파일은 ES256(또는 더 강한 PS256)을 권장한다 (출처: FAPI 2.0 Security Profile, 2025년 채택).
물론 외부 클라이언트의 라이브러리 호환성을 확인해야 한다. 오래된 SDK가 ES256을 지원 안 하는 경우가 아직 있다.
키 관리에서 실제로 깨지는 지점
알고리즘 선택보다 키 관리에서 사고가 훨씬 많이 난다. 환경별로 정리해본다.
HS256: 비밀이 새는 경로
실제로, 가장 흔한 사고는 비밀 키가 코드 저장소에 커밋되는 경우다. JWT_SECRET=mysecret이 .env.example에 들어가 있고, 누군가 .env를 .gitignore에 안 넣었다. 깃 히스토리에 영원히 남는다.
두 번째는 "약한 비밀"이다. HS256은 키 길이 = 보안 강도다. RFC 7518에서 최소 256비트(32바이트) 랜덤 값을 요구한다. "secret", "jwt-key" 같은 사람이 외울 수 있는 문자열은 그 자체로 취약점이다.
// 안 좋은 예
const secret = process.env.JWT_SECRET || "default-secret";
// 안전한 예 - 시작 시 검증
const secret = process.env.JWT_SECRET;
if (!secret || Buffer.from(secret).length < 32) {
throw new Error("JWT_SECRET은 32바이트 이상이어야 한다");
}
RS256/ES256: 키 회전이 실제 작업이다
비대칭 알고리즘은 키 회전이 핵심이다. 비밀 키가 새면 알고리즘이 아무리 강해도 의미가 없다. 그런데 회전은 "예전 키로 서명한 토큰이 아직 유효한 동안 새 키도 같이 운영"하는 작업이다.
따라서, 이걸 위해 JWKS는 kid(key ID)로 여러 키를 동시에 배포한다. 검증자는 토큰 헤더의 kid를 보고 맞는 키를 고른다. 회전 절차는 대략 이렇다.
- 새 키 쌍 생성, JWKS에 추가 (검증자들이 캐시 갱신할 때까지 대기)
- 발급자가 새 키로 서명 시작
- 토큰 최대 수명만큼 대기
- 옛 키를 JWKS에서 제거
여기서 자주 깨지는 게 검증자의 JWKS 캐시 TTL이다. 5분 캐시인데 회전 직후 바로 새 키로 서명을 시작하면, 5분 동안 검증 실패가 발생한다. 캐시 갱신 대기 시간을 캐시 TTL의 2배 이상 잡는 게 안전하다.
"키 로테이션 정책"이 없다는 사실
대부분의 팀이 키 회전 자체를 한 번도 안 한 상태로 운영 중이다. 비대칭 알고리즘의 보안 가정 자체가 회전 가능성을 전제하는데, 회전이 실제로 일어나지 않으면 그 가정이 깨진다. 이 부분은 알고리즘 선택만큼 중요하다.
그래서 어떻게 정하나
세 가지를 순서대로 본다.
첫째, 검증자가 발급자와 분리되어 있나? 아니라면 HS256이다. "나중에 분리될지도 모르니까"는 이유가 안 된다.
예를 들어, 둘째, 분리되어 있다면 검증자가 신뢰 경계 안에 있나? 같은 조직 안의 마이크로서비스라면 RS256으로 시작해도 무리 없다. ES256은 토큰 크기가 실제로 문제가 될 때 옮기면 된다.
셋째, 외부 공개 API라면 ES256을 우선 검토하되, 클라이언트 호환성 매트릭스를 먼저 확인한다. 호환성이 걸리면 RS256으로 시작하고 마이그레이션 경로(alg 필드 + kid로 점진 전환)를 미리 설계한다.
그런데, 지금 당장 할 수 있는 액션 세 가지를 남긴다.
.env의JWT_SECRET길이를 확인한다. 32바이트 미만이면 즉시 교체한다.- 비대칭 알고리즘을 쓰고 있다면, 마지막 키 회전이 언제였는지 확인한다. "한 번도 없음"이면 회전 절차부터 문서화한다.
- 검증자 측 JWKS 캐시 TTL이 회전 절차의 대기 시간보다 짧은지 확인한다. 짧으면 회전 시 인증 장애가 난다.
관련 글
- 세션 JWT 인증 비교: 로그아웃 안 되는 토큰 때문에 밤샌 이야기 (2026) – 세션 JWT 인증 비교를 다룬다. JWT로 갈아탔다가 로그아웃이 안 되는 문제로 헤맸던 경험, 그리고 어떤 기준으로 다시 결정했는지 정리한다.
- OAuth PKCE 구현 회고: React·Flutter 인증을 3개월간 다시 짠 기록 – Implicit Flow에서 Authorization Code + PKCE로 전환한 3개월 프로젝트. SPA와 모바일에서 만난 함정과 실용…
- HTTP 보안 헤더 설정 — Nginx·Express 환경별 실전 가이드 – 어제 운영 서버 점검하다가 securityheaders.com에서 D 받은 걸 보고 헤더를 다시 손봤다. 그 과정에서 정리한 메모다.