5 minute read

HTTP의 무상태성(Stateless)

HTTP는 기본적으로 무상태(Stateless) 프로토콜입니다. 각 요청은 독립적으로 처리되며, 서버는 이전 요청에 대한 정보를 기억하지 않습니다. 이런 특성은 서버 확장성에 유리하지만, 사용자 인증이나 장바구니 기능과 같이 상태 유지가 필요한 서비스에서는 제약이 됩니다. 즉 HTTP 프로토콜은 “stateless”한 특성을 가지기 때문에 서버는 클라이언트가 누구인지 매번 확인해야 합니다. 이를 해결하기 위해 쿠키, 세션, JWT와 같은 상태 유지 기술이 사용됩니다.


1. 쿠키(Cookie)

쿠키는 서버가 클라이언트의 웹 브라우저에 저장하는 작은 데이터 조각입니다. 클라이언트는 동일한 서버에 요청을 보낼 때마다 저장된 쿠키를 함께 전송합니다.

동작 원리

쿠키-동작-원리

서버는 HTTP 응답 헤더의 Set-Cookie를 통해 쿠키 생성을 지시합니다. 브라우저는 쿠키를 저장하고 이후 요청에서 브라우저는 Cookie 헤더에 쿠키를 포함시켜 서버로 전송합니다.

쿠키 속성

  • Domain: 쿠키가 전송될 도메인 지정
  • Path: 쿠키가 유효한 경로 지정
  • Expires/Max-Age: 쿠키 만료 시간 설정
  • Secure: HTTPS 연결에서만 쿠키 전송
  • HttpOnly: JavaScript를 통한 쿠키 접근 방지 (XSS 공격 방어)
  • SameSite: CSRF 공격 방어를 위한 설정 (None, Lax, Strict)

참고

  • 쿠키는 클라이언트 측에 저장되어 변조될 수 있으므로, 민감한 정보는 저장하지 않아야 함
  • HttpOnlySecure 속성을 적절히 사용하여 보안 강화
  • 쿠키 크기는 일반적으로 4KB로 제한됨
  • 사이트당 쿠키 개수도 브라우저마다 제한이 있음 (약 50-75개)
// Spring Boot 쿠키 설정 간단 예시
@RestController
public class CookieController {
    
    @GetMapping("/setCookie")
    public ResponseEntity<String> setCookie() {
        // 쿠키 생성
        ResponseCookie cookie = ResponseCookie.from("sessionId", "abc123")
            .httpOnly(true)  // 클라이언트에서 JavaScript로 접근할 수 없게 설정 (보안 목적)
            .secure(true)    // HTTPS 연결에서만 쿠키를 전송하도록 설정
            .maxAge(Duration.ofHours(1))  // 쿠키의 만료시간을 1시간으로 설정
            .sameSite("Strict")  // 동일 사이트에서만 쿠키를 전송하도록 설정
            .path("/")  // 쿠키가 유효한 경로 설정 (전체 경로에 대해 유효)
            .build();
        
        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())  // 응답 헤더에 쿠키를 설정
            .body("쿠키가 설정되었습니다");  // 응답 본문에 메시지를 담음
    }
    
    @GetMapping("/getCookie")
    public String getCookie(@CookieValue(name = "sessionId", required = false) String sessionId) {
        if (sessionId != null) {
            return "쿠키 값: " + sessionId;  // 쿠키가 있으면 쿠키 값을 반환
        } else {
            return "쿠키가 없습니다";  // 쿠키가 없으면 없는 경우 처리
        }
    }
}

쿠키 전송 예입니다. 쿠키

이 헤더는 클라이언트에게 쿠키를 저장하라고 알려줍니다.

쿠키

이제 서버에 대한 각각의 새로운 요청마다 브라우저는 Cookie 헤더를 사용하여 이전에 저장된 모든 쿠키를 서버로 다시 전송합니다.


2. 세션(Session)

세션은 서버 측에서 사용자 상태를 유지하는 방법으로, 세션 식별자만 클라이언트에 저장하고 실제 데이터는 서버에 보관합니다.

동작 원리

세션

사용자가 처음 접속하면 서버는 유니크한 세션 ID 생성합니다. 이후 세션 ID를 쿠키에 저장하여 클라이언트에 전송하며 세션 ID에 해당하는 사용자 데이터를 메모리, 데이터베이스 등에 저장합니다. 클라이언트가 재접속하면 쿠키의 세션 ID를 통해 서버에서 해당 사용자 데이터 조회합니다.

세션 저장소

  • 메모리: 빠르지만 서버 재시작 시 모든 세션 손실, 확장성 제약
  • 데이터베이스: 영구적이지만 속도 저하
  • Redis와 같은 인메모리 데이터 스토어: 빠른 접근과 영속성 사이의 균형

참고

  • 세션은 서버 자원을 사용하므로 많은 동시 사용자가 있을 때 메모리 부하 발생
  • 세션 만료 시간 설정으로 리소스 관리 필요
  • 수평적 확장(서버 여러 대) 시 세션 공유 전략 필요 (Sticky Session, 중앙화된 세션 저장소)
  • 세션 하이재킹 공격 방지를 위한 대책 필요
// Spring Boot 세션 사용 예시

/* application.properties 또는 application.yml 설정 */
// 메모리 세션 저장소 설정
spring.session.store-type=none
// 또는 Redis를 세션 저장소로 설정
spring.session.store-type=redis
spring.redis.host=localhost  // Redis 서버 호스트 설정
spring.redis.port=6379  // Redis 서버 포트 설정
// 세션 타임아웃 설정 (초 단위, 30분)
spring.session.timeout=1800
/* SessionConfig.java */
@Configuration  
@EnableRedisHttpSession  // Redis 세션 저장소 사용을 활성화
public class SessionConfig {

    // Redis 세션 저장소 연결을 위한 빈을 설정
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();  // Lettuce는 Redis 클라이언트 중 하나
    }
}
/* SessionController.java */
@RestController  // 이 클래스는 REST API를 처리하는 컨트롤러임을 나타냄
public class SessionController {

    // 로그인 처리 메서드
    @PostMapping("/login")
    public String login(HttpSession session, @RequestBody LoginDto loginDto) {
        // 사용자 인증 로직 (생략)
        User user = userService.authenticate(loginDto);  // 사용자 인증 수행
        
        if (user != null) {
            // 인증 성공 시 세션에 사용자 정보 저장
            session.setAttribute("userId", user.getId());  // 세션에 사용자 ID 저장
            session.setAttribute("isAuthenticated", true);  // 세션에 인증 상태 저장
            
            // 세션 타임아웃 설정 (30분)
            session.setMaxInactiveInterval(1800);  // 세션의 비활성화 시간 (30분)
            
            return "로그인 성공";
        }
        
        return "로그인 실패";
    }
    
    // 프로필 조회 메서드
    @GetMapping("/profile")
    public String profile(HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");  // 세션에서 사용자 ID 가져오기
        Boolean isAuthenticated = (Boolean) session.getAttribute("isAuthenticated");  // 세션에서 인증 상태 가져오기
        
        if (userId != null && isAuthenticated != null && isAuthenticated) {
            // 인증된 사용자일 경우
            return "사용자 프로필: " + userId;
        }
        
        return "인증이 필요합니다";
    }
    
    // 로그아웃 처리 메서드
    @PostMapping("/logout")
    public String logout(HttpSession session) {
        session.invalidate();  // 세션을 무효화하여 로그아웃 처리
        return "로그아웃 성공";
    }
}


3. JWT (JSON Web Token)

JWT는 당사자 간에 정보를 안전하게 JSON 객체로 전송하기 위한 간결하고 독립적인 방법을 정의하는 개방형 표준(RFC 7519)입니다. 이 정보는 디지털 서명되어 있어 신뢰할 수 있습니다.

JWT 구조

jwt-구조

  1. 헤더(Header): 토큰 유형과 사용된 서명 알고리즘 지정
    {
      "alg": "HS246",
      "typ": "JWT"
    }
    
  2. 페이로드(Payload): 클레임(claim)이라 불리는 엔티티와 추가 데이터 포함
    {
      "sub": "1234567890",     // subject
      "name": "Bob",           // custom claim
      "iat": 1516239022,       // issued at
      "exp": 1516242622        // expiration time
    }
    
  3. 서명(Signature): 헤더와 페이로드를 Base64Url로 인코딩한 후 비밀키로 서명
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )
    

동작 원리

jwt-동작-원리

사용자 인증 후 서버가 JWT 생성해 클라이언트에 반환합니다. 클라이언트는 모든 요청 시 Authorization 헤더에 JWT 포함하며 서버는 JWT 검증으로 요청의 인증/권한 확인합니다. (토큰 만료 시 재발급 필요)

토큰 저장 위치

  • 일반적으로 localStorage나 sessionStorage에 저장
  • XSS 공격에 취약할 수 있어 httpOnly 쿠키 사용도 고려

참고

  • JWT는 서명되어 있지만 암호화되지 않았으므로 민감 정보 포함 금지
  • 서버 측에서 토큰 무효화가 어려움 (토큰 블랙리스트 구현 필요)
  • 토큰 크기가 쿠키보다 클 수 있어 요청 크기 증가
  • Refresh Token과 Access Token 패턴
    • Access Token: 짧은 유효기간, 리소스 접근 용도
    • Refresh Token: 긴 유효기간, 새 Access Token 발급 용도

(jwt 구현할 때 개발자 유미라는 분께서 잘 정리해주신게 있으니 참고)

4. 기술 비교 및 선택 기준

쿠키 vs 세션

  • 보안: 세션이 쿠키보다 안전 (데이터가 서버에 저장)
  • 성능: 쿠키가 서버 부하 적음, 세션은 서버 리소스 사용
  • 확장성: 쿠키가 확장성에 유리 (서버 상태 저장 안 함)
  • 데이터 크기: 쿠키는 4KB 제한, 세션은 서버 한도에 따라 다름

세션 vs JWT

  • 상태: 세션은 서버에 상태 저장(stateful), JWT는 무상태(stateless)
  • 확장성: JWT가 수평 확장에 유리
  • 오버헤드: JWT는 매 요청마다 암호화 검증 필요
  • 무효화: 세션은 즉시 무효화 가능, JWT는 어려움

선택 기준

  • 단순한 사용자 추적: 쿠키
  • 보안이 중요한 사용자 인증: 세션
  • 마이크로서비스/분산 시스템: JWT
  • 모바일 API: JWT

5. 보안 고려사항

CSRF(Cross-Site Request Forgery) 방어

  • 쿠키: SameSite 속성, CSRF 토큰
  • JWT: Authorization 헤더 사용 (쿠키에 저장 시 추가 대책 필요)

XSS(Cross-Site Scripting) 방어

  • 쿠키: HttpOnly 속성
  • JWT: localStorage 사용 시 취약, HttpOnly 쿠키에 저장 고려

Leave a comment