티스토리 뷰
개요
웹의 기반이 되는 HTTP프로토콜은 근본적으로 무상태성(Stateless)이라는 특징을 가진다. 이는 서버가 클라이언트의 이전 요청을 기억하지 못한다는 의미이다. 모든 요청은 독립적인 것으로 취급되기 때문에, 서버는 방금 전 통신한 클라이언트가 다시 요청을 보내도 그 클라이언트가 누구였는지 알지 못한다.
마치 서버가 단기 기억상실증에 걸린 것처럼, 매번 "누구시죠?"라고 묻는 것과 같다. 이러한 방식은 서버의 부담을 줄이고 확장성을 높이는 장점이 있으나, 로그인 유지나 장바구니 기능 등 연속적인 상태가 필요한 서비스를 구현하기에는 어려움이 있다.
쿠키(Cookie)
쿠키는 HTTP의 무상태성을 보완하기 위해 등장한 것이 쿠키이다. 쿠키는 서버가 클라이언트(브라우저)에 데이터를 저장하고, 클라이언트가 서버에 요청할 때마다 그 데이터를 함께 보내도록 한다.
서버의 쿠키 발급은 응답헤더에 Set-Cookie를 포함하여 응답 메시지로 전송한다. 클라이언트(브라우저)는 서버로부터 받은 쿠키를 브라우저에 저장한다. 이후 브라우저는 해당 서버에 요청을 보낼 때마다, 요청 헤더에 저장해 둔 쿠키를 자동으로 포함하여 전송하게 된다. 서버는 이 쿠키를 보고 클라이언트가 누군지 식별하게 된다.

[서버 응답 메시지]
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: userId=user123
[클라이언트 요청 메시지]
GET /user/info HTTP/1.1
Host: www.example.com
Cookie: userId=user123
쿠키는 위와 같이 이름-값 쌍으로 저장된다. 또한, 다양한 속성을 설정하여 쿠키의 만료일(Expires, Max-Age), 사용범위(Domain, Path), 보안(HttpOnly, Secure, SameSite)등을 제어할 수 있다. (MDN 문서 참고)
쿠키의 보안 위험성
앞서 쿠키는 클라이언트 브라우저에 저장된다고 했다. 이는 곧 사용자가 쿠키의 내용을 수정할 수 있으며, XSS(Cross-Site Scripting)과 같은 공격을 통해 제3자에게 탈취될 위험이 있다는 것을 말한다.
물론, 쿠키가 보안 속성을 지원하기 때문에 이러한 위험을 완화할 수는 있으나, 만약 로그인 아이디, 개인정보 등 민감하고 중요한 정보를 쿠키에 그대로 담아둔다면, 이 정보가 유출되었을 때 심각한 보안사고로 이어질 수 있다.
세션(Session)
세션은 데이터를 클라이언트 측이 아닌 서버 측에 저장하고, 클라이언트에게는 그 정보에 접근할 수 있는 열쇠(Session ID)만 제공하는 방식이다. 그럼 Session ID는 클라이언트 어디에 저장될까? 클라이언트 측에 데이터를 저장할 공간은 사실상 쿠키밖에 없다. 그렇다면 여전히 쿠키에 저장하니까 보안에 취약한 것이 아닐까?
결론부터 말하면, 훨씬 안전하다. 핵심적 차이는 쿠키에 저장되는 데이터의 의미에 있다. 기존 방식처럼 'userId=user123'과 같이 직접적인 정보를 쿠키에 담는 대신, 세션 방식은 'sessionId=asdf-1234-qwer-5678'처럼 아무 의미 없는 긴 무작위 문자열을 쿠키에 저장한다. 만약 이 세션 ID가 탈취되더라도, 공격자는 이 문자열만으로는 어떠한 유의미한 정보도 얻을 수 없다. 서버는 이 세션 ID를 받아 세션 저장소에서 일치하는 실제 사용자 정보를 찾아 내부적으로 작업을 처리하기 떄문이다.
세션의 동작 방식은 다음과 같다. 클라이언트가 로그인에 성공하면, 서버는 고유한 세션 ID를 생성하고 이 ID와 연결된 세션을 만들어 세션 저장소에 보관한다. 이 세션 저장소에는 사용자의 아이디, 로그인 여부 등 필요한 정보가 저장된다.
서버는 생성된 세션 ID를 Set-Cookie헤더를 통해 클라이언트에게 전달한다. 클라이언트는 이 세션 ID를 쿠키에 저장하고, 이후 /user/info와 같이 인증이 필요한 페이지에 접근할 경우 브라우저는 쿠키(세션 ID)를 요청 헤더에 자동으로 포함하여 서버에 전송한다.
서버는 요청에 포함된 세션 ID를 받아 세션 저장소에서 일치하는 정보가 있는지 조회한다. 서버는 해당 사용자 정보를 찾아 확인 후, 해당 페이지를 인가할 수 있는 사용자인지 확인한 다음 클라이언트에게 응답한다.

세션의 분산 환경에서의 한계
세션 방식은 단일 서버 환경에서 효과적이지만, 현대 분산 환경에서는 문제점이 있다. 아래 예시를 한번 살펴보자.
사용자가 서버1에 로그인하여 세션을 생성했다고 가정한다면, 서버1의 세션 저장소(메모리)에는 해당 사용자의 세션 정보가 저장된다. 하지만 사용자의 다음 요청이 로드 밸런서에 의해 서버2로 전달되게 된다면 문제가 발생한다.(로드 밸런서는 라운드 로빈으로 동작한다고 가정한다.)
서버2에는 해당 사용자의 세션 정보가 존재하지 않기 때문에, 서버2는 세션 ID를 보고 어떤 사용자인지 알 수 없게 된다. 결과적으로 사용자는 로그인 상태임에도 불구하고 다시 로그인하라는 메시지를 받게 된다.


특정 사용자의 모든 요청을 항상 동일한 서버로 라우팅하는 방법이다. 하지만 해당 서버에 장애가 발생하면 그 서버의 속한 모든 세션이 한 번에 사라진다. 또한 특정 서버에만 트래픽이 집중되어 부하 분산의 효과를 제대로 누리지 못하게된다.
세션 클러스터링
모든 서버가 세션 정보를 공유하도록 하는 방식이다. 하지만 서버가 증가할수록 세션 동기화를 위한 네트워크 트래픽이 기하급수적으로 늘어나고, 한 서버의 장애가 전체 클러스터에 영향을 줄 수 있다.
결국 세션 방식으로 분산환경에서의 확장성을 확보하려면 추가적인 인프라 비용(외부 세션)과 복잡도를 감수해야 한다. 이러한 세션의 근본적인 한계 때문에 최근에는 서버 상태에 의존하지 않는 JWT같은 토큰 기반 인증 방식이 각광받고 있다.
토큰 인증(JWT 기준)
토큰 기반 인증은 세션 기반 인증과 달리, 서버가 사용자의 상태 정보를 직접 저장하지 않는 무상태(Stateless) 방식이다. 인증에 성공한 클라이언트에게 암호화된 토큰을 발급하고, 클라이언트는 이 토큰을 저장해 두었다가 서버에 요청을 보낼 때마다 HTTP 헤더에 담아 인증을 수행한다.

결론부터 말하자면, 토큰도 클라이언트에 저장되므로 위변조 및 탈취의 위험은 존재하지만, 이를 보완하는 메커니즘이 존재한다.
토큰의 위변조 방지: 전자 서명(Signature)
토큰의 위변조를 막는 핵심 기술은 바로 전자 서명(Signature)이다. 이 원리를 이해하기 위해, 토큰의 대표적인 JWT(JSON Web Token)의 구조를 먼저 살펴보자.
JWT는 마침표(.)를 구분자로 하여, Header, Payload, Signature라는 세부분으로 구성된 문자열이다.

- Header: 토큰의 타입(JWT)과 사용할 해시 알고리즘(예: HS256) 정보가 담긴다.
- Payload: 사용자 ID, 권한 등 실질적인 데이터가 담긴다.
- Signature: 가장 중요한 부분으로, 인코딩된 Header와 Payload를 Base64 URL로 인코딩한 문자열을 서버만 아는 비밀키(Secret Key)로 해싱하여 Base64 URL로 다시 인코딩한 값이다. 이 서명을 통해 서버는 토큰이 위변조되지 않았음을 검증할 수 있다.

이제 실제로 주어지는 데이터로 생성한 JWT의 Signature값과, 위에서 설명한 과정을 코드로 구현했을 때의 결과가 정확히 일치하는지 테스트를 통해 확인해보자.
Signature 테스트

위 데이터로 생성된 전체 JWT는 다음과 같으며, 여기서 세 번째 부분인 Signature값이 Header와 Payload를 Base64 URL로 인코딩한 후, Secret key로 Header에 명시한 해시함수를 적용한 값을 인코딩한 것이 동일한 값인지 확인해야한다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJlZ28yIiwiaWF0IjoxNTE2MjM5MDIyfQ.W4163odVS4KNKBJ0AYmNNchvcjqToTcEcn6aibsm-oc
@Test
void JWT_테스트() throws NoSuchAlgorithmException, InvalidKeyException {
// given
String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
String payLoad = "{\"sub\":\"1234567890\",\"username\":\"ego2\",\"iat\":1516239022}";
String secret = "hello";
String expectedSignature = "W4163odVS4KNKBJ0AYmNNchvcjqToTcEcn6aibsm-oc";
// 헤더와 페이로드를 Base64Url로 인코딩
Base64.Encoder encoder = getUrlEncoder().withoutPadding();
String encodedHeader = encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8));
String encodedPayload = encoder.encodeToString(payLoad.getBytes(StandardCharsets.UTF_8));
// 2. 서명할 데이터 준비 (인코딩된 헤더 + "." + 인코딩된 페이로드)
String dataToSign = encodedHeader + "." + encodedPayload;
// when
// HMACSHA256 알고리즘을 사용하여 서명 생성
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
// 서명 결과를 Base64Url로 인코딩
String generatedSignature = encoder.encodeToString(signatureBytes);
// then
System.out.println("Encoded Header: " + encodedHeader);
System.out.println("Encoded Payload: " + encodedPayload);
System.out.println("Generated Signature: " + generatedSignature);
Assertions.assertThat(generatedSignature).isEqualTo(expectedSignature);
}
테스트를 실행하면 생성한 generatedSignature가 expectedSignature와 정확히 일치하는 것을 확인할 수 있다. 만약 secret key가 다르다면 바로 실패하게 된다. 일반적으로 서버에만 저장되어 있는 Secret Key가 탈취되지 않는 이상, Signature는 절대로 위변조될 수 없다.


토큰 탈취 대응: 리프레시 토큰
서명(Signature) 덕분에 토큰의 위변조는 불가능하다는 것을 확인했다. 하지만 여전히 남은 문제가 있다. 바로 토큰 탈취이다.
만약 공격자가 사용자의 토큰 자체를 탈취하여 요청을 보낸다면, 서버 입장에서는 정상적인 토큰이므로 인가된 사용자로 판단할 수 밖에 없다. 이러한 토큰 탈취 공격에 대응하기 위해 액세스 토큰(Acess Token)과 리프레시 토큰(Refresh Token)이라는 두 가지 종류의 토큰을 사용하는 전략이 고안된다.
엑세스 토큰을 실질적 인증을 위해 사용하고, 유효기간을 짧게 가져가 탈취되더라도 공격자가 사용할 수 있는 시간을 제한적이게 한다. 리프레시 토큰은 만료된 액세스 토큰을 재발급하기 위해 사용되는 용도의 토큰으로 유효기간을 보통 엑세스 토큰 보다 길게 가져간다.
그렇다면 리프레시 토큰도 탈취된다면 어떻게 될까?
공격자는 이 토큰으로 계속해서 새로운 액세스 토큰을 발급하여 마스터 키처럼 사용할 수 있을 것이다. 이를 방지하기 위해 리프레시 토큰 순환(Refresh Token Rotation)방법을 사용한다. 리프레시 토큰을 일회용을 만들어서, 액세스 토큰을 갱신할 때마다 리프레시 토큰도 새로운 리프레시 토큰으로 갱신하는 방식이다. 이 전략을 사용하면, 공격자가 훔친 리프레시 토큰을 사용하려는 순간 서버는 이미 사용된 무효화된 토큰이 사용된 것을 감지할 수 있다. 서버는 이를 탈취 시도로 간주하고 조치를 취할 수 있게 된다.

마무리
지금까지 웹의 무상태(Stateless) 특성을 극복하기 위한 쿠키, 세션, 토큰에 대해 살펴보았다. 각 기술은 어떤 것이 절대적으로 우월하다기 보다 각자의 장단점을 이해하고 상황에 맞는 기술을 선택하는 것이 중요해 보인다.
세션 기반 인증은 서버가 모든 상태를 직접 관리하므로 구현이 직관적이고, 특정 사용자를 강제로 로그아웃시키는 등 통제가 용이하다. 따라서 단일 서버로 운영되는 전통적인 웹 서비스에서는 여전히 강력하고 효과적인 방식이다. 하지만 분산 환경에서는 확장성을 확보하기 위해 별도의 인프라 비용과 복잡도를 감수해야 하는 명확한 한계가 있다.
반면, 토큰 기반 인증은 서버의 무상태를 유지하여 확장성 측면에서 압도적인 우위를 보인다. 특히 수평 확장이 필수적인 MSA 환경에서 매우 유리하다. 하지만 토큰 방식에도 단점은 존재한다. Payload에 담는 정보가 많아질수록 토큰의 길이가 길어져 매 요청마다 네트워크에 부하를 줄 수 있으며, 세션과 달리 즉각적인 사용자 제어가 어렵다. 토큰은 한번 발급되면 만료 전까지 유효하므로, 이를 보완하기 위해서는 별도의 토큰 블랙리스트와 같은 추가적인 구현이 필요하다.
결론적으로, 안전하고 확장 가능한 웹 서비스를 구축하기 위해 이러한 인증 방식들의 기본 원리와 장단점을 깊이 이해하고, 우리가 만들고자 하는 서비스의 아키텍처에 가장 적합한 기술을 선택하여 적용하는 것이 백엔드 엔지니어의 중요한 역량이 아닐까 생각된다.
참고 자료
- 세션 기반 인증과 토큰 기반 인증 (feat. 인증과 인가), https://hudi.blog/session-based-auth-vs-token-based-auth/
- 쿠키와 세션 (ft. HTTP의 비연결성과 비상태성), https://hudi.blog/cookie-and-session/
- MDN Set-Cookie, https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/Set-Cookie
- JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)
- Refresh Token Rotation 과 Redis로 토큰 탈취 시나리오 대응, https://junior-datalist.tistory.com/352
'Computer Science' 카테고리의 다른 글
| 멀티 스레드 환경에서의 락(Lock) 메커니즘 (0) | 2025.08.07 |
|---|---|
| Zero-Copy 실전 적용: 정적파일 전송 최적화 (1) | 2025.06.23 |
| 메모리 복사를 최소화하는 기술, Zero Copy (0) | 2025.06.18 |
| I/O장치와 인터럽트 그리고 DMA (0) | 2025.06.14 |
| C10K 문제로 살펴보는 서버 아키텍처와 커널 I/O의 진화 (2) | 2025.05.05 |