본문 바로가기
Spring/MVC 1편

스프링 MVC - 기본 기능

by JHyun0302 2023. 8. 5.
728x90

◎ 참고: 스프링 부트에 Jar 를 사용하면 /resources/static/ 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.
             (
스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html 이 있으면 된다.)

 

 


로깅

 

※ 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.

★ 쉽게 이야기해서 SLF4J는 인터페이스이고, 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.

 

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

 

 

매핑 정보

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

 

 

※ 로그 레벨 설정에 따른 출력 결과

  • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
  • 개발 서버는 debug 출력
  • 운영 서버는 info 출력

 

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

#hello.springmvc 패키지와 그 하위 로그 레벨 설정 
logging.level.hello.springmvc=debug

 

log.debug("data="+data) //의미없는 "+" 연산 일어남! 사용 금지
log.debug("data={}", data) // 아무 일도 발생하지 않는다. 적극 사용!

 

 


요청 매핑

 

 

 

※ PathVariable(경로 변수) 사용

/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId */

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
	log.info("mappingPath userId={}", data);
	return "ok";
}

 

  • @RequestMapping 은 URL 경로를 템플릿화 가능, @PathVariable 을 사용하면 매칭 되는 부분을 편리하게 조회 가능.
  • @PathVariable 의 이름과 파라미터 이름이 같으면 생략 가능.

 

 

 

PathVariable 사용 - 다중

/**
* 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";
}
  • 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.

 

 

 

 

 

미디어 타입 조건 매핑 - HTTP 요청 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 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환한다.

 

 

 

 

 

미디어 타입 조건 매핑 - HTTP 요청 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 헤더를 기반으로 미디어 타입으로 매핑한다.
  • 만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다.

 

 

 


요청 매핑 - API 

 

※ 회원 관리 API

  • 회원 목록 조회: GET         /users
  • 회원 등록: POST              /users
  • 회원 조회: GET                /users/{userId}
  • 회원수정: PATCH            /users/{userId}
  • 회원 삭제: DELETE         /users/{userId}

 

 

HTTP 요청 - 기본, 헤더 조회

@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 메서드를 조회한다.     org.springframework.http.HttpMethod
  • Locale : Locale 정보를 조회한다.
  • @RequestHeader MultiValueMap<String, String> headerMap
             모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
  • @RequestHeader("host") String host
        특정 HTTP 헤더를 조회한다.
        속성
            필수 값 여부: required
            기본 값 속성: defaultValue
  • @CookieValue(value = "myCookie", required = false) String cookie
        특정 쿠키를 조회한다.
        속성
            필수 값 여부: required
            기본 값: defaultValue

 

 

◎ 참고 : MultiValueMap

  • MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
        keyA=value1&keyA=value2
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");

//[value1,value2]
List<String> values = map.get("keyA");

 

 

 


HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

 

 

 

  • GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회 가능\
  • 이것을 요청 파라미터(request parameter) 조회라 한다.
@Slf4j
@Controller
public class RequestParamController {

    /**
    * 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
    */
    @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");
    }
}

 

 

@RequestParam

/**
* @RequestParam 사용
* - 파라미터 이름으로 바인딩
* @ResponseBody 추가
* - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력 
*/
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String memberName, @RequestParam("age") int memberAge) {
    
    log.info("username={}, age={}", memberName, memberAge);
    return "ok";
}

 

  • HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
 public String requestParamV3(@RequestParam String username, @RequestParam int age)

 

 

  • String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
public String requestParamV4(String username, int age)

 

 

 

 

파라미터 필수 여부 - requestParamRequired

/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 400 예외 발생
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*
* 주의!
* /request-param-required
* int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용) 
*/
@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";
}

 

 

기본 값 적용 - requestParamDefault

/**
* @RequestParam
* - defaultValue 사용 
*
* 참고: defaultValue는 빈 문자의 경우에도 적용 
* /request-param-default?username=
*/
@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";
}

 

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

/**
* @RequestParam Map, MultiValueMap
* Map(key=value)
* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
*/
@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";
}

 

 

 


 HTTP 요청 파라미터 - @ModelAttribute

 

@ModelAttribute 생략

/**
* @ModelAttribute 생략 가능
* String, int 같은 단순 타입 = @RequestParam
* argument resolver 로 지정해둔 타입 외 = @ModelAttribute */
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    return "ok";
}

 

 

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

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

 

 

 

 


  HTTP 요청 메시지 - 단순 텍스트

 

 

 

 

HttpEntity

/**
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);
    
    return new HttpEntity<>("ok");
}

 

HttpEntity 를 상속받은 RequestEntity & ResponseEntity 객체 기능

1. RequestEntity

  • HttpMethod, url 정보가 추가, 요청에서 사용

2. ResponseEntity

  • HTTP 상태 코드 설정 가능, 응답에서 사용
return new ResponseEntity<String>("Hello World", responseHeaders,HttpStatus.CREATED)

 

 

 

 

 

@RequestBody

/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
    log.info("messageBody={}", messageBody);
    return "ok";
}

 

 

 

요청 파라미터 vs HTTP 메시지 바디

  • 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

 

 

 

 

※ HTTP 요청 메시지 - JSON

 

 

문자로 된 JSON 데이터인 messageBody objectMapper 를 통해서 자바 객체로 변환

/**
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 모든 메서드에 @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
    HelloData data = objectMapper.readValue(messageBody, HelloData.class);
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return "ok";
}

 

 

 

 

@RequestBody 객체 변환

/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content- type: application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
(Accept: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return data;
}

 

 


HTTP 응답 - 정적 리소스, 뷰 템플릿

 

1. 정적 리소스

  • ex) 웹 브라우저에 정적인 HTML, css, js를 제공할 때는, 정적 리소스를 사용한다.
     

스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.

/static , /public , /resources , /META-INF/resources

 

정적 리소스 경로

src/main/resources/static

 

 

 

 

 

2. 뷰 템플릿 사용

  • ex) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.

 

뷰 템플릿 경로

src/main/resources/templates

 

ResponseViewController - 뷰 템플릿을 호출하는 컨트롤러

@Controller
public class ResponseViewController {

	@RequestMapping("/response-view-v2")
      public String responseViewV2(Model model) {
          model.addAttribute("data", "hello!!");
          return "response/hello";
      }
      
      //이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않는다.
      @RequestMapping("/response/hello")
      public void responseViewV3(Model model) { 
      	model.addAttribute("data", "hello!!");
      }
}

 

 


Thymeleaf 스프링 부트 설정

 

 

 

build.gradle 추가

`implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'`

 

 

application.properties 

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

 

 

 

 

 


3. HTTP 메시지 사용

  • HTTP API를 제공하는 경우에는데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식의 데이터를 실어 보낸다.

 

@Slf4j
@Controller
//@RestController
public class ResponseBodyController {

    //@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. 
    //ResponseEntity 도 동일한 방식으로 동작한다.
    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }
    
    //프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다.
    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        
        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }
    
    //@ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다.
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        
        return helloData;
    } 
}

 

 

 


HTTP 메시지 컨버터

 

※ HTTP 메시지 컨버터 인터페이스: org.springframework.http.converter.HttpMessageConverter

 

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    
    List<MediaType> getSupportedMediaTypes();
    
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
    
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

 

※ HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다.

  • canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
  • read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

 

스프링 부트 기본 메시지 컨버터

  // byte[] 데이터를 처리한다.
  //클래스 타입: byte[] , 미디어타입: */*
  0 = ByteArrayHttpMessageConverter
  
  //String 문자로 데이터를 처리한다.
  //클래스 타입: String , 미디어타입: */*
  1 = StringHttpMessageConverter
  
  //application/json
  //클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
  2 = MappingJackson2HttpMessageConverter

 

 

HTTP 요청 데이터 읽기

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

 

 

HTTP 응답 데이터 생성

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

 

Spring MVC 구조

 

 

 

RequestMappingHandlerAdapter&nbsp; 동작 방식

 

 

 

 

★ ArgumentResolver

  • 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용한다.
  • HttpServletRequest , Model, @RequestParam , @ModelAttribute, @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
  • 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
  • HandlerMethodArgumentResolver를 줄여서  ArgumentResolver 라고 부른다.

 

	
public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, 
    				NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

 

 

 

 

ReturnValueHandler

  • HandlerMethodReturnValueHandler 를 줄여서 ReturnValueHandler 라 부른다.
  • ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리

 

 

 

 

 

◎ 참고: 가능한 응답 값 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types

 

 

 

 

HTTP 메시지 컨버터 위치

 

요청의 경우

  • @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다.
  • 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다. 

 

 

응답의 경우

  • @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다.
  • 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.

 

 

 

◎ 참고

  • @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver)
  • HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.

 

 

 

 

◎ 기능 확장

  • 기능 확장은 WebMvcConfigurer 를 상속 받아서 스프링 빈으로 등록하면 된다.
  • 실제 자주 사용하지는 않으니 실제 기능 확장이 필요할 때 WebMvcConfigurer 를 검색해보자.

 

반응형

'Spring > MVC 1편' 카테고리의 다른 글

스프링 MVC - 웹 페이지 만들기  (0) 2023.08.05
MVC 프레임워크 구조 이해  (0) 2023.08.05
서블릿, JSP, MVC 패턴  (0) 2023.08.04
서블릿  (0) 2023.08.04