Spring MVC의 기본 기능

스프링 MVC 1편 강의 정리

Logging

로깅 라이브러리

  • 스프링 부트를 사용하면 spring-boot-starter-logging 라이브러리가 포함됨

    • SLF4J, Logback 을 기본으로 사용
  • 로그 선언

    privage Logger log = LoggerFactory.getLogger(getClass());
    private static final Logger log = LoggerFactory.getLogger(Xxx.class);
    @Slf4j // lombok 사용 가능
    
  • 로그 레벨

    • TRACE > DEBUG > INFO > WARN > ERROR
    # 전체 로그 레벨 설정 (기본은 info)
    logging.level.root=debug
      
    # hello.springmvc 패키지와 하위 로그 레벨 설정
    logging.level.hello.springmvc=trace
    
  • 올바른 로그 사용법

    • log.info("data={}", data);
    • "data" + data 처럼 + 연산자를 사용할 경우, 로그 출력 레벨이 아니어도 문자 더하기 연산이 실행되어 불필요한 메모리, CPU 사용 발생
  • 로그 사용시 장점

    • 쓰레드 정보, 클래스 이름 등의 부가 정보 확인 가능
    • 출력 형식 조정 가능
    • 각 서버에 따라 로그 레벨 다르게 설정 가능
    • 로그를 파일, 네트워크 등 별도의 위치에 남길 수 있음
    • 로그를 파일로 남길 때, 특정 용량에 따라 분할 가능
    • System.out 보다 내부 버퍼링, 멀티 쓰레드 등 성능 좋음
import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Slf4j
@RestController
public class LogTestController {

  	// @Slf4j 쓰지 않을 경우, 아래처럼 선언하여 사용
    // private final Logger log = LoggerFactory.getLogger(LogTestController.class);

    @GetMapping("/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);

        return "OK";
    }
}

요청 매핑

  • @RequestMappingmethod 속성을 지정하지 않으면, HTTP 메서드와 무관하게 호출 가능

  • @PathVariable

    • @RequestMapping의 URL에 경로에 템플릿화 된 부분을 @PathVariable 이용해서 조회 가능
    @GetMapping("/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
          log.info("mappingPath userId={}, orderId={}", userId, orderId);
          return "ok";
      }
      
    
  • params 속성

    • @RequestMapping의 속성으로 특정 파라미터의 조건 추가 가능
    • 예: params=”mode”, params=”!mode”, params=”mode!=debug”, params={“mode=debug”, “name=tom”}
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }
    
  • headers 속성

    • HTTP 헤더 설정 가능
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }
    
  • consumes 속성

    • Content-Type 설정 가능
    • 해당 값을 직접 쓰거나 MediaType 이용 가능
    • 요청이 설정한 Content-Type 헤더 값과 맞지 않을 경우, 상태 코드 415(Unsupported Media Type) 반환
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }
    
  • produces 속성

    • Accept 설정 가능
    • 요청이 설정한 값과 맞지 않은 Accept 헤더 값을 가질 경우, 상태 코드 406(Not Acceptable) 반환
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
    

HTTP 요청 - 헤더 조회

  • HttpServletRequest, HttpServletResponse : HTTP 요청, 응답 관련 값
  • HttpMethod : HTTP 메서드 조회
  • Locale : locale 정보 조회
  • @RequestHedaer MultiValueMap<String, String> headeMap
    • 모든 HTTP 헤더를 MultiValueMap 형식으로 조회
    • MultiValueMap: 하나의 키에 여러 값을 가질 수 있음
  • @RequestHeader("{headerName}" String {headerName})
    • 특정 HTTP 헤더 조회
    • required : 필수 여부 속성
    • defaultValue : 기본 값 지정 속성
  • @CookieValue(value = "{cookieName}") String {cookieName}
    • 특정 쿠키 조회
    • required : 필수 여부 속성
    • defaultValue : 기본 값 지정 속성

HTTP 요청 파라미터 - @RequestParam

  • name/value 속성
    • 파라미터 이름을 넣어서 조회 가능
    • @RequestParam("username") String memberName
    • 파라미터 이름과 변수 이름이 같을 경우, 생략 가능
  • String, int, Integer 등의 단순 타입이면 @RequestParam 생략 가능
    • 생략할 경우, 스프링 MVC 내부에서 required=false 속성을 적용함
  • required 속성
    • true가 기본값으로, 해당 파라미터가 없을 경우 400 예외 발생
    • required 설정이 되어 있고 해당 파라미터 이름만 있고 값이 없을 경우 빈 문자열이 값이 됨
    • false일 경우, 해당 파라미터의 값은 Null이 됨
    • 기본형(primitive) 타입은 false일 경우, null이 될 수 없으므로 값을 안 보내면 500 에러 발생
    • 기본형 타입은 Integer 등의 타입을 쓰거나 defaultValue를 사용하는 것이 좋음
  • defaultValue 속성
    • 파라미터에 값이 없을 경우, 기본 값을 적용하게 하는 속성
    • 파라미터 이름만 있고 값이 없을 경우에도 기본 값이 적용됨
  • Map, MultiValueMap 으로 조회
    • 파라미터를 map 타입으로 조회
    • 파라미터 값이 1개 이상일 경우, MutliValueMap으로 조회 가능

HTTP 요청 파라미터 - @ModelAttribute

  • @ModelAttribute를 이용하면 요청 파라미터를 받아서 객체를 만드는 과정을 자동화해줌
  • 스프링 MVC에서 @ModelAttribute가 있을 경우, 객체를 생성하고 요청 파라미터의 이름으로 해당 객체의 프로퍼티를 찾아 setter 메서드를 호출하여 파라미터 값을 바인딩
@Data
public class User {
  private String name;
  privage int age;
}

@ResponeBody
@RequestMapping("/users")
public void users(@ModelAttribute User user) {
  log.info("user={}", user);  // user=User(name=tom, age=23)
}
  • @ModelAttribute는 생략 가능
  • @RequestParam도 생략 가능하여 스프링은 String, int, integer 등의 단순 타입은 @RequestParam을 적용하고 나머지 타입은 @ModelAtrribute을 적용

HTTP 요청 메세지 - 단순 문자열

  • HTTP message body에 데이터를 담아서 요청
    • JSON, XML, TEXT 등의 형식이 있으며 주로 JSON 사용
    • POST, PUT, PATCH HTTP 메서드 사용
  • InputStream을 이용해 읽을 수 있음
@Slf4j
@Controller
public class ExampleController {
  
  @PostMapping("/test")
  public void test(HttpServletRequest request, HttpServletResponse response) {
    ServletInputStream inputStream = request.getInputStream();
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    log.info("messageBody={}", messageBody);
    response.getWriter().write("ok");
  }
}
  • HttpServletRequest, HttpServletResponse 대신 InputStream(Reader),OutputStream(Writer) 를 이용해서 조회 가능
@PostMapping("/test")
public void test(InputStream inputStream, Writer responseWriter) {
  String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
  log.info("messageBody={}", messageBody);
  responseWriter.write("ok");
}

  • HttpEntity를 이용해서 HTTP header, body 정보를 편하게 조회 가능
    • HttpMessageConverter를 사용하여 StringHttpMessageConverter가 적용됨
    • 메세지 바디 정보도 직접 반환 가능하며 view는 조회하지 않음
    • RequestEntity : HttpEntity를 상속 받으며 HTTP method, url 정보를 가짐
    • ResponseEntity : HttpEntity를 상속 받으며 상태 코드 설정 가능
@PostMapping("/test")
public HttpEntity<String> test(HttpEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);
    return new HttpEntity<>("ok");
}
  • @ResponseBody
    • HTTP body 정보 조회 가능
    • @RequestParam, @ModelAttribute는 요청 파라미터를 조회하는 기능을 가지므로 서로 역할이 다름
  • @ResponseBody
    • 응답 결과를 HTTP message body에 담아 전달 가능
    • view 사용하지 않음

HTTP 요청 메세지 - JSON

  • HttpServletRequest를 이용해서 데이터를 읽어와서 문자로 변환 가능
  • 문자로 된 JSON 데이터를 ObjectMapper 라이브러리 이용하여 객체로 변환
@Controller
public class RequestBodyJsonController {
  private ObjectMapper objectMapper = new ObjectMapper();
  
  @PostMapping("/test")
  public void requestBodyJsonV1(HttpServletRequest request) throws IOException {
      ServletInputStream inputStream = request.getInputStream();
      String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
      HelloData data = objectMapper.readValue(messageBody, HelloData.class);
  }
}
  • @RequestBody를 사용하면 HttpMessageConverter를 이용해서 StringHttpMessageConverter가 적용되어 문자열로 가져올 수 있음
@Controller
public class RequestBodyJsonController {
  
  @PostMapping("/test")
  public void requestBodyJsonV1(@ResponseBody String messageBody) throws IOException {
      HelloData data = objectMapper.readValue(messageBody, HelloData.class);
  }
}
  • @RequestBody로 데이터를 객체로 변환 가능하며 ObjectMapper를 이용하지 않아도 됨
  • MappingJackson2HttpMessageConverter 가 적용됨
  • @RequestBody는 생략할 경우, @ModelAttribute가 적용되어 메세지 바디가 아닌 요청 파리미터를 처리하게 되므로 생략할 수 없음
@Controller
public class RequestBodyJsonController {
  
  @PostMapping("/test")
  public void requestBodyJsonV1(@ResponseBody HelloData helloData) throws IOException {
			String username = helloData.getUsername();
    	int age = helloData.getAge();
  }
}
  • HttpEntity를 이용해도 메세지 바디 조회 가능
@Controller
public class RequestBodyJsonController {
  
  @PostMapping("/test")
  public void requestBodyJsonV1(HttpEntity<HelloData> httpEntity) throws IOException {
    	HelloData helloData = httpEntity.getBody();
  }
}
  • @ResponseBody를 통해 MappingJackson2HttpMessageConverter가 적용되어 응답값의 메시지 바디에 객체를 넣을 수 있음
@Controller
public class RequestBodyJsonController {
  
  @ResponseBody
  @PostMapping("/test")
  public void requestBodyJsonV1(@ResponseBody HelloData helloData) throws IOException {
    	return helloData;
  }
}

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

  • 정적 리소스: HTML, css, js을 제공할 때 사용
  • 뷰 템플릿: 동적인 HTML을 제공할 때 사용
  • HTTP 메시지: HTTP API를 사용할 때, 데이터를 전달해야 하므로 JSON 같은 형식으로 데이터 전달

정적 리소스

  • 스프링 부트에서는 클래스 패스의/static, /public, /resources, /META-INF/resources 디렉토리에 있는 정적 리소스 제공
  • /src/main/resources는 리소스를 보관하는 곳이자 클래스 패스의 시작 경로
  • src/main/resources/static/basic/form.html 파일이 있을 경우, 웹 브라우저에서는 http://localhost:8080/basic/form.html로 실행

뷰 템플릿

  • 뷰 템플릿을 거쳐서 HTML이 생성되고 뷰가 응답을 생성해서 전달
  • 스프링 부트의 기본 뷰 템플릿 경로: src/main/templates
  • String을 반환하는 경우: view 또는 HTTP 메시지
    • @ResponseBody가 없으면 뷰 리졸버가 해당 문자열의 뷰를 찾아 렌더링
    • @ResponseBody가 있으면 해당 문자열을 HTTP 메세지 바디로 반환
  • void를 반환하는 경우
    • @Controller를 사용하고 HTTP 메세지 바디를 처리하는 파라미터가 없으면 요청 URL을 논리 뷰 이름으로 사용해서 뷰를 찾음
    • 명시적이지 않고 딱 맞는 경우가 많지 않아서 권장되지 않는 방법
  • @ResponseBodyHttpEntity를 이용하면 HTTP 메시지 바디에 응답 데이터 전달 가능

Thymeleaf 설정

  • 스프링 부트가 ThymeleafViewResolver와 필요한 스프링 빈 등록
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  • 기본 설정값으로 필요한 경우에 설정
# application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

HTTP 응답 - HttpMessageConverter

  • JSON 데이터를 직접 읽거나 쓸 때 HttpMessageConverter를 이용하면 편리함

  • @ResponseBody 원리

    • 클라이언트에서 요청이 들어와서 해당 컨트롤러를 찾게 됨
    • @ResponseBody가 있을 경우 viewResolver가 아닌 컨트롤러 반환값과 HTTP Accept 헤더에 맞는 HttpMessageConverter가 동작
  • HttpMessageConverter가 적용되는 경우

    • HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
    • HTTP 응답: @ReponseBody, HttpEntity(ResponseEntity)
  • HttpMessageConverter 인터페이스

    • canRead() : 주어진 클래스가 해당 컨버터에 의해서 읽힐 수 있는지 판별
    • canWrite() : 주어진 클래스가 해당 클래스에 의해 써질 수 있는지 판별
    • getSupportedMediaTypes(): 해당 컨버터가 지원할 수 있는 미디어 타입 목록 반환
    • read() : 주어진 인풋 메세지의 타입의 객체를 읽고 반환
    • write() : 주어진 아웃풋 메세지를 주어진 객체로 쓰기
    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;
      
    }
    
  • 스프링 부트가 제공하는 기본 메세지 컨버터

    0 순위: ByteArrayHttpMessageConverter

    1 순위: StringHttpMessageConverter

    2 순위: MappingJackson2HttpMessageConverter

    그외 생략

    • 대상 클래스 타입과 미디어 타입을 확인해서 메세지 컨버터 사용 여부 결정
    • 조건을 만족하지 않을 경우, 다음 순위의 메세지 컨버터로 넘어감
    • ByteArrayHttpMessageConverter
      • byte[] 데이터 처리
      • 클래스 타입: byte[], 미디어 타입: */*
      • 응답의 경우, 미디어 타입은 application/octet-stream이 됨
    • StringHttpMessageConverter
      • 문자열 데이터 처리
      • 클래스 타입: String, 미디어 타입: */*
      • 응답의 경우, 미디어 타입은 text/plain이 됨
    • MappingJackson2HttpMessageConverter
      • 클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json
  • HTTP 요청 데이터 읽는 과정

    • HTTP 요청이 오고 해당 컨트롤러는 @RequestBody 또는 HttpEntity 파라미터 사용
    • 메세지 컨버터가 메세지를 읽을 수 있는 확인하기 위해 canRead() 메서드 호출
      • 대상 클래스 타입을 지원하는지 확인
      • HTTP 요청의 Content-Type 미디어 타입을 지원하는지 확인
    • 조건을 충족할 경우, read()를 호출해서 객체를 생성 후 반환
  • HTTP 응답 데이터 생성 과정

    • 컨트롤러에서 @ResponseBody 또는 HttpEntity로 값을 반환
    • 메세지 컨버터가 메세지를 쓸 수 있는지 canWrite() 통해서 확인
      • 대상 클래스 타입을 지원하는지 확인
      • HTTP 요청의 Accept 미디어 타입을 지원하는지, @RequestMappingproduces가 있을 경우 해당 타입을 지원하는지 확인
    • 조건을 충족할 경우, write() 호출해서 HTTP 응답 메세지 바디에 데이터 생성

RequestMappingHandlerAdapter 구조

  • 동작 방식

    • DispatcherServlet에서 RequestMappingHandlerAdapter 호출

    • RequestMappingHandlerAdapter에서 argumentResolver를 호출

    • argumentResolver가 컨트롤러가 필요한 파라미터를 생성해서 컨트롤러를 호출하면서 생성한 값을 넘겨줌

  • HandlerMethodArgumentResolver

    • supportsParameter() : 해당 리졸버에 의해 주어진 메서드 파라미터가 지원되는지 여부 판별
    • resolveArgument() : 메소드 매개변수를 주어진 요청의 인수 값으로 생성해서 반환
    • supportsParameter()를 호출해서 해당 파라미터를 지원하는지 확인하고 지원하면 resolveArgument()를 호출해서 객체를 생성하고 컨트롤러에 객체가 넘어감
    • 스프링에서 지원하는 method arguments (ex. @Requestparam, @RequestBody, @ModelAttribute, Pricipal 등)
    public interface HandlerMethodArgumentResolver {
      boolean supportsParameter(MethodParameter parameter);
      
      @Nullable
      Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
      
      }
    
  • HandleMethodReturnValueHandler

  • HttpMessageConverter

    • 요청: ArgumentResolver에서 메세지 컨버터를 이용해서 필요한 객체 생성
    • 응답: ReturnValueHandler에서 메세지 컨버터를 이용해서 응답 객체 생성
    • RequestResponseBodyMethodProcessor : @RequestBody, @ResponseBody가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolver
    • HttpEntityMethodProcessor : HttpEntity가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolver
  • 기능 확장

    • HandlerMethodArgumentResolver, HandleMethodReturnValueHandler, HttpMessageConverter 모두 인터페이스로 제공되어 필요한 기능 확장 가능
    • WebMvcConfigurer를 상속받아 스프링 빈으로 등록하여 기능 확장 가능