Spring MVC - 기본 기능

Kwon Yongho·2023년 4월 19일
0

Spring-MVC

목록 보기
5/7
post-thumbnail

스프링 MVC - 기본 기능(1)

  1. 로깅 간단히 알아보기
  2. 요청 매핑
  3. 요청 매핑 - API 예시
  4. HTTP 요청 - 기본, 헤더 조회
  5. HTTP 요청 파라미터
  6. HTTP 요청 메시지
  7. HTTP 응답
  8. HTTP 메시지 컨버터
  9. 요청 매핑 헨들러 어뎁터 구조

Spring MVC의 기본 기능을 알아보기 위해 프로젝트를 새로 생성했다.

  • Dependencies: Spring Web, Thymeleaf, Lombok

1. 로깅 간단히 알아보기

앞으로 로그를 사용할 것이기 때문에, 로그에 대해서 간단히 알아보자.

  • 운영 시스템에서는 System.out.println() 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다.

로깅 라이브러리

LogTestController

package com.example.springmvc.basic;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LogTestController {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "Spring";

        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);

        //로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X
        log.debug("String concat log=" + name);
        return "ok";
    }
}

결과

@RestController

  • @Controller 는 반환 값이 String 이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
  • @RestController 는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.

로그 레벨 설정
application.properties

#전체 로그 레벨 설정(기본 info)
logging.level.root=info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
  • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
  • 개발 서버는 debug 출력
  • 운영 서버는 info 출력

올바른 로그 사용법

  • log.debug("data="+data) x
    • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다.
  • log.debug("data={}", data)
    • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다.

로그 사용시 장점

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

요청 매핑

MappingController

package com.example.springmvc.basic.requestmapping;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
public class MappingController {
    private Logger log = LoggerFactory.getLogger(getClass());

    /**
     * 기본 요청
     * 둘다 허용 /hello-basic, /hello-basic/
     * HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
     */
    @RequestMapping("/hello-basic")
    public String helloBasic() {
        log.info("helloBasic");
        return "ok";
    }

    /**
     * method 특정 HTTP 메서드 요청만 허용
     * GET, HEAD, POST, PUT, PATCH, DELETE
     * POST 요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed) 반환한다.
     */
    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }

    /**
     * 편리한 축약 애노테이션 (코드보기)
     * @GetMapping
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */

    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }

    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable("userId") String userId -> @PathVariable userId
     */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId") String data) {
        log.info("mappingPath userId={}", data);
        return "ok";
    }

    /**
     * PathVariable 사용 다중
     */
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }

    /**
     * 파라미터로 추가 매핑
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

    /**
     * 특정 헤더로 추가 매핑
     * 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 헤더 기반 추가 매핑 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";
    }

    /**
     * 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";
    }
}

앞에서 해봤던 테스트들은 빼고 결과를 확인해보자

PathVariable 사용 - 다중

특정 파라미터 조건 매핑

  • 특정 파라미터가 있거나 없는 조건을 추가할 수 있다. 잘 사용하지는 않는다.

특정 헤더 조건 매핑

  • 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.

미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

바꾸면?

오류 발생

  • HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다.

3. 요청 매핑 - API 예시

MappingClassController

package com.example.springmvc.basic.requestmapping;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    @GetMapping
    public String users() {
        return "get users";
    }

    @PostMapping
    public String addUser() {
        return "post user";
    }

    /**
     * /mapping/users/{userId}
     */

    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }

    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }

    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }
}

Postman으로 테스트 결과 모두 정상적으로 반환하는 것을 확인해볼 수 있다.

4. HTTP 요청 - 기본, 헤더 조회

  • 애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.
  • 는 HTTP 헤더 정보를 조회하는 방법을 알아보자.

RequestHeaderController

package com.example.springmvc.basic.request;

import jakarta.servlet.http.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;

import java.util.Locale;

@Slf4j
@RestController
public class RequestHeaderController {
    @RequestMapping("/headers")
    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";
    }
}

  • HttpServletRequest
  • HttpServletResponse
  • HttpMethod: HTTP 메서드를 조회한다.
  • Locale: Locale 정보를 조회한다.
  • @RequestHeader MultiValueMap<String, String> headerMap: 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
    • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
    • keyA=value1&keyA=value2
  • @RequestHeader("host") String host: 특정 HTTP 헤더를 조회
    • 필수 값 여부: required
    • 기본 값 속성: defaultValue
  • @CookieValue(value = "myCookie", required = false) String cookie: 특정 쿠키 조회
    • 필수 값 여부: required
    • 기본 값: defaultValue

@Conroller 의 사용 가능한 파라미터 목록은 다음 공식 메뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments

5. HTTP 요청 파라미터

5-1. 쿼리 파라미터, HTML Form

클라이언트에서 서버로 요청 데이터를 전달할 때는 주로 다음 3가지 방법을 사용한다.

  • GET - 쿼리 파라미터
    • /url?username=hello&age=20
    • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
    • 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
  • POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
    • 예) 회원 가입, 상품 주문, HTML Form 사용
  • HTTP message body에 데이터를 직접 담아서 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용

서블릿이랑 똑같다. 얼마나 스프링이 편하게 제공해주는지 알아보자.

RequestParamController

package com.example.springmvc.basic.request;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.IOException;

@Slf4j
@Controller
public class RequestParamController {

    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username={}, age={}", username, age);
        response.getWriter().write("ok");
    }
}

localhost:8080/request-param-v1?username=kwonyongho&age=27 요청

5-2. @RequestParam

RequestParamController 소스 추가

    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(@RequestParam("username") String memberName, @RequestParam("age") int memberAge) {
        log.info("username={}, age={}", memberName, memberAge);
        return "ok";
    }
    
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(@RequestParam String username, @RequestParam int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(@RequestParam(required = true) String username, @RequestParam(required = false) Integer age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
    
    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "-1") int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
  • HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
  • String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
  • @RequestParam.required
    • 파라미터 필수 여부
    • 기본값이 파라미터 필수(true)이다.
  • /request-param?username= -> 빈문자로 통과
  • @RequestParam(required = false) int age
    • null 을 int 에 입력하는 것은 불가능(500 예외 발생) 따라서 null 을 받을 수 있는 Integer 로 변경하거나, 또는 defaultValue 사용
  • @RequestParam(required = true, defaultValue = "guest")
    • 파라미터에 값이 없는 경우 defaultValue 를 사용하면 기본 값을 적용할 수 있다.
    • defaultValue 는 빈 문자의 경우에도 설정한 기본 값이 적용된다.

파라미터를 Map으로 조회하기 - requestParamMap

    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }
}

파라미터는 보통 하나를 쓴다.

5-3. @ModelAttribute

  • 실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다.
  • 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.

HelloData

package com.example.springmvc.basic;

import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}

@ModelAttribute 적용 - modelAttributeV1

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

localhost:8080/model-attribute-v1?username=kwonyong?age=27 요청

스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.

  • HelloData 객체를 생성한다.
  • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
  • username 프로퍼티의 값을 변경하면 setUsername() 이 호출되고, 조회하면 getUsername()이 호출된다.

바인딩 오류

  • age=abc 처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException 이 발생한다. 이런 바인딩 오류를 처리하는 방법은 검증 부분에서 다룬다.

@ModelAttribute 생략 - modelAttributeV2

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
  • @ModelAttribute 는 생략할 수 있다.

스프링은 해당 생략시 다음과 같은 규칙을 적용한다.

  • String, int, Integer 같은 단순 타입 = @RequestParam
  • 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외)

참고
김영한: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(인프런)
Github - https://github.com/b2b2004/Spring_MVC

0개의 댓글