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
그외 생략
- 대상 클래스 타입과 미디어 타입을 확인해서 메세지 컨버터 사용 여부 결정
- 조건을 만족하지 않을 경우, 다음 순위의 메세지 컨버터로 넘어감
ByteArrayHttpMessageConverter
byte[]
데이터 처리- 클래스 타입:
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가 컨트롤러가 필요한 파라미터를 생성해서 컨트롤러를 호출하면서 생성한 값을 넘겨줌
-
-
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
- 응답값을 변화하고 처리하는 인터페이스
- 스프링에서 지원하는 컨트롤러 메서드 응답값 (ex. @ResponseBody, HttpEntity<B>, String 등)
-
HttpMessageConverter
- 요청: ArgumentResolver에서 메세지 컨버터를 이용해서 필요한 객체 생성
- 응답: ReturnValueHandler에서 메세지 컨버터를 이용해서 응답 객체 생성
RequestResponseBodyMethodProcessor
: @RequestBody, @ResponseBody가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolverHttpEntityMethodProcessor
: HttpEntity가 있을 경우 메세지 컨버터를 이용하는 ArgumentResolver
-
기능 확장
- HandlerMethodArgumentResolver, HandleMethodReturnValueHandler, HttpMessageConverter 모두 인터페이스로 제공되어 필요한 기능 확장 가능
WebMvcConfigurer
를 상속받아 스프링 빈으로 등록하여 기능 확장 가능