본문 바로가기
Back-End/Spring

[Spring Boot] Spring Boot - Web(Servelet Web Applications)

by hongdor 2023. 3. 30.
728x90

Spring Boot의 공식 문서 : Spring Boot Reference Documentation

그 중 Documentation Overview 문서 : Documentation Overview (spring.io)

 

5번 항목에서 Servlet Web Applications 개발자는
Spring MVC, Jersey, Embedded Servlet Containers 공식문서를 읽어보라고 나와 있다

 

그 중 Spring MVC 항목을 살펴봤다

 

Spring Boot의 공식 문서 :Spring Boot Reference Documentation

그 중 Web 문서 : Web (spring.io)

Servlet Web Applications : Web (spring.io) - Servelet Web Applications

에서 1번 The Spring Web MVC Framework

 

 

The Spring Web MVC Framework

  • http 요청을 다루기위한 @Controller 와 @RestController 를 제공한다.
  • @RequestMapping 을 통해 맵핑할 수 있다.
  • 아래는 전형적인 @RestController 코드 이다.
@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId).get();
    }

    @GetMapping("/{userId}/customers")
    public List<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();
    }

    @DeleteMapping("/{userId}")
    public void deleteUser(@PathVariable Long userId) {
        this.userRepository.deleteById(userId);
    }

}

 

 

  • WebMvc는 아래에 보는 것처럼 요청의 처리와 라우팅 설정을 분리한다
    (내 생각 - 요청의 처리에 집중할 수 있게끔 했다는 것을 보여주려는 설명 같다)
@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

}
@Component
public class MyUserHandler {

    public ServerResponse getUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse getUserCustomers(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse deleteUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

}

 


1. Spring MVC 자동 설정

  • 자동 설정의 기능
    • ContentNegotiatingViewResolver와 BeanNameViewResolver 빈들을 포함
    • static 자원 제공 지원 (WebJars 등)
    • Converter, GenericConverter, Formatter 빈들 등을 자동 등록
    • HttpMessageConverters 지원
    • MessageCodesResolver 자동 등록
    • static index.html 지원
    • ConfigurableWebBindingInitializer 빈 자동 사용
  • 일부 instance 커스텀도 가능하다 (문서 참고)
  • 자동 설정을 위해 @Configuration과 함께 @EnableWebMvc를 추가하면 된다.

 

2. HttpMessageConverters

  • Spring MVC는 요청과 응답을 변환하기 위해 HttpMessageConverters를 사용한다.
  • object는 자동적으로 Json, UTF-8로 변환된다.
  • 아래와 같이 커스텀도 가능하다
@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
        HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
        return new HttpMessageConverters(additional, another);
    }

}

 

 

3. MessageCodesResolver

  • Spring MVC는 error codes 전략 패턴을 가지고 있다.
  • spring.mvc.message-codes-resolver-format 설정을PREFIX_ERROR_CODE 또는  POSTFIX_ERROR_CODE 등으로 바꿀 수 있다.

 

4. Static Content

  • classpath의 /static 위치 또는 ServletContext의 root에서 static content를 자동으로 제공한다.
    이 때 ResourceHttpRequestHandler를 사용한다.
    WebMvcConfigurer를 추가 또는 addResourceHandler 메소드 오버라이딩으로 수정가능하다
  • Spring에서 설정이 없으면, 기본 servlet은 ServletContext의 root에서 content를 제공하는 역할만 한다.
    왜냐하면 Spring은 항상 DispatcherServlet으로 요청을 처리할 수 있기 때문이다.
  • 기본적으로 resource들은 /**에 맵핑되어 있다. spring.mvc.static-path-pattern 속성으로 아래와 같이 수정 가능하다.
spring.mvc.static-path-pattern=/resources/**

 

 

  • static resource들의 위치도  spring.web.resources.static-locations 속성으로 변경 가능하다.
    기본은 경로는 / 이다.
  • Webjars format 포맷인 /webjars/** 경로의 resource들은 jar files 로부터 제공된다
    spring.mvc.webjars-path-pattern 속성으로 경로는 변경 가능하다.
  • Webjar 버전에 구애받지 않는 URL을 위해서는 webjars-locator-core dependency를 추가해라
    jquery를 예로 들면
    "/webjars/jquery/jquery.min.js” 가 x.y.z 버전의 Webjar를 따라 "/webjars/jquery/x.y.z/jquery.min.js”가 된다.
  • cache busting을 사용하기 위해서 아래와 같이 설정해라
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**

 

 

  • `<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>`
    와 같이 효과적으로 content hash를 추가할 수 있다.
  • javascript 모듈 로더를 사용해 리소스를 동적으로 생성하는 경우, 파일 이름을 변경할 수 없다.
    그럴 땐 아래와 같이 설정하면, 파일이름 변경 대신, URL에 버전을 추가한다
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.paths=/js/lib/
spring.web.resources.chain.strategy.fixed.version=v12

 

 

  • /js/lib 아래 파일은 /v12/js/lib/mymodule.js 가 된다. 반면에 다른 자원들을 여전히 <link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/> 같은 형식을 사용한다.

 

5. Welcom Page

  • static content location 경로에서 index.html → index 이름 순서로 파일을 찾는다.

 

6. Custom Favicon

  • static content location 경로에서 favicon.io를 찾는다.

 

7. Path Matching and Content Negotiation 

  • Spring MVC는 @GetMapping 등을 통해 http 요청을 handler(controller)에 맵핑한다
  • Spring Boot은 suffix patter matching을 하지 않는다. 즉, "GET /projects/spring-boot.json”은 @GetMapping("/projects/spring-boot") 로 맵핑되지 않는다.
    (이것은 과거에 Accept header가 틀린 요청을 처리하기 위한 좋은 보완 방법이었다)
    대신 이제는 "GET /projects/spring-boot?format=json”와 같은 방법으로 대체한다.
  • 다른 parameter 이름 format에서 myparam으로 바꾸고 싶은경우 아래와 같이 설정
spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=myparam

 

 

  • 대부분 media types도 지원한다. 새롭게 정의 하고 싶다면 아래와 같이 설정
spring.mvc.contentnegotiation.media-types.markdown=text/markdown

 

 

  • spring framework 5.3부터 path 매칭 전략으로 PathPatterParser도 지원한다. 
    (이전까지는 antPathMatcher)
spring.mvc.pathmatch.matching-strategy=path-pattern-parser

 

 

  • 만약 path를 matching할 수 없다면 404 에러를 응답한다.
  • 만약 NoHandlerFoundException를 대신 던지고 싶다면 
    spring.mvc.throw-exception-if-no-handler-found를 true로 설정한다.
    하지만, 기본적으로 모든 static content이 /**로 맵핑되어있기 때문에
    static 경로를 바꾸거나, static content 제공을 정지해야 exception을 던질 수 있다.

 

8. ConfigurableWebBindingInitializer

  • parameter에 따라 Level이라는 Object로 새롭게 바인딩 하고 싶을때
@GetMapping("/binding")
public void binding(@RequestParam Level level){
    log.debug("level : {}", level);
}

    ConfigurableWebBindingInitializer 를 사용한다.

 

9. Template Engines

  • Spring MVC는 다양한 템플릿을 지원한다.
    Thymeleaf, FreeMarker, JSP, Groovy, Mustache 등
  • template를 찾는 경로는 “src/main/resources/templates” 이다

 

 

10. Error Handling

  • 기본적으로 모든 에러를 다루는 /error 맵핑을 제공한다.
    이것은 글로벌 에러 페이지로 등록된다.
    machine에 대한 응답은 에러,HTTP 상태, 에러 메세지를 JSON으로 응답하고
    브라우저에 대한 응답은 위 데이터를 whitelabel 에러 페이지에 담아 응답한다
  • 많은 server.error 특성을 설정해서 기본 에러 처리를 커스텀할 수 있다.
  • 완전히 대체하기위해 ErrorController를 구현하거나 ErrorAttributes로 내용만 대체할 수 있다.
  • Spring framework 6.0부터 RFC 7807이 지원된다. spring.mvc.problemdetails.enabled 를 true로 설정하면 된다. 형태는 아래와 같다.
{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

 

 

  • @ControllerAdvice 로 특정한 컨트롤러 혹은 exception 타입에 따라 JSON 내용을 커스텀 할 수 있다.
  • 아래는 SomeController에서 MyException이 발생 시, MyErrorBody를 응답하는 예시이다.
@ControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {

    @ResponseBody
    @ExceptionHandler(MyException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        HttpStatus status = HttpStatus.resolve(code);
        return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
    }

}

 

 

  • controller level에서 확실하게 기록되도록 하기위해 속성에 exception을 넣어줄 수도 있다.
@Controller
public class MyController {

    @ExceptionHandler(CustomException.class)
    String handleCustomException(HttpServletRequest request, CustomException ex) {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);
        return "errorView";
    }

}

 

 

  • Custom Error Pages
  • status code 마다 error page를 다르게 띄우고 싶다면, /error 에 파일을 추가하면 된다.
    static HTML 혹은, template page를 추가하면 된다.
    파일이름은 status code나 정해진 형식을 맞춰야 한다. (404.html or 5xx.html)
src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftlh
             +- <other templates>

 

 

  • 더 복잡한 맵핑을 원한다면, ErrorViewResolver interface를 구현하면 된다.
    (@ExceptionHandler 또는 @ControllerAdvice도 물론 사용할 수 있다.)
public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            new ModelAndView("myview");
        }
        return null;
    }

}

 

 

  • Mapping Error Pages Outside of Spring MVC
    Srping MVC를 사용하고 있지 않는 application을 위해 ErrorPageRegister interface를 사용해 dispatcherServlet 없이 기본으로 포함된 servlet container와 직접 연동해 등록할 수 있다.
@Configuration(proxyBeanMethods = false)
public class MyErrorPagesConfiguration {

    @Bean
    public ErrorPageRegistrar errorPageRegistrar() {
        return this::registerErrorPages;
    }

    private void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }

}

 

 

  • Jersey, Wicket 처럼 Filter에 의해 path가 핸들링 되는 경우 명확히 ERROR dispatcher로써 등록 되어야 한다.
@Configuration(proxyBeanMethods = false)
public class MyFilterConfiguration {

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(new MyFilter());
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
        return registration;
    }

}

 

 

  • WAR 배포에서 에러 처리(잘모르겠음. 본문 번역 첨부)
    서블릿 컨테이너에 배포할 때 Spring Boot는 오류 페이지 필터를 사용하여 오류 상태의 요청을 적절한 오류 페이지로 전달합니다. 이는 서블릿 사양이 오류 페이지 등록을 위한 API를 제공하지 않기 때문에 필요합니다. WAR 파일을 배포하는 컨테이너와 애플리케이션에서 사용하는 기술에 따라 몇 가지 추가 구성이 필요할 수 있습니다.
    오류 페이지 필터는 응답이 아직 커밋되지 않은 경우에만 요청을 올바른 오류 페이지로 전달할 수 있습니다. 기본적으로 WebSphere Application Server 8.0 이상은 서블릿의 서비스 메서드가 성공적으로 완료되면 응답을 커밋합니다. 이 동작을 비활성화하려면 com.ibm.ws.webcontainer.invokeFlushAfterService를 false로 설정해야 합니다.

 

11. CORS 지원

  • Web on Servlet Stack (spring.io) 에 나와있는대로 @CrossOrigin을 사용하거나 아래처럼 addCorsMappings(CorsRegistry) 를 Bean으로 등록하면 된다.
@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {

            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }

        };
    }

}
728x90

댓글