본문 바로가기

Back-End/Spring

Spring MVC에 관하여

 

Spring MVC 요청 흐름 개요

  1. 사용자가 요청( URL + parameters )을 서버에게 날린다.
  2. DispatcherServlet이 처음에 이 요청을 받는다.
  3. DispatcherServlet은 미리 설정되어 있는 Handlermapping 정보(web.xml or @Controller)를 통해 request를 Handler에게 위임(delegate)한다.
  4. Handler(Controller)에서 비즈니스 로직을 처리 한다.
  5. 그 결과에 View와 관련된 정보가 있다면 DispatcherServlet은 ViewResolver를 통해 view에 대한 정보를 받아온다.
  6. DispatcherServlet은 받아온 View와 Model을 통해 render을 하고 이를 브라우저에 돌려준다.

Filter는 무엇을 하는가?

위의 그림을 보면, Dispatcher Servlet 앞단에 Filter가 존재한다.

 

spring-web 모듈은 4가지 Filter를 제공한다. 각각에 대한 자세한 기능은 글 맨 아래에 첨부한 Spring 공식 문서를 참고하면 된다.

 

  1. Form Data
  2. Forwarded Headers
  3. Shallow ETag
  4. CORS 

Servlet, Servlet Container가 도대체 뭔데?

서블릿이란 웹프로그래밍에서 클라이언트의 요청을 처리하고 그 결과를 다시 클라이언트에게 전송하는 javax.servlet.Servlet 클래스의 구현 규칙을 지킨 자바 프로그래밍 기술이다.

  • 클라이언트의 요청에 대해 동적으로 작동하는 웹 어플리케이션 컴포넌트
  • Java Thread를 이용하여 동작
  • MVC 패턴에서 Controller로 동작
  • javax.servlet.http.HttpServlet 클래스를 상속받음

서블릿의 간단한 동작 방식

서블릿 컨테어는 말그대로 서블릿을 관리해주는 컨테이너이다.

  • 웹서버와의 통신을 지원 : 서블릿과 웹서버가 소켓 생성같은 거 신경 안쓰고 손쉽게 통신할 수 있게 해줌. 
  • 서블릿 생명주기 관리
  • 멀티쓰레디 지원 및 관리 : 요청이 올때마다 자바 스레드를 하나 생성하는데, 끝나고나면 또 자동으로 죽여준다.

아래는 Spring project를 키면 기본적으로 사용하는 서블릿컨테이너(톰켓)이 시작된 것을 볼 수 있다. Spring 5.0때부터는 Netty를 사용하는 component도 있다.

Spring MVC - 코드단으로 살펴보기

DispatcherServlet

 

함수에 대한 설명을 보면

Handler에게 실제로 보내주는 과정을 처리한다. Handler는 순서대로 Servlet의 HandlerMapping들에 의해 적용되어 있는 것을 기반으로 가져온다. 

위에서는 다음과 같은 작업을 진행한다.

  • Determine handler for the current request
  • Determine handler adapter for the current request
  • Process last-modified haeder, if supported by the handler

그 작업 후에 실제로 handler를 실행하는 부분이다.

Dispatch Servlet이 실제로 Controller 코드를 부르는 곳

mappedHandler의 handler를 보면 내가 원하던 Controller가 나온다.

Annotation Controllers

Spring MVC는 @Controller와 @RestController annotation 기반으로 프로그래밍할 수 있도록 제공을 해준다. 

 

다양한 Request Mapping 방법이나 URL pattern 등은 Spring MVC 공식문서 1.3.2를 참고하면 된다. 

DataBinder

@Controller나 @ControllerAdvice 클래스는 WebDataBinder를 구현한 @InitBinder method를 사용할 수 있다.

이 어노테이션을 사용하면,

  • Request 파라미터( form, query data )를 model object에 bind할 수 있다.
  • HTML form을 렌더링 할 때, model object 값을 string으로 포맷화 할 수 있다.

Exceptions

@ExceptionHandler

Spring에서 발생한 Exception을 기반으로 오류 처리할 수 있도록 @ExceptionHandler를 제공해준다. 

@RestController
@RequestMapping("/boards")
public class BoardController {

  @GetMapping("/{id}")
  public Board get(@PathVariable Long id) {
    if (id < 1L) {
      throw new BoardNotFoundException("invalid id: " + id);
    }
    return new Board("title", "content");
  }

  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ExceptionHandler(BoardNotFoundException.class)
  public Map<String, String> handle(BoardNotFoundException e) {
      log.error(e.getMessage(), e);
      Map<String, String> errorAttributes = new HashMap<>();
      errorAttributes.put("code", "BOARD_NOT_FOUND");
      errorAttributes.put("message", e.getMessage());
      return errorAttribute;
  }
}

응답 예제

HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 17 Feb 2019 04:31:33 GMT

{"code":"BOARD_NOT_FOUND","message":"invalid id: 0"}
  • 그렇다면 해당 Exception이 발생했을 때 어떻게 ExceptionHandler를 찾는것일까?

 

 

Controller Advice

보통 @Controller에 적용된 @ExceptionHandler, @InitBinder, @ModelAttribute method는 @Controller class에만 해당이 된다. 만약에 global하게 적용을 하고 싶다면 @ControllerAdvice, @RestControllerAdvice에 적용을 하면 된다.

 

@ControllerAdvice도 @Component를 가지고 있기 때문에, 똑같이 component scaning을 통해 bean으로 등록이 된다.

 

아래와 같이 범위를 정할 수도 있다.

ErrorController

Filter는 DispatcherServlet 외부에서 발생하기 때문에 ErrorController에서 처리해야 한다.

 

{
  "timestamp": "2019-02-15T21:48:44.447+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/123"
}

404 에러 json은 도대체 누가 기본으로 만들어 주는걸까?

 

AbstractErrorController와 ErrorAttributes

getErrorAttributes() 함수에서 ErrorAttributes 인터페이스의 getErrorAttributes 호출을 통해 에러 처리에 대해 책임을 위임하고 있다. ( delegate pattern )

public abstract class AbstractErrorController implements ErrorController {
  private final ErrorAttributes errorAttributes;
    
  protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
    boolean includeStackTrace) {

    WebRequest webRequest = new ServletWebRequest(request);
    return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
  }
}

스프링에서 기본은 ErrorAttributes를 구현한 DefaultErrorAttributes이다. 해당 함수에서 request에 담긴 정보를 토대로 Error 정보를 만든다.

public interface ErrorAttributes {

	/**
	 * Returns a {@link Map} of the error attributes. The map can be used as the model of
	 * an error page {@link ModelAndView}, or returned as a {@link ResponseBody}.
	 * @param webRequest the source request
	 * @param includeStackTrace if stack trace elements should be included
	 * @return a map of error attributes
	 */
	Map<String, Object> getErrorAttributes(WebRequest webRequest,
			boolean includeStackTrace);

	/**
	 * Return the underlying cause of the error or {@code null} if the error cannot be
	 * extracted.
	 * @param webRequest the source request
	 * @return the {@link Exception} that caused the error or {@code null}
	 */
	Throwable getError(WebRequest webRequest);

}
public DefaultErrorAttributes {
  // 생성자 및 메서드
  @Override
  public Map<String, Object> getErrorAttributes(WebRequest request, boolean includeStackTrace) {
    Map<String, Object> errorAttributes = new LinkedHashMap<>();
    errorAttributes.put("timestamp", new Date()); // timestamp 생성
    addStatus(errorAttributes, request); // status 생성
    addErrorDetails(errorAttributes, request, includeStackTrace); // 오류 상세 내용
    addPath(errorAttributes, request); // path 생성
    return errorAttributes;
  }
}

ErrorAttributes 확장 포인트 - 커스터마이징하기

오류가 발생했을 때 응답을 내려줄 모델을 커스터마이징 하고 싶은 경우가 있다. 스프링 덕분에 ErrorAttributes 인터페이스만 구현해주면 확장할 수 있다.

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> result = super.getErrorAttributes(webRequest, includeStackTrace);
        result.put("greeting", "Hello");
        return result;
    }
}

응답 예제

{
  "timestamp": "2019-02-15T22:24:41.275+0000",
  "status": 404,
  "error": "Not Found",
  "message": "No message available",
  "path": "/123",
  "greeting": "Hello"
}

 

Spring Boot yml 설정을 통한 에러 설정

# spring boot의 기본 properties
server.error:
  include-exception: false  # 오류 응답에 exception의 내용을 포함할지 여부
  include-stacktrace: never # 오류 응답에 stacktrace 내용을 포함할지 여부
  path: '/error' # 오류 응답을 처리할 Handler의 경로
  whitelabel.enabled: true # 서버 오류 발생시 브라우저에 보여줄 기본 페이지 생성 여부

ErrorController는 어떻게 호출되는데??

ErrorController가 어떻게 처리되는지는 이해 되었는데 그전에 어떻게 호출 될까?

 

  1. 서블릿 컨테이너에서 등록된 서블릿에서 요청을 처리함
  2. 오류 발생!
  3. 해당 서블릿에서 처리하지 못함
  4. 서블릿 컨테이너까지 오류가 전파됐을 때, 서블릿 컨테이너가 오류를 처리하기 위해 특정 경로 ( server.error.path)로 해당 요청처리를 위임할 때 사용

 

정리

매일 @Controller를 작성하면, 막상 내부적으로 어떻게 돌아가는지에 대해서는 잘 몰랐다.

 

그래서 다음 포스팅을 작성하게 되었고,

 

다음에는 Spring WebFlux에 대해 포스팅도 해보겠다!

 

참고

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html

'Back-End > Spring' 카테고리의 다른 글

Jackson 관련  (0) 2019.10.11
AOP란  (0) 2019.06.20
Spring layered architecture와 객체지향적으로 개발하기  (0) 2019.05.31
Spring Security 아키텍쳐  (0) 2019.05.21
Spring boot 2.0에 관하여  (0) 2019.05.20