-
OAuth2, JWT, Session 기반 인증의 발전 과정 : JWT는 Stateless한 방식일까? 아닐까?백엔드/기능 2025. 7. 6. 20:18
소셜로그인 백엔드 개발을 진행하면서, 기본적인 인증 발전 과정에 대해 간략히 정리해보고자 한다.
더불어 JWT 방식이 RESTful 하다고 말할 수 있는지에 대한 의문도 함께 정리해보고자 한다.
1. 인증 방식의 발전 흐름
1-1. 세션 기반 인증: 서버가 모든 걸 기억하던 시절...
초창기 웹 서비스는 간단했음
- 사용자가 로그인하면 서버가 사용자의 정보를 저장하고,
- 고유 식별자인 세션 ID(session id)를 생성해서 클라이언트에 쿠키(cookie)로 전달했음.
- 그다음부터 클라이언트는 요청할 때마다 쿠키에 저장된 세션 ID를 서버에 보내고,
- 서버는 그걸 기준으로 로그인 상태를 확인함.
// 1. 사용자가 로그인 - 서버가 세션 생성하고 그 안에 아래 데이터 저장 { "userId": "12345", "username": "홍길동", "roles": ["USER"], "cart": ["상품1", "상품2"] } // 2. 클라이언트는 다음 요청 때 세션 ID만 보내고 // 3. 서버는 이 세션 ID에 해당하는 상태를 불러와서 사용 - stateful여기서의 문제는 확장성 이었다고 함
서버가 상태를 유지해야 하니까 서버를 여러 대로 확장하면 세션 정보를 공유하는 데 Redis 같은 중앙 저장소가 필요함.
사용자가 많아질수록 서버 메모리나 Redis 부하가 커지고, 서비스 규모가 커질수록 관리가 어려워짐.더보기<참고> 세션?
1. 세션이란?
서버가 사용자의 로그인 상태나 일시적인 데이터를 유지하는 저장 공간을 말함
이 세션을 구분하기 위한 고유한 ID가 세션ID임
2. 세션의 동작 구조
- 사용자가 로그인 요청을 보냄
- 서버는 사용자의 정보를 메모리에 저장함 (세션 생성)
- 이 세션을 구분하기 위해 무작위 값(세션 ID)를 생성
- 서버는 클라이언트에게 이 세션 ID를 응답으로 돌려줌<참고> 클라이언트의 쿠키(Cookie)?
1. 쿠키란?
클라이언트(브라우저)에 저장되는 작은 데이터 조각
HTTP 요청시마다 자동으로 서버로 전송됨
2. 쿠키의 동작 방식
- 서버가 세션 ID를 생성해서 클라이언트에 돌려줄 때 아래와 같은 방식으로 응답 헤더에 포함해서 보냄
Set-Cookie: SESSION_ID=abc123xyz456; Path=/; HttpOnly;
- 클라이언트(브라우저)는 이 값을 저장해뒀다가,
- 다음 요청 때 자동으로 포함해서 서버로 보냄
Cookie: SESSION_ID=abc123xyz4561-2. OAuth2의 등장: 외부 서비스 연동 문제 해결
OAuth2의 등장 : 2012년 10월 RFC6749로 표준화
웹 서비스 규모가 커지고, 구글/애플/카카오 같은 외부 서비스와의 연동이 필수요건이 됨.
OAuth는 이런 상황에서 타사 서비스가 사용자 정보를 대신 인증해주는 권한 위임(Authorization Delegation) 방식을 제공함.중요한 점은, OAuth2는 인증(Authentication) 자체가 아니라 권한 위임을 위한 프로토콜이라는 것.
실제 사용자를 인증하는 건 OAuth2 위에 얹어지는 서비스 로직이 필요함.그래서 OAuth2 인증 이후 서버에서 별도로 로그인 처리를 구현해야 함.
더보기<참고> OAuth2는 어떤 프로토콜인가?
OAuth 2.0은 인터넷 사용자(Resource Owner)가 자신의 리소스(Resource Server)에 대한 액세스 권한을 타 애플리케이션(Client)에 위임하기 위한 권한 위임(Authorization Delegation) 프로토콜이다.
(인증이나 로그인 프로토콜이 아님!X)
즉, 사용자 인증(Authentication)을 처리하지 않으며, 타 애플리케이션이 사용자로부터 권한을 위임받아 리소스 서버의 보호된 리소스에 접근할 수 있도록 허용하는 권한 위임과 관련된 프로토콜임
해당 프로토콜은 아래의 사항을 기술함.
- 어떤 URL로 요청해야 하는지
- 어떤 파라미터를 포함해야 하는지 (client_id, redirect_uri 등)
- 응답으로 어떤 토큰이 어떻게 반환되는지
- 에러 시 어떤 에러 코드가 나오는지
위 사항들을 RFC 6749라는 공식 문서로 정해놓은 것
이걸 확장해서 "로그인"으로 활용하려고 만든 게 OpenID Connect (OIDC).
OIDC는 OAuth2를 기반으로 하고, 사용자 인증을 보장하는 계층이라고 할 수 있음 -> 추후에 다룰 것1-3. JWT의 등장: 확장성 좋은 인증 방식의 필요성
JWT의 등장 : 2010년 12월 RFC7519에서 초안 발표, 2015년 5월에 정식 표준 제정
* JWT는 OAuth2보다 먼저 등장했으나, 표준이 되기까지 시간이 걸렸음. 생태계에서 두 방식이 결합되어 쓰이기 시작한 것은 OAuth2의 등장 이후부터.서버는 요청만 처리하고 상태를 저장하지 않음으로써(stateless) 확장성과 유연성을 확보하기 위해, RESTful API를 지향함.
즉, RESTful API의 근본적인 설계 철학이 Stateless (<-> Stateful)라고 할 수 있음
<참고 1> Stateless란? 서버가 클라이언트의 상태를 저장하지 않는 것을 의미
각 요청(Request)는 독립적이며, 서버는 이전에 이 클라이언트가 어떤 요청을 보냈는지, 어떤 상태였는지 기억하지 않음<참고 2> RESTful API란? REST(Representational State Transfer)란 웹 서비스가 지켜야 할 설계 원칙과 구조적 스타일을 제안한 것으로, RESTful API는 REST의 원칙에 맞게 설계된 API를 의미
- Client-Server 구조 : 클라이언트(웹/앱)와 서버가 역할을 분리, 서버는 데이터 관리, 클라이언트는 UI/UX 담당
- Stateless(무상태성) : 서버는 요청 간의 상태를 저장하지 않음, 매 요청마다 필요한 모든 정보 포함 필요
- Cacheable(캐시가능성) : 서버 응답에 캐싱 가능 여부를 명확히 설정 - 네트워크 효율 향상
- Uniform Interface(일관된 인터페이스) : 리소스는 고유한 URI로 식별, 행위는 HTTP 메서드로 구분
GET /users/1 - 사용자 조회
PUT /users/1 - 사용자 수정
DELETE /users/1 - 사용자 삭제
- Layered System(계층 구조) : API 서버는 여러 계층으로 구성 가능 (ex: 로드밸런서, 인증 프록시, 캐시 서버 등)
- Code on Demand(옵션) : 필요시 서버가 클라이언트에 코드를 전송 가능 (현대의 REST API에서는 거의 사용X)서버가 확장되고, 모바일 앱이 활성화되면서 Stateless한 인증 방식이 필요해짐.
이때 나온 게 JWT (JSON Web Token). JWT는 토큰 안에 로그인 관련 정보를 담고, 서버는 상태를 저장하지 않아도 토큰만으로 사용자를 확인할 수 있음.덕분에 서버 확장 시 별도의 세션 공유 없이 인증이 가능해짐.
// 1. 사용자가 로그인 → 서버가 JWT 발급 // 2. JWT 안에 이런 정보가 포함됨: { "sub": "12345", "name": "홍길동", "roles": ["USER"] } // 3. 클라이언트가 매번 이 JWT를 보내면, // 4. 서버는 JWT 안의 정보만으로 사용자를 확인하고 처리함 (별도 상태 저장 X) // → 서버가 상태를 저장하지 않으니까 stateless2. OAuth2 + JWT 조합: 현대 인증의 기본 구조
요즘 서비스는 보통 이렇게 사용함:
- OAuth2를 통해 외부 서비스(구글, 애플 등)로 사용자 인증
- 인증 결과를 바탕으로 우리 서버에서 JWT 발급
- 클라이언트는 JWT를 들고 다니면서 인증 유지
이렇게 하면 서버는 사용자의 인증 상태를 저장할 필요가 없고, 클라이언트는 매 요청마다 JWT를 포함해서 자신을 인증함.
3. OAuth2 로그인 흐름 (Authorization Code 방식)
OAuth2의 여러 방식 중에서 모바일 앱/웹에서 가장 많이 쓰는 Authorization Code 방식을 기준으로 보면 아래와 같음.
- 사용자가 앱에서 "Apple로 로그인" 버튼 클릭
- 앱이 Apple 인증서버에 로그인 요청 (Client ID, Redirect URI 포함)
- 사용자가 Apple ID로 로그인하고 권한 동의
- Apple이 설정한 Redirect URI로 인증 코드 발급
- 앱이 인증 코드를 서버에 전달
- 서버가 Apple에 인증 코드로 Access Token 요청
- Apple이 Access Token과 ID Token을 반환
- 서버가 ID Token을 파싱해서 사용자 정보를 확인
- 서버가 자체 JWT를 발급해서 클라이언트에 전달
- 이후부터 클라이언트는 이 JWT를 Authorization 헤더에 담아서 요청
4. JWT 구조와 발급 방법
JWT는 총 3개 영역으로 구성됨:
- Header: 토큰 타입, 암호화 알고리즘
- Payload: 사용자 ID, 만료 시간 등 인증 정보
- Signature: 서버 비밀키로 암호화한 값 (변조 방지)

JWT Structure JWT 생성 예시 (Spring Boot + jjwt 사용)
public String generateToken(String userId) { Date now = new Date(); Date expiry = new Date(now.getTime() + 3600 * 1000); // 1시간 만료 return Jwts.builder() .setSubject(userId) .setIssuedAt(now) .setExpiration(expiry) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }JWT 검증 예시
public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); }5. JWT의 보안 문제 (Refresh Token 도입)
JWT는 서버가 상태를 저장하지 않아도 돼서 편하지만, 문제는 보안.
- JWT가 탈취당하면 만료 시간 전까지는 누구나 사용할 수 있음.
- 서버가 JWT를 무효화할 방법이 없음.
그래서 나온 게 Access Token + Refresh Token 구조
- Access Token: 만료 시간이 짧음 (ex: 30분)
- Refresh Token: 만료 시간이 길고, 주로 서버가 저장해서 관리
Access Token이 만료되면 클라이언트는 Refresh Token으로 새 Access Token을 요청함.
6. JWT 방식은 완전히 Stateless일까?
여기서 중요한 점이 하나 있음.
JWT 인증 방식이 Stateless라고 하지만, Refresh Token을 서버에 저장하면 결국 일부 stateful이 되는 게 아닐까?
맞음.
Refresh Token을 저장하니까 완전한 Stateless는 아님. 다만 이 구조는 아래처럼 이해하면 됨:
- Access Token 검증 → Stateless
- Refresh Token 관리 → Minimal stateful
- 필요에 따라서 토큰 블랙리스트 관리 → stateful
대부분의 요청은 Access Token 검증만으로 끝나기 때문에 서버 부하가 거의 없음.
Refresh Token은 만료 연장이나 보안 관리를 위해 최소한의 상태만 저장함.결론 : JWT 인증 방식(Refresh Token을 곁들인..)은 확장성과 보안의 현실적인 절충안으로 등장한 개념으로 생각할 수 있음
JWT 기반 인증 자체는 stateless한 방식(자체 포함 토큰이기 때문에 서버에 저장하지 않아도 됨)이지만, JWT를 운영하는 전체 시스템이 반드시 완전한 stateless는 아님다음 글에서는 SpringSecurity + OAuth2 + JWT를 이용한 애플로그인 구현에 대해 정리해보고자 함.