스프링 MVC - 기본기능

유동우·2023년 5월 3일
0
post-thumbnail

로깅 간단히 알아보기

SLF4J : 로그 라이브러리의 인터페이스
Logback : 구현체

스프링 부트가 기본으로 Logback을 제공하고 대부분 사용한다.

@Controller 와 @RestController의 차이

Controller는 반환 값이 String이면 view로 인식되어 뷰 리졸버가 필요한데
(뷰를 찾고 뷰가 렌더링 되기 위함)

RestController는 String을 그냥 반환해버린다 (HTTP 메시지 바디에 바로 입력)
(Rest API 의 Rest이다)

System.out.println() 은 항상 출력을 하기때문에 사용하지 않는다

log 메서드 사용

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

log의 올바른 사용

log.debug("data="+data) 
log.debug("data={}", data)

log 레벨에 따라 출력할 때 첫번째처럼 더하기연산을 사용하면 출력하지 않을 때도
더하기 연산이 실행되어 메모리가 낭비된다.

따라서 꼭 두번째처럼 사용해야 한다.

요청 매핑

매핑정보

@RestController

  • @Controller는 반환값이 String 이면 뷰 이름으로 인식된다 -> 뷰를 찾고 뷰가 랜더링 된다
  • @RestController는 뷰를 찾지 않고 HTTP 메시지 바디에 바로 입력한다

@RequestMapping("/hello-basic")

  • /hello-basic URL 호출이 오면 이 메서드가 실행
  • 배열로 다중 설정 가능 {"/hello-basic", "/hello-go"}

HTTP 메서드

@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)

HTTP 메서드를 축약하여

  • @GetMapping, @PostMapping 등 사용 가능

PathVariable(경로 변수) 사용

  • @PathVariable("userId") String userId -> @PathVariable userId
    (userId = userID 로 변수명이 같으면 생략이 가능)

특정 파라미터 조건 매핑
특정 헤더 조건 매핑
미디어 타입 조건 매핑


요청 매핑 - API 예시


HTTP 요청 - 기본, 헤더 조회

@RequestMapping("/headers")
    public String headers(HttpServletRequest request,
	HttpServletResponse response,
	HttpMethod httpMethod, //HTTP 메서드 조회
	Locale locale, //Locale 정보 조회
	@RequestHeader MultiValueMap<String, String> headerMap, //모든 HTTP 헤더를 MultiValueMap 형식으로 조회, MAP과 유사, 하나의 키에 여러 값을 받을 수 있음
	@RequestHeader("host") String host, // 특정 HTTP 헤더를 조회
	@CookieValue(value = "myCookie", required = false) String cookie //특정 쿠키를 조회 
)

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

클라이언트 -> 서버 데이터 전달 세 가지 방법

  • GET - 쿼리 파라미터
  • POST - HTML Form
  • HTTP message body에 데이터를 직접 담아서 요청

GET, POST 전송 방식 모두 형식이 같으므로 구분없이 조회 가능 -> 요청 파라미터 조회라 한다.

@RequestMapping("/request-param-v1")
	  //반환 타입이 없고 응답에 값을 직접 넣으면 view 조회 X
      
      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");

HTTP 요청 파라미터 - @RequestParam

스프링이 제공하는 @RequesetParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다

@ResponseBody //View조회를 무시하고, HTTP message vody에 직접 해당 내용 입력
@RequestMapping("/request-param-v2")
public String requestParamV2(
        @RequestParam("username") String memberName,  // = @RequestParam String username
        @RequestParam("age") int memberAge) // = @RequestParam int age 로 축약 가능 {
    log.info("username={}, age={}", memberName, memberAge);
    
    return "ok";
}

//RequestParam 까지도 완전히 생략할 수 있지만 너무 없어도 과하다 !!
//기본 값 적용 - requestParamDefault

@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age)

//required 의 기본값 = true (빈 문자는("") 통과시키기 때문에 주의
//defaultValue 가 있을 경우에는 required는 의미가 없음

HTTP 요청 파라미터 - @ModelAttribute

//요청 파라미터를 바인딩 받을 객체 생성
package hello.springmvc.basic;

import lombok.Data; //@Data, @Getter, @Setter, @ToString, @EqualsAndHashCode, 
					//@RequiredArgsConstructor 를 자동으로 적용해준다.

@Data
public class HelloData {
   private String username;
   private int age;
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
   //@ModelAttribute 생략 가능 = @RequestParam 또한 생략 가능
   //String, int, Integer와 같은 단순 타입 = @RequestParam 호출
   //argument resolver로 지정한 타입과 위 단순 타입을 뺀 나머지는 @ModelAttribute 호출
  log.info("username={}, age={}", helloData.getUsername(),
  helloData.getAge());

  return "ok";
}

스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
1. HelloData 객체를 생성
2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
(예시) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.


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

요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없다. (물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)

 @PostMapping("/request-body-string-v1")
  public void requestBodyString(HttpServletRequest request, HttpServletResponse response) 
  			throws IOException
                
 @PostMapping("/request-body-string-v2")
  public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)
			throws IOException
            
 @PostMapping("/request-body-string-v3")
  public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity)
//스프링MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, 
//이때 HTTP 메시지 컨버터( HttpMessageConverter )라는 기능을 사용한다.


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

HTTP 요청 메시지 - JSON

@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request,
  HttpServletResponse response) throws IOException
  
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) 	
	throws IOException
    
    
//JSON을 문자로 바꾸고 다시 JSON으로 변환하기 귀찮다
//@Requestbody 는 생략 불가능 !! (@ModelAttribute가 우선적용된다)
@PostMapping("/request-body-json-v3")
  public String requestBodyJsonV3(@RequestBody HelloData data) {
   		log.info("username={}, age={}", data.getUsername(), data.getAge());
        
        return "ok";
}

@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
      HelloData data = httpEntity.getBody();
      log.info("username={}, age={}", data.getUsername(), data.getAge());
      return "ok";
}


@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
      log.info("username={}, age={}", data.getUsername(), data.getAge());
	  return data;
}

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

스프링 서버에서 응답 데이터를 만드는 세 가지 방법

  • 정적 리소스
    예) 웹 브라우저에 정적인 HTML, css, js를 제공할 때는, 정적 리소스를 사용한다.
  • 뷰 템플릿 사용
    예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
  • HTTP 메시지 사용
    예) HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

정적 리소스의 경로 : src/main/resources/static
=> 해당 파일 변경없이 그대로 실행

뷰 템플릿 경로: src/main/resources/templates

public class ResponseViewController {

   @RequestMapping("/response-view-v1")
   public ModelAndView responseViewV1() {
       ModelAndView mav = new ModelAndView("response/hello")
              .addObject("data", "hello!");
       return mav; 
   }

  @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!!");
  }
}

String을 반환하는 경우 - View or HTTP 메시지

  • @ResponseBody 가 없으면 response/hello 로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링 한다.

  • @ResponseBody 가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello 라는 문자가 입력된다.

Void를 반환하는 경우

  • @Controller 를 사용하고, HttpServletResponse , OutputStream(Writer) 같은 HTTP 메시지
    바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용

    (참고로 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않는다.)

HTTP 메시지

  • @ResponseBody , HttpEntity 를 사용하면, 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 응답 데이터를 출력할 수 있다.

HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

	//서블릿을 직접 다룰 때 처럼 HttpServletResponse 객체를 통해서 
    //HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException
	{
        response.getWriter().write("ok");
    }
    
    //ResponseEntity 엔티티는 HttpEntity 를 상속 받았는데, 
    //HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다.
    //ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() {
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }
    
    //@ResponseBody 를 사용하면 view를 사용하지 않고, 
    //HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. 
    //ResponseEntity 도 동일한 방식으로 동작한다.
    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }

	//ResponseEntity 를 반환한다. 
    //HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.
	@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);
    }
    
    
    //ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, 
    //@ResponseBody 를 사용하면 이런 것을 설정하기 까다롭다.
	//@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 메시지 컨버터

뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지
바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

<img src=https://velog.velcdn.com/images/dw_db/post/d23f215d-a02f-44f6-a945-290ac8ed3b9f/image.png 60%>

ResponseBody를 사용

  • HTTP의 BODY에 문자 내용을 직접 반환한다

  • viewResolver 대신에 HttpMessageConverter가 동작한다

  • 기본 바이트처리: 0 = ByteArrayHttpMessageConverter

    	  클래스타입 : byte[], 미디어타입: */*,
    요청 예) @RequestBody byte[] data
    	  응답 예) @ResponseBody return byte[] 
    쓰기 미디어타입 application/octet-stream
  • 기본 문자처리: 1 = StringHttpMessageConverter

    	  클래스 타입: String , 미디어타입: */*
    	  요청 예) @RequestBody String data
    	  응답 예) @ResponseBody return "ok" 
    쓰기 미디어타입 text/plain
  • 기본 객체처리: 2 = MappingJackson2HttpMessageConverter

    	  클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
    	  요청 예) @RequestBody HelloData data
    응답 예) @ResponseBody return helloData 
    쓰기 미디어타입 application/json 관련
//HTTP 메시지 컨버터 인터페이스

 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 메시지 컨버터는 요청과 응답 모두 사용
  • canRead(), canWrite() 메서드로 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크

요청 매핑 헨들러 어뎁터 구조

HTTP 메시지 컨버터는 스프링 MVC 구조의 어디쯤에서 쓰일까?

답은 애노테이션 기반의 컨트롤러, @RequestMapping을 처리하는 핸들러 어탭터인 RequestMappingHandlerAdapter(요청 매핑 헨들러 어뎁터)에 있다.

애노테이션 기반의 컨트롤러에서 다양한 파라미터를 사용할 수 있던 이유는 ArgumentResolver 덕분이다.
애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 ArgumentResolver를 생성해서 컨트롤러(헨들러)가 필요로하는 파라미터값 (객체)를 생성한다.

ArgumentReslove의 풀네임은 HandlerMethodArgumentResovler
ReturnValueHandler의 풀네임은 HandlerMethodReturnValueHandler


public interface HandlerMethodArgumentResolver {
      
      boolean supportsParameter(MethodParameter parameter); //해당 파라미터 지원여부 확인
      
	  @Nullable 
      Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; //실제 객체 생성 -> 컨트롤러 호출시 넘어간다
}

정리

  • 요청: @RequestBody와 HttpEntity 각각을 처리하는 ArgumentResolver가 존재
    이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성

  • 응답: @ResponseBody와 HttpEntity 각각을 처리하는 ReturnValueHandler가 존재
    이 ReturnValueHandler 들이 HTTP 메시지 컨버터를 호출해서 응답결과 생성


정리


Reference
김영한 님 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
효율적이고 꾸준하게

0개의 댓글