JWT RS256 HS256 차이, 환경별로 다른 답이 나오는 이유

목차

레거시 모놀리스에 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를 보고 맞는 키를 고른다. 회전 절차는 대략 이렇다.

  1. 새 키 쌍 생성, JWKS에 추가 (검증자들이 캐시 갱신할 때까지 대기)
  2. 발급자가 새 키로 서명 시작
  3. 토큰 최대 수명만큼 대기
  4. 옛 키를 JWKS에서 제거

여기서 자주 깨지는 게 검증자의 JWKS 캐시 TTL이다. 5분 캐시인데 회전 직후 바로 새 키로 서명을 시작하면, 5분 동안 검증 실패가 발생한다. 캐시 갱신 대기 시간을 캐시 TTL의 2배 이상 잡는 게 안전하다.

"키 로테이션 정책"이 없다는 사실

대부분의 팀이 키 회전 자체를 한 번도 안 한 상태로 운영 중이다. 비대칭 알고리즘의 보안 가정 자체가 회전 가능성을 전제하는데, 회전이 실제로 일어나지 않으면 그 가정이 깨진다. 이 부분은 알고리즘 선택만큼 중요하다.

그래서 어떻게 정하나

세 가지를 순서대로 본다.

첫째, 검증자가 발급자와 분리되어 있나? 아니라면 HS256이다. "나중에 분리될지도 모르니까"는 이유가 안 된다.

예를 들어, 둘째, 분리되어 있다면 검증자가 신뢰 경계 안에 있나? 같은 조직 안의 마이크로서비스라면 RS256으로 시작해도 무리 없다. ES256은 토큰 크기가 실제로 문제가 될 때 옮기면 된다.

셋째, 외부 공개 API라면 ES256을 우선 검토하되, 클라이언트 호환성 매트릭스를 먼저 확인한다. 호환성이 걸리면 RS256으로 시작하고 마이그레이션 경로(alg 필드 + kid로 점진 전환)를 미리 설계한다.

그런데, 지금 당장 할 수 있는 액션 세 가지를 남긴다.

  • .envJWT_SECRET 길이를 확인한다. 32바이트 미만이면 즉시 교체한다.
  • 비대칭 알고리즘을 쓰고 있다면, 마지막 키 회전이 언제였는지 확인한다. "한 번도 없음"이면 회전 절차부터 문서화한다.
  • 검증자 측 JWKS 캐시 TTL이 회전 절차의 대기 시간보다 짧은지 확인한다. 짧으면 회전 시 인증 장애가 난다.

관련 글