부자 되기 위한 블로그, 머니킹

최근에 김영한 선생님의 인프런 강의를 정말 많이 돌려보는것 같다. 너무 이해가 잘 되게 설명해주셔서 김영한 선생님이 출시한 모든 강의를 구매하였다 (내 지갑은 텅텅 비어버렸지만...) 오늘은 spring mvc 의 기본 개념 구조를 정리해보았다.

 

웹 어플리케이션 이해

 

스프링에서도 Servlet을 만들 수 있다. @WebServlet 어노테이션을 통해 만들어주면된다.

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
          protected void service(HttpServletRequest request, HttpServletResponse
      response){

    }
}

 

Web Servlet은 해당 구조로 웹 요청과 응답을 받는다. HttpServletRequest에서 parameter및 headear등을 볼 수 있는 요청 관련 객체이다. HttpServletResponse에서 Header를 Set해서 보낼수 있고 message body를 포함해서 보낸다.

 

 

Http 요청 데이터 종류

  • GET : 쿼리 파라미터
    • 메시지 바디 없이, url의 쿼리 파라미터에 데이터를 포함해서 전달
    • 메시지 바디가 없기 때문에 content-type이 따로 없다.
  • POST : HTML FORM
    • content-type : application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달
  • HTTP Mesage body에 데이터를 직접 담아서 요청
    • HTTP API에 사용, JSON, XML, TEXT

 

 

스프링 MVC 구조 이해

패키지 설계시 controller, service, repository 부를 나눈다. view 에 데이터를 본래때는 model 객체를 사용한다. 하지만 실제 개발 환경에서 해당 구조에 한계가 있어 Spring은 추가적인 구조로 보완을 한다.

 

Front Controller

해당 구조처럼 Controller 로직이 request시 바로 controller에 보내면 공용 처리가 어렵다는 단점이 있다. 이를 보완하기 위해 나온 구조가 Front Controller이다. 스프링 웹 MVC에서는 DIspacherServlet이 이 역할을 한다.

 

 

View Resolver

URL 매핑 이후에 view render를 위해 해당 view 파일을 찾는 과정에서 쓸데없이 반복되는 부분이나 확장자가 있다. 해당 반복을 줄이기위해 나온 개념이 viewResolver이다.

 

 

Handler Adapter

  • 핸들러 어댑터 : 어댑터 역할을 해주어 다양한 종류의 컨트롤러를 호출할 수 있다.
    • 예를 들어 컨트롤러 반환 타입이 어떤것은 viewResolver를 사용하여 String이고 어떤것은 ModelAndView등을 사용한다. 이럴 때 어댑터의 역할로서 모두 사용 가능하게 만들어준다.
  • 핸들러 : 핸들러 어댑터를 통해 나온 결과. 컨트롤러라고 보면 된다.

 

 

DispacherServlet

  • DispacherServlet 도 부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, 서블릿으로 동작한다.
  • DispatcherServlet FrameworkServlet HttpServletBean HttpServlet 스프링 부트는DispacherServlet 을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/" )에
    대해서 매핑한다.
  • 참고: 더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작한다.

 

 

요청 흐름

  1. 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출됨
  2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service를 오버라이드함
  3. FrameworkServlet.service를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch가 호출됨
  4. doDispatch 로직 실행
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest); 
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}

//2.핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환 
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView
mv, Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request,
HttpServletResponse response) throws Exception {
  View view;
String viewName = mv.getViewName(); //6. 뷰 리졸버를 통해서 뷰 찾기,7.View 반환
  view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
  view.render(mv.getModelInternal(), request, response);
}

 

 

동작 순서

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
    1. @RequestMapping 핸들러 어뎁터 통해 핸들러(controller) 실행
  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서
  6. 반환한다.
  7. viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
  8. JSP의 경우: InternalResourceViewResolver 가 자동 등록되고, 사용된다.
  9. View반환:뷰리졸버는뷰의논리이름을물리이름으로바꾸고,렌더링역할을담당하는뷰객체를
  10. 반환한다.
    JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다.
  11. 뷰렌더링:뷰를 통해서 뷰를 렌더링한다.

 

 

스프링 MVC 기본 기능

 

최종적인 스프링 MVC Controller 구조

@GetMapping("/new-form")
public String newForm() {
    return "new-form";
}

@PostMapping("/save")
    public String save(
            @RequestParam("username") String username,
            @RequestParam("age") int age,
            Model model) {
        Member member = new Member(username, age);
        memberRepository.save(member);
        model.addAttribute("member", member);
        return "save-result";
}

@GetMapping
      public String members(Model model) {
          List<Member> members = memberRepository.findAll();
          model.addAttribute("members", members);
          return "members";
}

@GetMapping("/mapping/users/{userId}/orders/{orderId}")
  public String mappingPath(@PathVariable String userId, @PathVariable Long
  orderId) {
      log.info("mappingPath userId={}, orderId={}", userId, orderId);
      return "ok";
  }
  • Model 파라미터 : MVC의 모델 객체
  • RequestParam : 해당 어노테이션을 통해 쉽게 request로 오는 요청 파라미터 받을 수 있음, 나중에 이를 객체 @ModelAttribute를 통해 객체 형태로도 받을 수 있음
  • ViewName 직접 반환
  • @RestController 어노테이션을 사용하여 쉽게 Rest API 형태 (HTTP 메시지 바디로 바로 입력) 사용 가능
  • @PathVariabble을 사용하면 path 파라미터 변수를 쉽게 사용할 수 있다.

 

 

로깅 (Logging)

@S1f4j 어노테이션으로 쉽게 사용 가능

log.trace("trace log={}", name);
log.debug("debug log={}", name);
log.info(" info log={}", name);
log.warn(" warn log={}", name);
log.error("error log={}", name);

 

로그 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에
    맞게 조절할 수 있다.
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다.
    특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
  • 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 실무에서는 로그를 사용한다.

 

Header 조회

@RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                                                    headerMap,
                                                    String cookie
                                                    HttpServletResponse response,
                                                    HttpMethod httpMethod,
                                                    Locale locale,
                                                    @RequestHeader MultiValueMap<String, String>
                                                    @RequestHeader("host") String host,
                                                    @CookieValue(value = "myCookie", required = false)
){
}
    log.info("request={}", request);
    log.info("response={}", response);
    log.info("httpMethod={}", httpMethod);
    log.info("locale={}", locale);
    log.info("headerMap={}", headerMap);
    log.info("header host={}", host);
    log.info("myCookie={}", cookie);
    return "ok";
}

 

GET,POST 요청 파라미터

// default value 지정, 필수 요청 지정
@RequestParam(required = true, defaultValue = "guest")

// Map 형태로 받기
@RequestParam Map<String, Object> paramMap

// 요청 파라미터 객체 형태로 받기, Model Attribute 생략 가능
@ModelAttribute HelloData helloData

// 메시지 바디 정보를 직접 조회
@RequestBody String messageBody

// 메시지 바디 정보 직접 반환, String으로 반환시에도 view 조회 안함
@ResponseBody

// message body에 담긴 정보를 객체 형식으로 변환
@RequestBody HelloData data

// ResponseBody를 이용해 객체 형식으로 반환하면 json형식으로 변환되어 응답됨
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return helloData;
}

 

Message Converter

 

HTTP 요청 데이터 읽기

  1. HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용한다.
    메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출한다.
    1. 대상 클래스 타입을 지원하는가.
      예) @RequestBody 의 대상 클래스
    2. HTTP 요청의 Content-Type 미디어 타입을 지원하는가.
      예) text/plain , application/json , /
  2. canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환한다.

 

 

HTTP 응답 데이터 생성

  1. 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환된다. 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출한다.
    1. 대상 클래스 타입을 지원하는가.
      예) return의 대상 클래스 ( byte[] , String , HelloData )
    2. HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces )
      예) text/plain , application/json , /
  2. canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.