새소식

Welcome to the tech blog of Junior Backend Developer Seulbin Kang!

Backend

(BE 개발자 3개월차) 현업에서 고민한 JWT 로그인

  • -

👀 들어가기 전에

 

원래 이 포스팅은 프로젝트가 갈무리 되어갈 시점에 올릴 예정이었으나, 한분의 요청으로 예상보다 일찍(?) 작성하게 되었다.

결국 저러고 사라져버렸다고 한다

2025년 11월 중순, 이전 글에서 언급했던 프로젝트를 통째로 혼자 맡게 되었다.
Admin 페이지를 개발하던 중, 기존에 “그냥 쓰고 있던” JWT 로그인 방식에 대해 다시 한번 제대로 정리할 필요를 느꼈다.
이 글은 그 과정에서 고민했던 JWT 로그인 전반의 흐름과 설계 포인트를 정리한 글이다.


👀 본론

JWT로그인이란 무엇일까?

다들 아는 그대로 토큰을 사용한 로그인 방식이다. 워낙, 관습처럼 코딩해오던 '그 방식'을 많이들 사용하기에 특별한 의문을 품지 않고 코딩해왔기에 근본적인 것에 대한 질문을 던지기 시작하는 순간 아래와 같은 현상이 발생하게 된다.

 

현업에서 JWT 로그인을 진행하면서 질문을 던지게 된 것은 아래와 같다.

- 왜 JWT 토큰이어야만 하는가? Session이면 안될 이유가 무엇인가?

- JWT 토큰의 암호화 방식(?)은 어떤 방식을 사용하는 것이 적합한가? (뒤에 나오는 부분을 꼭 읽어주세요)

- 토큰의 저장소로는 어디가 적합할 것인가? 

- JWT 블랙리스트는 정말 필요한가?

 

- 이 토큰을 어디에 담아 전달해야 하는가?


1. 왜 JWT토큰을 사용한 로그인이어야만 하는가? 

이 질문은 비교적 쉽게 해결이 되었다. 알고 있는 지식 선에서 충분한 해결이 가능한 질문이었다.

 

'JWT 토큰이란 무엇인가?'에 대한 질문에 답을 먼저 하려한다.

JWT 토큰이란, Json 객체에 인증에 필요한 정보들을 담은 후 비밀키로 서명한 토큰을 뜻한다.

비밀키로 암호화한 토큰이라는 것이다.

반면, Session 로그인 방식 혹은 쿠키 로그인 방식은 위 JWT 토큰 기반 암호화에 비하면 매우 보안에 취약하다는 단점이 있다.

 

왜 취약할까?

이는 인증 방식을 뜯어보면 해결이 된다.

 

1)세션

세션 기반 인증은 사용자가 로그인하면, 서버가 세션을 생성하고 이를 서버 메모리 또는 별도의 저장소에 보관한다.
이후 클라이언트는 매 요청마다 세션 ID를 쿠키에 담아 전달하고, 서버는 해당 세션 ID를 기준으로 사용자를 식별한다.

문제는 이 세션 ID 자체에는 아무런 사용자 정보도, 무결성 보장도 포함되어 있지 않다는 점이다.


즉, 세션 ID는 단순한 식별자일 뿐이며, 만약 이 값이 탈취된다면 서버 입장에서는 정상 사용자와 공격자를 구분할 방법이 없다.

 

2) 쿠키

쿠키는 브라우저에 의해 자동으로 전송되기 때문에, 의도하지 않은 요청에서도 세션이 함께 전송될 수 있다.
이로 인해 CSRF(Cross-Site Request Forgery) 공격에 취약해지며, 이를 방어하기 위해 별도의 CSRF 토큰이나 추가적인 보안 장치가 필수적으로 요구된다.

 

반면 JWT는 토큰 내부에 사용자 정보와 만료 시간, 서명(Signature)을 포함하고 있으며, 서버는 토큰의 서명 검증만으로 위·변조 여부를 즉시 판단할 수 있다. 

여기서 헷갈리면 안되는 문제는 (저도 처음 배웠을 때는 많이 헷갈렸습니다.)
'세션 기반 로그인에서 세션을 사용하는 것은 알겠으나, 쿠키라고 적혀있는데 왜 세션이 함께 전송된다고 하지?' 라는 생각을 많이 하게 되는데, 
쿠키에 세션 ID를 담아서 인증하는 방식을 사용하기에 흔히 '쿠키 기반 로그인을 사용한다'라고 표현을 하는 것 같았습니다.

 

그리고 현재 담당한 프로젝트의 Admin 페이지는 회사 내부 관계자들이 사용하는 시스템으로, 인증 구조에 대한 보안 요구 수준이 높았다.

이에 따라 세션 기반 인증과 비교해 토큰 위·변조 검증과 서버 확장성 측면에서 유리한 JWT 기반 인증을 하나의 선택지로 두고 설계를 진행했다.


2. JWT 토큰의 암호화 방식(?)에 대한 오해

처음 요구사항이 들어왔을때,

"JWT로그인으로 해주시고, 토큰은 암호화해주세요."

라고 요청이 들어왔다.

 

말이 어색해 당황했던 기억이 있다. '토큰 서명을 암호화한다고 표현하나?' 

JWT에서 흔히 혼동되는 부분이 암호화(Encryption)서명(Signature) 이 부분이다.

JWT는 기본적으로 토큰을 암호화하지 않는다.


대신, Header와 Payload에 대해 서명을 수행한다. 이로 인해 JWT에서는 다음과 같은 선택지가 존재한다.

  • HS256: 대칭키 기반 서명
  • RS256: 비대칭키 기반 서명

본 프로젝트에서는 키 관리, 인증 책임 분리, 그리고 다중 서버 환경에서의 운영 안정성을 고려하여 RS256을 선택했다.

(결론적으로 요구사항이 들어왔던 것은 토큰 암호화와 서명을 모두 고려하는 것이었으나, JWT의 서명만으로 요구되는 보안 요건을 충족할 수 있다고 판단해 서명 기반 구조로 최종 결정했다.)


3. 토큰의 저장소로는 어디가 적합한가?

JWT 기반 인증을 다루는 많은 글에서는 토큰 관리 수단으로 Redis를 사용하는 사례를 쉽게 찾아볼 수 있다.
특히 Refresh Token 관리나 블랙리스트 구현 관점에서 Redis는 매우 합리적인 선택처럼 보인다.

하지만 한 단계 더 고민해보면, “과연 이 프로젝트에서도 Redis가 반드시 필요한가?”라는 질문이 남는다.

 

결론부터 말하면, 본 프로젝트에서는 그럴 필요가 없다고 판단했다.
이미 안정적으로 운영 중인 MySQL RDS 환경이 존재했고, 이를 대체하거나 보완하기 위해 별도의 ElasticCache(Redis)를 도입할 만큼의 명확한 이점이 존재하지 않았기 때문이다.

 

그 판단의 근거는 다음과 같다.


현재 JWT 인증을 적용한 대상은 Admin 전용 서버로, 외부에 공개된 서비스가 아닌 내부 운영자용 시스템이다.

  • 예상 사용자 수: 최대 10명 내외
  • 동시 접속 및 인증 요청 빈도: 매우 낮음
  • 트래픽 스파이크 가능성: 사실상 없음

이와 같은 환경에서 Redis를 도입하기 위해 별도의 ElasticCache를 구성하고, 네트워크 연결, 보안 그룹, 운영 비용을 추가로 감수하는 것은 과도한 설계라고 판단했다.

 

JWT 토큰 저장 및 조회는:

  • 초저지연이 요구되는 영역이 아니며
  • RDB 기반 조회로도 충분히 감당 가능한 수준이었다

따라서 기존에 잘 사용하고 있던 MySQL RDS를 그대로 활용하는 것이 비용과 운영 복잡도 측면에서 더 합리적인 선택이었다.


4. JWT 블랙리스트?

간혹 JWT 블랙리스트를 사용해야하고, 꼭 Redis를 써야한다고 생각하는 분들이 많은 것 같다.

(실제로 겪은 일이다. 블랙리스트를 써야한다고..Redis를 깔아달라고 했던...)

 

흔히 말하는 JWT 블랙리스트 방식을 조금 더 들여다보면, 그 본질은 생각보다 단순하다.

블랙리스트란 결국

“더 이상 유효하지 않은 토큰을 저장해두고, 매 요청 시 해당 토큰이 존재하는지 확인하여 접근을 차단하는 구조”에 가깝다.

 

이때 사용되는 저장소가 Redis이든 RDB이든, 개념적인 동작 방식에는 큰 차이가 없다.
차이는 주로 성능 특성과 운영 목적에서 발생한다.

JWT 블랙리스트 방식은 일반적으로 다음과 같이 동작한다.

  1. 로그아웃 또는 강제 무효화 시
    • Access Token을 저장소에 기록
  2. 이후 요청 시
    • 전달된 토큰이 블랙리스트에 존재하는지 조회
  3. 존재할 경우
    • 인증 실패 처리

즉, “토큰을 저장하고, 존재 여부를 조회한다”는 점이며, 이 로직 자체는 RDB에서도 충분히 구현 가능하다.


4-1. 과연 블랙리스트를 사용하는 것이 맞을까?

JWT를 사용하는 가장 큰 이유 중 하나는 무상태성(Stateless) 에 있다.
서버가 인증 상태를 별도로 저장하지 않고, 토큰 자체만으로 사용자를 검증할 수 있다는 점이 핵심이다.

하지만 블랙리스트 방식을 도입하는 순간, 이 전제는 흔들리기 시작한다.

블랙리스트를 사용한다는 것은 곧 매 요청마다 토큰이 무효화되었는지를 확인하기 위해 DB 또는 메모리(캐시)를 반드시 조회해야 한다는 의미이기 때문이다.

 

그렇다면 이 구조를 여전히 '무상태성을 갖는 인증 방식이라고 볼 수 있을까?' 하는 의문이 남게 되었다.

엄밀히 말하면, '그렇지 않다.'는 생각이다. 블랙리스트를 도입한 순간 JWT 인증은 토큰 기반 세션 모델에 가까워지며,
서버는 다시 인증 상태를 추적하는 주체가 된다.


상태 개입의 범위 최소화를 위해 선택한 RTR

이러한 고민 끝에, 본 프로젝트에서는 블랙리스트를 중심으로 한 설계 대신 RTR(Refresh Token Rotation) 전략을 선택했다.

RTR의 핵심은 단순하다.

  • Refresh Token은 단 한 번만 사용 가능하며
  • 사용 즉시 폐기되고
  • 재사용이 감지될 경우 해당 인증 흐름 전체를 무효화한다

이를 통해 서버는

  • 정상적인 요청 흐름에서는 별도의 토큰 상태를 추적하지 않고
  • 오직 Refresh Token 사용 시점에서만 상태를 확인한다

즉, 인증 요청이 몰리는 부분에서는 JWT의 무상태성을 최대한 유지하면서도,
보안적으로 민감한 지점에서는 필요한 만큼의 상태만을 도입한 것이다.


그래서 토큰의 보관은 어디에..?

그렇기 때문에 Redis를 도입하지는 않았다.

다만, 만약

  • 인증/인가를 수행하는 API 수가 크게 늘어나고
  • 인증 요청 트래픽이 집중되며
  • 토큰 조회가 병목 지점이 되는 상황이라면

Redis는 충분히 검토해볼 만한 선택지가 되었을 것이다.


5. 이 토큰을 어디다 담아서 전달해야할 것인가.

이 부분에 대해서 제일 크게 고민했던 것 같다. 개발에는 정답이 없듯이 어떻게 어디다가 넣어도 동작은 한다.

허나, 보안을 생각하면 이야기가 달라진다. 일반적인 권장 구조는 다음과 같다.

  • Access Token → Authorization Header
  • Refresh Token → HttpOnly Cookie

위 방식대로 담아 전달하게 될 경우 CSRF 위험이 최소화되며 Access Token 자동 전송을 방지할 수 있고, 명확한 책임 분리가 가능하다.

 

여기서 의문을 제기하였다.

둘 다 쿠키에 전달하면 안되는 것인가??

Access Token은 담고 있는 정보의 민감도와 무관하게, 그 자체로 권한을 행사할 수 있는 자격 증명이다.
따라서 핵심은 어디에 담느냐보다 노출 가능성을 어떻게 최소화하느냐에 있다고 판단했다.

쿠키는 다음 조건이 충족될 경우, 의도하지 않은 노출을 상당 부분 통제할 수 있다.

  • HttpOnly 설정을 통해 JavaScript 접근 차단
  • Secure 설정 및 Nginx를 통한 HTTP 접근 차단
  • SameSite 정책을 통한 CSRF 방어

반면 Authorization 헤더 방식은 프론트엔드가 토큰을 저장하고 요청마다 직접 첨부해야 하며, 이 과정에서 구현 복잡도와 함께 토큰 노출 가능성이 증가할 수 있다. 이에 본 프로젝트에서는 Access Token과 Refresh Token을 모두 HttpOnly 쿠키로 전달하고, 토큰은 브라우저 내부에서만 관리되도록 하여 애플리케이션 코드에서 직접 접근할 수 없도록 설계했다.

 

Admin 시스템의 특성상, 프론트엔드 공격 표면을 최대한 줄이는 것이 더 중요하다고 판단했기 때문이다.

 

물론 이 구조는:

  • CSRF 방어 설정(SameSite, HTTPS 등)에 강하게 의존하며
  • 브라우저 정책 변경에 민감하고
  • Access Token은 발급 후 만료 전까지 즉시 폐기하기 어렵다

라는 단점이 존재한다. 따라서 본 설계는 AccessToken TTL을 짧게 가져가고, RTR로 Refresh를 통제한다는 전제 하에서만 성립한다.


👀 결론

실무 환경에서 JWT 로그인을 직접 구현해보며 명확하게 느낀 점이 있다.

보안 설계를 하면서 끊임없이 스스로에게 “왜?”, “이게 정말 최선인가?”라는 질문을 던지다 보면,
깊이 파고들수록 끝이 보이지 않는 망망대해 앞에 서 있는 기분이 들었다.


아무리 최선을 다해도, 보안이 100% 완벽하게 지켜진다고 말하기는 어렵다는 사실도 함께 깨닫게 되었다.

그래서 더더욱, ‘완벽한 보안’보다는 ‘이 서비스에 맞는 보안’이 무엇인지에 집중하려 했다.
막연히 더 강한 기술을 선택하기보다, 이 서비스의 사용자, 트래픽, 운영 환경, 그리고 실제 위협 모델을 하나씩 되짚어보며 감당할 수 있는 위험과 감수해서는 안 되는 위험을 구분하려 노력했다.

 

그 결과로

  • JWT를 사용했고
  • 블랙리스트 대신 RTR를 선택했으며
  • Redis 도입을 보류했고
  • Access Token과 Refresh Token을 모두 HttpOnly 쿠키로 전달하는 구조를 택했다

이 선택들이 언제나 옳다고 주장하고 싶지는 않다.
다만 적어도, 왜 이렇게 설계했는지에 대해서는 명확하게 설명할 수 있는 구조를 만들고 싶었다.

 

보안 설계는 정답을 맞히는 문제가 아니라, 상황에 대한 이해와 책임 있는 선택의 문제라고 생각한다.
그리고 이 글은 그 선택의 결과를 정리한 하나의 기록이다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.