Spring MVC의 기본 기능
05 Nov 2023스프링 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";
}
}
요청 매핑
-
@RequestMapping에method속성을 지정하지 않으면, 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속성을 적용함
- 생략할 경우, 스프링 MVC 내부에서
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을 논리 뷰 이름으로 사용해서 뷰를 찾음- 명시적이지 않고 딱 맞는 경우가 많지 않아서 권장되지 않는 방법
@ResponseBody나HttpEntity를 이용하면 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)
- HTTP 요청:
-
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
그외 생략
- 대상 클래스 타입과 미디어 타입을 확인해서 메세지 컨버터 사용 여부 결정
- 조건을 만족하지 않을 경우, 다음 순위의 메세지 컨버터로 넘어감
ByteArrayHttpMessageConverterbyte[]데이터 처리- 클래스 타입:
byte[], 미디어 타입:*/* - 응답의 경우, 미디어 타입은
application/octet-stream이 됨
StringHttpMessageConverter- 문자열 데이터 처리
- 클래스 타입:
String, 미디어 타입:*/* - 응답의 경우, 미디어 타입은
text/plain이 됨
MappingJackson2HttpMessageConverter- 클래스 타입: 객체 또는
HashMap, 미디어 타입:application/json
- 클래스 타입: 객체 또는
-
HTTP 요청 데이터 읽는 과정
- HTTP 요청이 오고 해당 컨트롤러는
@RequestBody또는HttpEntity파라미터 사용 - 메세지 컨버터가 메세지를 읽을 수 있는 확인하기 위해
canRead()메서드 호출- 대상 클래스 타입을 지원하는지 확인
- HTTP 요청의 Content-Type 미디어 타입을 지원하는지 확인
- 조건을 충족할 경우,
read()를 호출해서 객체를 생성 후 반환
- HTTP 요청이 오고 해당 컨트롤러는
-
HTTP 응답 데이터 생성 과정
- 컨트롤러에서
@ResponseBody또는HttpEntity로 값을 반환 - 메세지 컨버터가 메세지를 쓸 수 있는지
canWrite()통해서 확인- 대상 클래스 타입을 지원하는지 확인
- HTTP 요청의 Accept 미디어 타입을 지원하는지,
@RequestMapping에produces가 있을 경우 해당 타입을 지원하는지 확인
- 조건을 충족할 경우,
write()호출해서 HTTP 응답 메세지 바디에 데이터 생성
- 컨트롤러에서
RequestMappingHandlerAdapter 구조
-
동작 방식
-
DispatcherServlet에서 RequestMappingHandlerAdapter 호출
-
RequestMappingHandlerAdapter에서 argumentResolver를 호출
-
argumentResolver가 컨트롤러가 필요한 파라미터를 생성해서 컨트롤러를 호출하면서 생성한 값을 넘겨줌
-
-
HandlerMethodArgumentResolversupportsParameter(): 해당 리졸버에 의해 주어진 메서드 파라미터가 지원되는지 여부 판별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- 응답값을 변화하고 처리하는 인터페이스
- 스프링에서 지원하는 컨트롤러 메서드 응답값 (ex. @ResponseBody, HttpEntity<B>, String 등)
-
HttpMessageConverter- 요청: ArgumentResolver에서 메세지 컨버터를 이용해서 필요한 객체 생성
- 응답: ReturnValueHandler에서 메세지 컨버터를 이용해서 응답 객체 생성
RequestResponseBodyMethodProcessor: @RequestBody, @ResponseBody가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolverHttpEntityMethodProcessor: HttpEntity가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolver
-
기능 확장
- HandlerMethodArgumentResolver, HandleMethodReturnValueHandler, HttpMessageConverter 모두 인터페이스로 제공되어 필요한 기능 확장 가능
WebMvcConfigurer를 상속받아 스프링 빈으로 등록하여 기능 확장 가능