스프링 MVC

OneTwoThree·2023년 9월 10일
0

스프링mvc1

목록 보기
4/5

출처

구조

전 강의에서 만들었던 FrontController가 스프링 MVC에서는 DispatcherServlet이다
(제일 중요함)

DispatcherServelt도 부모 클래스에서 HttpServlet을 상속받아 사용하고 서블릿으로 동작한다.

스프링부트가 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로에 대해 매핑한다
(urlPatterns="/")

DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이딩 해뒀다.
service 호출에 의해 여러 메서드가 호출되면서 최종적으로 DispatcherServlet.doDispatch()가 호출된다.

핸들러 매핑, 핸들러 어댑터, 뷰 리졸버, 뷰 등이 모두 인터페이스로 선언되어 있다. 따라서 DispatcherServlet의 코드 변경 없이 원하는 기능을 변경하거나 확장할 수 있다.

동작 순서
1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(=컨트롤러) 조회
2. 핸들러 어댑터 조회 : 매핑된 핸들러를 실행할 수 있는 핸들러 어댑터 조회
3. 핸들러 어댑터 실행
4. 핸들러 실행 : 핸들러 어댑터가 핸들러를 실행한다
5. ModelAndView 반환 : 핸들러 어댑터는 핸들러의 반환 정보를 ModelAndView로 변환해서 반환한다
6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다
7. view 반환 : 뷰 리졸버가 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
8. 뷰 렌더링 : 뷰를 통해 뷰를 렌더링 한다

핸들러 매핑과 핸들러 어댑터
특정 컨트롤러가 호출되기 위해서는 이러한 과정을 거친다

  • 핸들러 매핑
    • 핸들러 매핑에서 컨트롤러를 찾을 수 있어야 한다
  • 핸들러 어댑터
    • 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다

스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터 대부분을 구현해두었다.

우선순위대로 찾고 없을 경우 다음으로 넘어간다.

뷰 리졸버

스프링부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동 등록한다.
이 때 application.properties에 등록한 정보를 토대로 등록한다.

스프링부트가 자동 등록하는 뷰 리졸버는 아래와 같다

뷰 리졸버의 동작방식은 이러하다.
1. 핸들러 어댑터 호출 : 논리 뷰 이름 획득
2. 뷰리졸버 호출 : 우선순위에 따라 뷰 리졸버를 호출한다
3. 뷰리졸버가 뷰 반환
4. 뷰를 렌더링한다

@RequestMapping

앞에서 봤듯이 가장 우선순위가 높은 핸들러 매핑과 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다.
실무에서는 대부분 애노테이션 기반의 컨트롤러를 사용한다.

실용적인 방식은 아래와 같다

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    MemberRepository memberRepository =  MemberRepository.getInstance();

    @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";
    }
}
  • Model을 파라미터로 받을 수 있다
    • model.addAttribute("member",member)로 뷰에 데이터를 전달할 수 있다
  • View의 논리 이름을 직접 반환할 수 있다
    • 물리 이름 : 실제 경로
    • 논리 이름 : 식별을 위한 논리적인 이름, 위의 예시에서 사용한 이름들
  • @RequestParam 사용 : HTTP 요청 파라미터를 받는다
    • Get 방식의 쿼리 파라미터
    • POST Form 방식 모두 지원
  • 애노테이션으로 HTTP 메서드를 구분한다

기본 기능

@Controller : 반환 값이 String이면 View 이름으로 인식한다. 뷰를 찾고 뷰가 렌더링 된다.

@RestController : 반환 값으로 View를 찾는 것이 아니라, HTTP 메세지 바디에 바로 입력한다.

@GetMapping , @PostMapping 등 HTTP 메서드 구분해서 매핑 가능하다.

@RequestMapping으로 사용할 경우 모든 메서드를 허용한다.

@PathVariable 으로 URL 경로를 템플릿화 할 수 있다.

✔ 특정 헤더 조건으로 매핑

/**
 * 특정 헤더로 추가 매핑
 * headers="mode",
 * headers="!mode"
 * headers="mode=debug"
 * headers="mode!=debug" (! = )
 */
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
 log.info("mappingHeader");
 return "ok";
}

✔ 미디어 타입 조건 매핑 Content-Type, Consume

/**
 * Content-Type 헤더 기반 추가 매핑 Media Type
 * consumes="application/json"
 * consumes="!application/json"
 * consumes="application/*"
 * consumes="*\/*"
 * MediaType.APPLICATION_JSON_VALUE
 */
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
 log.info("mappingConsumes");
 return "ok";
}

HTTP 요청의 content-type 헤더를 기반으로 미디어 타입으로 매핑한다.
맞지 않을 경우 415(Unsupported Media Type)을 반환한다.

✔ 미디어 타입 조건 매핑 Accept, produce

/**
 * Accept 헤더 기반 Media Type
 * produces = "text/html"
 * produces = "!text/html"
 * produces = "text/*"
 * produces = "*\/*"
 */
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
 log.info("mappingProduces");
 return "ok";
}

HTTP 요청의 Accept 헤더를 기반으로 매핑한다. 맞지 않으면 406(Not Acceptable)을 반환한다.

HTTP 헤더 조회

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/hedaers")
    public String headers(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpMethod httpMethod,
            Locale locale,
            @RequestHeader MultiValueMap<String,String> headerMap,
            @RequestHeader("host") String host,
            @CookieValue(value="myCookie", required = false) String cookie
            ){
        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";
    }

}

@RequestHeader MultiValueMap<String,String> headerMap
모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다. MultiValueMap은 하나의 키에 여러 값을 받을 수 있다
@RequestHeader("host") String host 특정 HTTP 헤더를 조회한다.
@CookieValue(value="myCookie", required = false) String cookie
특정 쿠키를 조회한다

헤더와 쿠키에 모두 required, defaultValue 속성 사용 가능하다.

HTML 요청 파라미터 : 쿼리 파라미터, HTML FORM

클라이언트에서 서버로 요청 데이터를 전달하는 방법은 크게 3가지다.

  1. GET 쿼리 파라미터
  • /url?username=hello&age=20
  • URL의 쿼리 파라미터에 데이터를 포함해서 전달
  • 검색, 필터, 페이징 등에서 많이 사용
  1. POST - HTML FORM
  • content-type : application/x-www-form-urlencoded
  • 메세지 바디에 쿼리 파라미터 형식으로 전달
  1. HTTP 메세지 바디에 데이터를 직접 담아서 요청
  • HTTP API에서 주로 사용
  • 주로 JSON 형식 사용
  • POST, PUT, PATCH

요청 파라미터
1,2번 방식 모두 형식이 요청 파라미터 형식으로 같아서 구분없이 조회할 수 있다.
둘 다 요청 파라미터라 한다.

요청 파라미터를 조회하는 다양한 방법이 있다.

단순히 HttpServletRequest가 제공하는 request.getParameter("username") 과 같이 조회할 수 있다.

RequestParam
스프링에서 제공하는 @RequestParam을 사용해서 요청 파라미터를 편리하게 조회할 수 있다.

@RequestParam("username") String memberName 과 같이 사용하면 된다. 컨트롤러에서 파라미터로 바로 값을 사용할 수 있다.

그리고 @RequestParam("username") String memberName 과 같은 경우 실제 쿼리파라미터 명은 username이 된다. 즉 요청할 때 api/v1?username=jidam 과 같이 요청이 들어오는 것이다.

만약 HTTP 파라미터 이름이 변수 이름과 같다면 @RequestParam String name과 같이 생략할 수 있다.

@RequestParam(required=true) String name
파라미터 필수 여부를 결정한다. 기본값이 true이다

@RequestParam(defaultValue="guest") String name
파라미터에 값이 없을 때 defaultValue 속성을 활용해 기본 값을 적용할 수 있다. 이때는 required 속성은 의미가 없어진다.

파라미터가 여러개인 경우 Map이나 MultiValueMap을 활용할 수 있다.
파라미터가 여러개라면 MultiValueMap을 사용하자.

ModelAttribute

일반적으로 파라미터를 통해 값을 입력받고 그 값으로 객체를 생성한다
예를 들자면 아래와 같다

    public Member test(@RequestParam String name){
        Member member = new Member(1L, name);
        return member;
    }

스프링에서는 @ModelAttribute를 통해 이 과정을 자동화 할 수 있다

    public Member test2(@ModelAttribute Member member){
        return member;
    }

위와 같이 @ModelAttribute가 있으면 자동으로 쿼리 파라미터에서 Member에 필드에 해당하는 값을 찾아서 값을 넣어준다.

이 때, age=abc 처럼 숫자가 들어가야 할 곳에 문자를 넣으면 바인딩 오류가 발생한다.

HTTP 바디를 통한 요청

요청 파라미터 (쿼리파라미터 또는 html form) 이 아닌 HTTP 요청 바디에 직접 데이터가 넘어오는 경우는 @RequestParam이나 @ModelAttribute를 사용할 수 없다.

HttpEntity

    @PostMapping("api/v1")
    public HttpEntity<String> httpEntity(HttpEntity<String> httpEntity){
        HttpHeaders headers = httpEntity.getHeaders();
        String body = httpEntity.getBody();
        return new HttpEntity<>("ok");
    }

HttpEntity를 사용하면 HTTP header, body 정보를 편리하게 조회할 수 있다. 응답에도 사용할 수 있다. 메세지 바디 정보를 직접 반환하며 헤더 정보도 포함할 수 있다.

RequestBody

    @ResponseBody
    @PostMapping("api/v2")
    public String requestBody(@RequestBody String messageBody){
        log.info("messageBody = {}",messageBody);
        return "OK";
    }

RequestBody를 통해 메세지 바디 정보를 편리하게 조회할 수 있다.
헤더 정보가 필요하면 RequestHeader를 사용하면 된다

또한 ResponseBody를 사용하면 응답 결과를 Http 메세지 바디에 직접 담아서 전달할 수도 있다.

    @ResponseBody
    @PostMapping("api/v3")
    public String requestBodyJson(@RequestBody Member member){
        log.info("member.getName = {}",member.getName());
        return member.getName();
    }

RequestBody를 사용하면 Json 요청도 객체로 바로 변환할 수 있다.
이 때, Http 메세지 컨버터가 HTTP 바디의 내용을 우리가 원하는 문자, 객체의 내용으로 변환해준다. 또한 JSON도 객체로 변환해준다.

JSON으로 요청할 때는 HTTP 요청 content-type이 application/json이어야 한다.

HTTP 응답

스프링에서 HTTP 응답을 하는 방법은 크게 3가지이다
1. 정적 리소스
웹 브라우저에 정적인 HTML,css,js 제공
2. 뷰 템플릿
웹 브라우저에 동적인 HTML 제공
3. HTTP 메세지
HTTP API를 제공하는 경우 HTML이 아니라 데이터를 제공하므로 HTTP 메세지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다

정적 리소스

정적 리소스는 파일을 변경 없이 그대로 서비스하는 것

src/main/resources/static 경로에 정적 리소스를 넣을 수 있다.
src/main/resources/static/basic/hello.html 파일이 있다면
http:://{HOST}:8080/basic/hello.html 과 같이 요청하면 된다.

뷰 템플릿
뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
일반적으로 HTML을 동적으로 만드는 데 사용한다.

스프링부트 기본 뷰 템플릿 경로
src/main/resources/templates

    @RequestMapping("/view")
    public String view(Model model){
        model.addAttribute("data","hello");
        return "response/hello";
    }

위와 같이 String을 담아서 응답하고 @ResponseBody가 없을 때는
src/main/resources/templates/response/hello에 있는 뷰를 반환한다.

@ResponseBody가 있다면 Http 메세지 바디에 직접 response/hello라는 문자가 입력된다.

메세지 바디에 직접 입력

HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메세지 바디에 HTML 데이터가 담겨서 전달된다.

    @GetMapping("/responseEntity")
    public ResponseEntity<Member> responseEntity(){
        return ResponseEntity.ok().body(new Member(1L,"jidam"));
    }

ResponseEntity는 HttpEntity를 상속받는다. Http응답의 헤더, 바디, 응답 코드를 설정할 수 있다.
return new ResponseEntity<>(member,HttpStatus.OK)와 같이 응답할 수도 있다.
HTTP 메세지 컨버터에 의해 JSON 형식으로 변환되어서 반환한다.

    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/responseBody")
    public Member responseBody(){
        return new Member(1L,"jidam");
    }

ResponseEntity를 사용하지 않고 @ResponseBody를 사용할 수도 있다.
이 때는 HTTP 상태코드는 @ResponseStatus(HttpStatus.OK)와 같이 지정한다.

@RestController 애노테이션은 @Controller + @ResponseBody 이다.

0개의 댓글