Spring/Spring Boot

[Spring Boot] 3. Web: Servlet Web Application

noahkim_ 2023. 10. 12. 16:05

1. The “Spring Web MVC Framework”

  • Spring Web MVC는 풍부한 기능을 가진 "model-view-controller" 웹 프레임워크 입니다.

 

Controller

  • @Controller (or @RestController) 빈으로 들어오는 요청을 처리하도록 돕습니다
  • @Controller 빈의 메서드는 @RequestMapping을 사용하여 요청을 URI 기반으로 매핑할 수 있습니다.

 

ConversionService

  • 객체의 타입을 다른 타입으로 변환하는 인터페이스입니다.
항목 ApplicationConversionService @ConfigurationProperties 바인딩
Spring Web MVC
역할 중앙 변환 서비스 프로퍼티 → 객체 바인딩 시 사용
요청 파라미터
→ 컨트롤러 파라미터 변환
기본 지원 타입 Period, Duration, DataSize 등
(자동 설정에서 관련 컨버터 추가)
Period, Duration, DataSize 등 ❌ 포함되지 않음
기본 위임 대상 내부 컨버터/포맷터 DateTimeFormatter
특수 Formatter (DurationFormatter 등)
DateTimeFormatter
ConversionService
등록 방식 직접 등록 가능
- addConverter()
- addFormatter() 
ConfigurationPropertiesBindingPostProcessor가
DataBinder에 위임
WebMvcConfigurer
자동 등록 포맷터 ✅ 지원 ✅ 지원 ❌ 별도 등록 필요
커스터마이징 방법 직접 수정 @ConfigurationPropertiesBinding 커스터마이징 WebMvcConfigurer
관련 프로퍼티 없음 없음
spring.mvc.format.* → DateTimeFormatter에 위임됨

 

예제) ApplicationConversionService

더보기
@Configuration
public class ConversionServiceConfig {

    @Bean
    public ConversionService conversionService() {
        ApplicationConversionService service = new ApplicationConversionService();
        service.addConverter(new StringToCustomTypeconverter());
        return service;
    }

    public static class StringToCustomTypeconverter implements Converter<String, CustomType> {
        @Override
        public CustomType convert(String source) {
            return new CustomType(source.toUpperCase());
        }
    }

    public static class CustomType {
        private final String value;

        public CustomType(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "컨버팅 성공! 값은 " + value + " 입니다.";
        }
    }
}
@GetMapping("/conversion-service")
public String conversionService() {
    ConversionServiceConfig.CustomType hello = conversionService.convert("hello", ConversionServiceConfig.CustomType.class);

    return hello.toString();
}

 

예제) @ConfigurationProperties

더보기
app:
  timeout: 10s
  size: 512MB
  retention: P1Y2M3D
  type: test
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private Duration timeout;
    private DataSize size;
    private Period retention;
    private ConversionServiceConfig.CustomType type;
}
@Bean
@ConfigurationPropertiesBinding
public Converter<String, CustomType> stringToCustomTypeconverter() {
    return new Converter<>() {
        @Override
        public CustomType convert(String source) {
            return new CustomType(source.toUpperCase());
        }
    };
}
@GetMapping("/configuration-properties")
public String configurationProperties() {
    System.out.println(appProperties.getTimeout());
    System.out.println(appProperties.getRetention());
    System.out.println(appProperties.getSize().toKilobytes());
    System.out.println(appProperties.getType());

    return "ok";
}

 

예제) Spring Web MVC

더보기
public class DurationFormatter implements Formatter<Duration> {
    @Override
    public Duration parse(String text, Locale locale) throws ParseException {
        return Duration.parse(text);
    }

    @Override
    public String print(Duration object, Locale locale) {
        return object.toString();
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new DurationFormatter());
    }
}
@GetMapping("/spring-web-mvc")
public String springWebMvc(@RequestParam("timeout") Duration timeout) {
    System.out.println(timeout.toMinutes());

    return "ok";
}

 

예제) Spring Web MVC - LocalDateTime

더보기
spring:
  mvc:
    format:
      date-time: iso
@GetMapping("/spring-web-mvc")
public String springWebMvc(@RequestParam("time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time) {                               
    System.out.println(time);

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    System.out.println(formatter.format(time));

    return "ok";
}

 

HttpMessageConverter

항목 설명
역할
HTTP Request → Java 객체
Java 객체 → HTTP Response 로 직렬화/역직렬화 처리
사용되는 구현체 (기본)
- MappingJackson2HttpMessageConverter → JSON 처리
- MappingJackson2XmlHttpMessageConverter → XML 처리 (jackson-dataformat-xml 필요)
- Jaxb2RootElementHttpMessageConverter → XML 처리 (JAXB 기반)
XML 처리 방식
- jackson-dataformat-xml 의존성 없으면 → JAXB 사용
- 있으면 → Jackson XML 사용
커스터마이징 방법 ①
WebMvcConfigurer를 구현한 설정 클래스에서
configureMessageConverters(List<HttpMessageConverter<?>> converters) 또는
extendMessageConverters(List<HttpMessageConverter<?>> converters) 오버라이드
커스터마이징 방법 ②
HttpMessageConverters를 Bean으로 등록하면
Spring Boot가 자동 구성 시 우선 적용
자동 구성 관련
Spring Boot는 기본적으로 여러 MessageConverter들을 등록함
WebMvcConfigurer#configureMessageConverters는 직접 설정 (자동 구성이 무시될 수 있음)

 

예제) MappingJackson2XmlHttpMessageConverter

더보기
dependencies {
    implementation      'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
}
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

@JacksonXmlRootElement(localName = "person") // XML root element 이름 설정
public class Person {
    private String name;
    private int age;

    // 기본 생성자 필수
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter/Setter 필수
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}
@RestController
@RequestMapping("/api")
public class XmlController {

    @GetMapping(value = "/person", produces = "application/xml")
    public Person getPerson() {
        return new Person("이름", 30);
    }
}

 

 

예제) MappingJackson2HttpMessageConverter 커스터마이징

더보기
@Configuration
public class MessageConverterConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();

        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); // 빈 값 + NULL 인 필드를 JSON 출력에서 제외
        mapper.setDateFormat(new SimpleDateFormat("yyyy/MM/dd")); // 날짜(Date, Calendar) -> 문자열 포맷

        JavaTimeModule module = new JavaTimeModule();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); // 날짜(LocalDate, LocalDateTime) -> 문자열 포맷
        mapper.registerModule(module);

        return mapper;
    }

    @Bean
    public HttpMessageConverters customConverters() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper());

        return new HttpMessageConverters(converter);
    }
}

 

 java.time.* (LocalDate, LocalDateTime...) 을 포맷팅하고 싶으면 아래 모듈 추가 필요

implementation      'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'

 

MessageCodesResolver

  • binding 실패 시, 실패 메시지를 위한 에러코드를 만드는 역할을 담당합니다.
  • 에러코드 생성 전략을 spring.mvc.message-codes.resolver-format를 통해 선택할 수 있습니다
옵션 설명
에러 코드 형식 예시
PREFIX_ERROR_CODE 에러 코드를 errorCode.objectname.field 순으로 생성
errorCode.user.name
errorCode.user.email
POSTFIX_ERROR_CODE 에러 코드를 field.objectname.errorCode 순으로 생성
name.user.errorCode
email.user.errorCode

 

예제) PREFIX_ERROR_CODE

더보기
spring:
  mvc:
    message-codes-resolver-format: prefix_error_code
public class User {

    @NotBlank(message = "name.blank")
    private String name;

    @NotBlank(message = "email.blank")
    private String email;

    // getter, setter..
}
  • NotBlank.name.blank
  • NotBlank.email.blank

 

Static Content

항목 설명
기본 디렉토리 /static
대체 디렉토리
/public, /resources, /META-INF/resources
정적 리소스 요청 경로 패턴 설정
spring.mvc.static-path-pattern
→ 기본값: /**
→ 예시: /static/**
정적 리소스 위치 설정
spring.web.resources.static-locations
→ 기본값: classpath:/static/, classpath:/public/ 등
→ 커스텀 가능
리소스 서빙 핸들러
ResourceHttpRequestHandler
→ 내부적으로 정적 파일을 처리하는 역할 담당

 

Path Matching

항목 설명
요청 매핑
HTTP 요청의 URL을 컨트롤러에 매핑
접미사 패턴 매칭 ❌ 사용하지 않음
핸들러 없음
기본적으로 404 Not Found 반환
예외 설정
NoHandlerFoundException 발생시키려면:
spring.mvc.throw-exception-if-no-handler-found=true
정적 컨텐츠 제공
기본 매핑: /**
컨트롤러가 없을 경우 정적 리소스로 응답
정적 리소스 설정
정적 경로 설정: spring.mvc.static-path-pattern=/resources/**
정적 리소스 제공 비활성화: spring.web.resources.add-mappings=false
매칭 전략 설정
spring.mvc.pathmatch.matching-strategy 통해 설정 가능

 

Path Matching 전략 비교
구분 PathPatternParser
AntPathMatcher
도입 버전 Spring 5.3부터 기본 전략
성능 ✅ 더 효율적
⚠️ 대규모에서 성능 저하 가능
유연성 제한적 유연함
패턴 분석 엄격한 패턴 분석 덜 엄격함
허용되지 않는 패턴 / {varName:.*} 지원 안 함 지원
DispatcherServlet 경로 접두사와 호환성 ❌ 호환 안 됨 ✅ 호환
기본 설정 여부 ❌ 기본 아님 (설정 필요) ✅ 기본 설정

 

예제) PathPatternParser

더보기
spring:
  mvc:
    pathmatch:
      matching-strategy: path_pattern_parser  # PathPatternParser 사용
    throw-exception-if-no-handler-found: true  # 핸들러가 없을 경우 404 Not Found 응답
  web:
    resources:
      add-mappings: false  # 정적 리소스 제공 비활성화

 

@GetMapping("/hi/{name}")
public String hi(@PathVariable String name) {
    return "hi, " + name;
}

 

예제) AntPathMatcher

더보기
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
    throw-exception-if-no-handler-found: true    
  web:
    resources:
      add-mappings: false

 

@GetMapping("/hi/*.log")
public String log() {
    return "hi, log";
}

 

Content Negotiation

  • 클라이언트가 어떤 형식의 데이터를 받고 싶은지 서버에 요청하고, 서버는 그에 맞게 응답하는 기능
방식 설명 Spring 설정 필요 여부 권장 여부
Accept Header HTTP 헤더에 원하는 형식을 명시 ❌ (기본 지원) ✅ 권장
Query Parameter URL에 형식을 쿼리로 명시 ✅ favor-parameter: true 설정 필요 ⚠️ 테스트용/유연한 API 설계 시 사용

 

설정 키 설명
spring.mvc.contentnegotiation.favor-parameter
쿼리 파라미터로 포맷 지정 허용 여부
spring.mvc.contentnegotiation.parameter-name
포맷 지정 파라미터 이름 (기본: format)
spring.mvc.contentnegotiation.media-types
포맷별 MIME 타입 지정
spring.mvc.contentnegotiation.favor-path-extension
URL 확장자 사용 여부 (기본 false)

 

예제) Accept Header

더보기
spring:
  mvc:
    contentnegotiation:
      favor-parameter: false     # 쿼리 파라미터 방식 비활성화
      favor-accept-header: true  # Accept 헤더 방식 활성화
@RestController
@RequestMapping("/api")
public class ContentController {

    @GetMapping(value = "/user", produces = {
        MediaType.APPLICATION_JSON_VALUE,
        MediaType.APPLICATION_XML_VALUE
    })
    public User getUser() {
        return new User("Noah", 25);
    }

    public record User(String name, int age) {}
}
GET localhost:8080/acceptheader
Accept: application/json

 

예제) Query Parameter

더보기
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true      # 쿼리 파라미터 방식 활성화
      parameter-name: format     # 쿼리 파라미터 이름을 'format'으로 설정
      media-types:
        json: application/json   # 'json' 값은 application/json 형식으로 응답
        xml: application/xml     # 'xml' 값은 application/xml 형식으로 응답
@RestController
@RequestMapping("/api")
public class ContentController {

    @GetMapping("/user")
    public User getUser() {
        return new User("Noah", 25);
    }

    public record User(String name, int age) {}
}
GET /api/user?format=json
GET /api/user?format=xml

 

ConfigurableWebBindingInitializer

  • WebBindingInitializer 인터페이스를 구현하는 클래스입니다.
  • 컨트롤러 메소드에서 HTTP 요청의 매개변수를 바인딩 하기 위해 WebDataBinder 초기화를 수행합니다.
항목 설명 주요 역할/기능
WebDataBinder 웹 요청 데이터를 Java Bean에 바인딩하는 핵심 객체
🔹 요청 파라미터 바인딩
🔹 타입 변환
🔹 유효성 검사
WebBindingInitializer 각 요청에 대해 WebDataBinder를 초기화하는 인터페이스
🔹 PropertyEditor, Validator, Converter 등록
🔹 공통 바인딩 설정 재사용

 

CORS Support

항목 설명
정의
브라우저의 동일 출처 정책(Same-Origin Policy)을 완화하여, 다른 출처의 요청을 허용하기 위한 정책
사용 목적
보안을 유지하면서도 다른 도메인에서의 리소스 접근을 허용할 수 있도록 함
세부 제어 항목
- 허용 Origin (origin)
- 허용 HTTP 메소드 (method)
- 허용 헤더 (header)
설정 범위
- 핸들러(컨트롤러) 단위 설정
- 전역(Global) 설정 가능

 

예시) @CorsOrigin

더보기
// 컨트롤러에서 개별 설정
@CrossOrigin(origins = "https://example.com", methods = {RequestMethod.GET, RequestMethod.POST})
@GetMapping("/api/data")
public String getData() {
    return "data";
}

 

예시) WebMvcConfigurer

더보기
// 전역 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("https://example.com")
                .allowedMethods("GET", "POST")
                .allowedHeaders("*");
    }
}

 

2. Embedded Servlet Container Support

Filter

항목 설명
주의 사항
HTTP 요청의 InputStream은 한 번만 읽을 수 있음
→ 본문을 먼저 읽는 필터는 다른 필터나 컨트롤러에서 본문 접근 불가
우선순위 주의
Ordered.HIGHEST_PRECEDENCE(가장 높은 우선순위) 사용 시 위험
→ 지양해야 함

 

우선순위 설정
방식 설명 예시
@Order 어노테이션 클래스에 직접 우선순위를 지정할 수 있음 @Order(1)
Ordered 인터페이스 우선순위를 지정하는 인터페이스 구현 (getOrder() 메서드 활용)
implements Ordered
FilterRegistrationBean.setOrder() Bean 등록 시 우선순위를 설정
bean.setOrder(2);

 

Wrapper Filter

항목 설명
Wrapper Filter 목적
HttpServletRequest 등을 감싸서 수정하거나 기능을 추가함
- 보통 요청 본문을 여러번 읽기위해 사용됨
우선순위 설정 기준
Ordered.REQUEST_WRAPPER_FILTER_MAX_ORDER 이하로 설정해야 함
→ 다른 필터들이 감싼 요청을 인식하도록 보장

 

예시) RequestWrapperFilter

더보기
public class CustomRequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    public CustomRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) sb.append(line);
        }

        this.body = sb.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));

        return new ServletInputStream() {
            @Override
            public boolean isFinished() { return byteArrayInputStream.available() == 0; }

            @Override
            public boolean isReady() { return true; }

            @Override
            public void setReadListener(ReadListener listener) {}

            @Override
            public int read() throws IOException { return byteArrayInputStream.read(); }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBody() { return body; }
}
public class RequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        CustomRequestWrapper wrappedRequest = new CustomRequestWrapper(httpServletRequest);

        System.out.println(wrappedRequest.getBody());

        chain.doFilter(wrappedRequest, response);
    }
}
@Bean
public FilterRegistrationBean<RequestWrapperFilter> requestWrapperFilter() {
    FilterRegistrationBean<RequestWrapperFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new RequestWrapperFilter());
    registrationBean.addUrlPatterns("/*");
    registrationBean.setOrder(OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 1);

    return registrationBean;
}

 

 

Registering Servlets, Filters, and Listeners as Spring Beans

항목 설명
ServletRegistrationBean
Servlet을 Spring Bean으로 등록하여 커스텀 서블릿을 매핑할 수 있음
FilterRegistrationBean
Filter를 Spring Bean으로 등록하며, 필터 순서 및 URL 패턴 등 설정 가능
ServletListenerRegistrationBean
ServletContextListener, HttpSessionListener 등 리스너를 Bean으로 등록
특징
- Spring Boot의 Embedded Servlet Container에서 유용
- application.properties 참조 가능
- Bean 이름이 경로 접두사로 사용될 수 있음

 

DelegatingFilterProxyRegistrationBean

  • DelegatingFilterProxy를 Spring Bean으로 등록하고, 서블릿 필터 체인에 등록하기 위한 설정 도구.
항목 설명
DelegatingFilterProxy Spring의 Filter 빈을 서블릿 필터로 위임하기 위한 프록시 Filter.
서블릿 필터 체인에 등록됨
실제 Filter 로직은 Spring Bean에 있는 Filter가 처리함.
사용 이유 서블릿 컨테이너가 초기화 되기 전에, 빈이 완전히 초기화되지 않을 수 있음
장점 안정적인 의존성 주입
Spring Lifecycle 관리
등록 방법 DelegatingFilterProxyRegistrationBean
FilterRegistrationBean<DelegatingFilterProxy>

 

예제) DelegatingFilterProxyRegistrationBean

더보기
@Component("myCustomSpringFilter")
public class MyCustomSpringFilter implements Filter {

    private final LogService logService;

    public MyCustomSpringFilter(LogService logService) {
        this.logService = logService;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println(logService.hello());

        chain.doFilter(request, response);
    }
}
@Bean
public DelegatingFilterProxyRegistrationBean delegatingFilterProxy() {
    DelegatingFilterProxyRegistrationBean registrationBean = new DelegatingFilterProxyRegistrationBean("myCustomSpringFilter");
    registrationBean.setUrlPatterns(List.of("/*"));
    registrationBean.setOrder(1);

    return registrationBean;
}

예제) FilterRegistrationBean<DelegatingFilterProxy>

더보기
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> delegatingFilterProxy() {
    FilterRegistrationBean<DelegatingFilterProxy> registrationBean = new FilterRegistrationBean<>();
    DelegatingFilterProxy proxy = new DelegatingFilterProxy("myCustomSpringFilter");

    registrationBean.setFilter(proxy);
    registrationBean.addUrlPatterns("/*");
    registrationBean.setOrder(1);

    return registrationBean;
}

 

Servlet Context Initialization

  • ServletContextInitializer 인터페이스의 onStartup 메서드를 구현하여 ServletContex 초기화를 담당합니다.
  • WebApplicationInitializer를 가지고 구현합니다.
    • web.xml 을 대체할 수 있습니다.

 

Scanning for Servlets, Filters, and listeners

  • @WebServlet, @WebFilter, @WebListener로 빈을 등록할 수 있습니다.
  • @ServletComponentScan 어노테이션으로 위의 빈을 스캔할 수 있습니다.

 

The ServletWebServerApplicationContext

항목 설명
정의
Spring Boot에서 서블릿 환경에서 사용하는 ApplicationContext
ServletWebServerFactory
내장 서블릿 컨테이너를 설정
ServletWebServerApplicationContext에 등록
WebServer 제어
시작, 중지, 포트 변경 등 웹 서버를 직접 제어할 수 있음
ServletContext 빈 등록
ServletContext 객체를 Spring Bean으로 등록
Bean과 ServletContext 초기화 시점 차이
Bean이 먼저 초기화될 수 있어 ServletContext가 아직 준비되지 않은 경우가 발생 가능
문제점
Bean이 ServletContext에 접근해야 할 경우, 아직 초기화되지 않아 접근 실패 가능성 존재
해결 방법
ApplicationListener<ServletWebServerInitializedEvent>를 사용해, 서버가 완전히 초기화된 후 ServletContext에 접근하도록 처리
public class MyDemoBean implements ApplicationListener<ApplicationStartedEvent> {

    private ServletContext servletContext;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        this.servletContext = ((WebApplicationContext) applicationContext).getServletContext();
    }
}

 

SameSite Cookie

  • 쿠키의 크로스 사이트 요청 동작을 제어하는 속성입니다.

 

쿠키 속성
속성 설명 예시
name 쿠키의 이름 JSESSIONID
value 쿠키에 저장되는 값 abc123xyz
domain 도메인 기반 쿠키 범위 설정
(하위 도메인 포함 가능)
.myapp.com → sub.myapp.com 가능
path URL 경로 기반 쿠키 범위 설정
(하위 경로 포함 가능)
/admin -> /admin/settings 가능
maxAge 쿠키의 유효 시간 (초 단위)
- 0: 즉시 삭제
- -1: 세션 쿠키 (브라우저 닫으면 자동 삭제됨)
3600 (1시간)
expires 쿠키 만료 날짜 (RFC1123 형식)
Set-Cookie 응답 헤더에 포함됨
Wed, 21 Oct 2025 07:28:00 GMT
secure true일 경우 HTTPS 연결에서만 쿠키 전송 Secure
httpOnly JavaScript로 쿠키 접근 불가 (XSS 방지) HttpOnly
sameSite 크로스 사이트 요청에 대한 쿠키 전송 제어
Strict, Lax, None
SameSite=Lax

 

SameSite 쿠키 속성
속성 값 동작 크로스 사이트 요청 시 전송 여부 비고
Strict 완전 차단 ❌ 전송 안 됨 가장 보안이 강함
로그인 등 민감 정보 보호에 적합
단, 외부 서비스와 연동 어려움
Lax 부분 허용 ✅ 탐색 요청에는 전송
❌ AJAX / 폼 전송 등에는 전송 안 됨
기본값(Lax)
링크 클릭, 주소창 직접 입력 시 쿠키 전송
None 전체 허용 ✅ 모든 요청에 전송 반드시 Secure 속성과 함께 사용해야 함
HTTPS 필수

 

요청 방식 Strict Lax None
링크 클릭 (a 태그)
GET 요청 (AJAX)
POST 요청 (AJAX / 폼)
이미지, iframe 등 외부 리소스 요청

 

설정) server.servlet.session.cookie.same-site 

더보기
server:
  servlet:
    session:
      cookie:
        same-site: Lax
  • id가 JSESSIONID인 세션 쿠키일 경우, 자동으로 SameSite를 설정해줌

 

@GetMapping("/set-cookie")
    public ResponseEntity<String> setCookie(HttpServletResponse response) {
        // 세션 쿠키는 Spring에서 자동으로 관리되므로 아래는 실제로 추가할 필요가 없지만, 설명을 위해 작성
        Cookie sessionCookie = new Cookie("JSESSIONID", "some-session-id");
        sessionCookie.setPath("/");  // 모든 경로에서 쿠키 접근 가능
        sessionCookie.setHttpOnly(true);  // JavaScript에서 접근 불가능
        sessionCookie.setSecure(true);   // HTTPS를 사용하지 않으면 전송되지 않음
        sessionCookie.setMaxAge(-1);  // Max-Age를 -1로 설정하면 세션 쿠키로 설정됨

        response.addCookie(sessionCookie);  // 응답에 세션 쿠키 추가

        return ResponseEntity.ok("쿠키 설정 완료");
    }

 

설정) CookieSameSiteSupplier

더보기
@Configuration
public class CookieConfig {

    @Bean
    public CookieSameSiteSupplier cookieSameSiteSupplier() {
        return (Cookie cookie) -> {
            if (cookie.getName().matches("myapp.*")) {
                return "Lax";  // SameSite의 Lax 값을 반환합니다.
            }
            
            return null;  // 그 외의 쿠키는 변경하지 않습니다.
        };
    }
}

 

예시) Strict

더보기
@GetMapping("/set-cookie")
public ResponseEntity<String> setCookie(HttpServletResponse response) {
    // 세션 쿠키는 Spring에서 자동으로 관리되므로 아래는 실제로 추가할 필요가 없지만, 설명을 위해 작성
    Cookie sessionCookie = new Cookie("JSESSIONID", "some-session-id");
    sessionCookie.setPath("/");  // 모든 경로에서 쿠키 접근 가능
    sessionCookie.setHttpOnly(true);  // JavaScript에서 접근 불가능
    sessionCookie.setSecure(true);   // HTTPS를 사용하지 않으면 전송되지 않음
    sessionCookie.setMaxAge(-1);  // Max-Age를 -1로 설정하면 세션 쿠키로 설정됨

    response.addCookie(sessionCookie);  // 응답에 세션 쿠키 추가

    return ResponseEntity.ok("쿠키 설정 완료");
}

@GetMapping("/get-cookie")
public ResponseEntity<String> getCookie(@CookieValue(value = "JSESSIONID", required = false) String cookieValue) {
    return ResponseEntity.ok("[GET] Received cookie: " + cookieValue);
}

@PostMapping(value = "/get-cookie", consumes = "application/json")
public ResponseEntity<String> getCookie2(@CookieValue(value = "JSESSIONID", required = false) String cookieValue) {
    return ResponseEntity.ok("[POST-json] Received cookie: " + cookieValue);
}

@PostMapping(value = "/get-cookie", consumes = "multipart/form-data")
public ResponseEntity<String> getCookie3(@CookieValue(value = "JSESSIONID", required = false) String cookieValue) {
    return ResponseEntity.ok("[POST-formdata] Received cookie: " + cookieValue);
}

@GetMapping("/cookie-test/image.png")
public ResponseEntity<byte[]> imageTest(@CookieValue(value = "JSESSIONID", required = false) String cookieValue) throws IOException {
    System.out.println("[GET-img] Received cookie: " + cookieValue);

    byte[] image = StreamUtils.copyToByteArray(new ClassPathResource("static/image.png").getInputStream()); // ...load from classpath

    return ResponseEntity.ok().contentType(MediaType.IMAGE_PNG).body(image);
}

@GetMapping("/cookie-test/frame.html")
public ResponseEntity<String> frameTest(@CookieValue(value = "JSESSIONID", required = false) String cookieValue) throws IOException {
    System.out.println("[GET-iframe] Received cookie: " + cookieValue);

    String html = StreamUtils.copyToString(new ClassPathResource("static/frame.html").getInputStream(), UTF_8);

    return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(html);
}
❌ [GET] Received cookie: null 		# <a>
❌ [GET] Received cookie: null 		# ajax
❌ [POST-json] Received cookie: null
❌ [POST-formdata] Received cookie: null
❌ [GET-img] Received cookie: null
❌ [GET-iframe] Received cookie: null

 

예시) Lax

더보기
✅ [GET] Received cookie: some-session-id    # <a> 
❌ [GET] Received cookie: null               # ajax				
❌ [POST-json] Received cookie: null
❌ [POST-formdata] Received cookie: null
❌ [GET-img] Received cookie: null
❌ [GET-iframe] Received cookie: null

 

예시) None

더보기
✅ [GET] Received cookie: some-session-id    	# <a> 
✅ [GET] Received cookie: some-session-id        # ajax				
✅ [POST-json] Received cookie: some-session-id
✅ [POST-formdata] Received cookie: some-session-id
✅ [GET-img] Received cookie: some-session-id
✅ [GET-iframe] Received cookie: some-session-id

 

 

참고