✔️로깅 라이브러리
스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리가 함께 포함된다. 스프링 푸트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
✔️application.properties
spring.application.name=springmvc
logging.level.hello.springmvc=trace
✔️로그 선언 및 로그 호출
private final Logger log = LoggerFactory.getLogger(getClass());
// ...
log.info("hello");
✔️LogTestController
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
System.out.println("name = " + name);
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";
}
}
실행 결과
@Slf4j
로 변경System.out
보다 좋다.(내부 버퍼링, 멀티 쓰레드 등등)✔️@Slf4j 롬복 어노테이션 기반 LogTestController
@Slf4j
@RestController
public class LogTestController {
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
System.out.println("name = " + name);
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";
}
}
//@Slf4j
@RestController
public class MappingController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
// ✔️HTTP 메서드
// 특정 HTTP 메서드 요청만 허용
// GET, HEAD, PUT, POST, PATCH, DELETE
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
// ✔️HTTP 메서드 매핑 축약
// 편리한 축약 어노테이션
// @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mappingGetV2");
return "ok";
}
// ✔️PathVariable(경로 변수) 사용
@GetMapping(value = "/mapping/{userId}")
public String mappingPath1(@PathVariable(name = "userId") String userId) {
log.info("mappingPath1 userId={}", userId);
return "ok";
}
// ✔️PathVariable(경로 변수) 사용 - 다중
@GetMapping(value = "/mapping/{userId}/order/{orderId}")
public String mappingPath2(@PathVariable(name = "userId") String userId, @PathVariable(name = "orderId") Long orderId) {
log.info("mappingPath2 userId={}, orderId={}", userId, orderId);
return "ok";
}
// ✔️특정 파라미터 조건 매핑
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
// ✔️특정 헤더 조건 매핑
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
// ✔️미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsume() {
log.info("mappingConsume");
return "ok";
}
// ✔️미디어 타입 조건 매핑 - HTTP 요청 Accept, produce
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduce() {
log.info("mappingProduce");
return "ok";
}
}
✔️회원 관리 HTTP API 예시
/users
/users
/users/{userId}
/users/{userId}
/users/{userId}
@RestController
@RequestMapping(value = "/mapping/users") // 계층적 매핑👍
public class MappingClassController {
@GetMapping()
public String users() {
return "get users";
}
@PostMapping()
public String addUser() {
return "add user";
}
@GetMapping("/{userId}")
public String findUser(@PathVariable(name = "userId") String userId) {
return "get userId = " + userId;
}
@PatchMapping("/{userId}")
public String updateUser(@PathVariable(name = "userId") String userId) {
return "update userId = " + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable(name = "userId") String userId) {
return "delete userId = " + userId;
}
}
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(
// ✔️ HttpServletResponse
HttpServletResponse response,
// ✔️ HttpServletRequest
HttpServletRequest request,
// ✔️ HttpMethod : HTTP 메서드를 조회한다.
HttpMethod httpMethod,
// ✔️ Locale : Locale 정보를 조회한다.
Locale locale,
// ✔️ @RequestHeader : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap,
// ✔️ @RequestHeader : 특정 HTTP 헤더를 조회한다.
@RequestHeader(name = "host") String host,
// ✔️ @CookieValue : 특정 쿠키를 조회한다.
@CookieValue(value = "mvCookie", 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";
}
}
Method Arguments
Return Values
@Slf4j
@Controller
public class RequestParamController {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
}
→ Jar
를 사용하면 webapp
경로를 사용할 수 없다. 정적 리소스도 클래스 경로에 함께 포함해야한다.
✔️RequestParamControllerV1
@Slf4j
@Controller
public class RequestParamControllerV1 {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
}
✔️RequestParamControllerV2
@Slf4j
@Controller
public class RequestParamControllerV2 {
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam(name = "username") String username, @RequestParam(name = "age") int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
❗Controller 어노테이션과 String 타입이 같이 사용되면 이는 뷰를 렌더링하라는 의미가 되는데 현재
ok
라는 이름의 정적 리소스가 없기 때문에 문자ok
를 리턴하려면@ResponseBody
를 사용하면 된다.
✔️RequestParamControllerV3
@Slf4j
@Controller
public class RequestParamControllerV3 {
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV2(@RequestParam String username, @RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
✔️RequestParamControllerV4
@Slf4j
@Controller
public class RequestParamControllerV4 {
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV2(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
❗어노테이션을 생략해도 되지만 없는 것보다는
@RequestParam
이 있으면 명확하게 요청 파라미터에서 데이터를 읽는다는 것을 알 수 있다.
✔️RequestParamControllerV5 - 요청 파라미터 필수
@Slf4j
@Controller
public class RequestParamControllerV5 {
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(String username, @RequestParam(required = false) int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
❗String타입의 username은 입력을 필수로 받지 않아도 null값이 들어갈 수 있으나 int는 기본형 타입이기 때문에 null이란 값이 들어갈 수 없다. 따라서 int타입의 age에서 입력을 필수로 받지 않으려면 래퍼 클래스인 Integer를 사용해야한다.
✔️RequestParamControllerV6 - 기본값 지정
@Slf4j
@Controller
public class RequestParamControllerV6 {
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamRequired(@RequestParam(defaultValue = "GUEST") String username, @RequestParam(defaultValue = "-1") Integer age) {
log.info("username={}, age={}", username, age);
return "ok";
}
}
✔️RequestParamControllerV7 - 파라미터를 Map으로 조회하기
@Slf4j
@Controller
public class RequestParamControllerV7 {
@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";
}
}
✔️RequestParamControllerV8 - 파라미터를 MultiValueMap으로 조회하기
@Slf4j
@Controller
public class RequestParamControllerV8 {
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam MultiValueMap<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
}
✔️@Data → @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 지원
@Data
public class HelloData {
private String username;
private int age;
}
✔️ModelAttribute 적용 - modelAttributeV1
@ModelAttribute
가 있으면 다음을 실행한다.HelloData
객체를 생성한다.HelloData
객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter
를 호출해서 파라미터의 값을 바인딩한다.@Slf4j
@Controller
public class ModelAttributeV1 {
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("helloData={}", helloData);
return "ok";
}
}
✔️ModelAttribute 생략 - modelAttributeV2
@Slf4j
@Controller
public class ModelAttributeV2 {
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("helloData={}", helloData);
return "ok";
}
}
✔️RequestBodyStringController
InputStream(Reader)
: HTTP 요청 메시지 바디의 내용을 직접 조회 OutputStream(Writer)
: HTTP 응답 메시지 바디에 직접 결과 출력HttpEntity
: HTTP header, body 정보를 편리하게 조회HttpEntity
를 상속받은 다음 객체들도 같은 기능을 제공한다.ResponseEntity
: HTTP 상태 코드 설정 가능, 응답에서 사용RequestEntity
: HTTP Method, url정보 추가, 요청에서 사용@RequestBody
를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam
이나 @ModelAttribute
와는 전혀 관계가 없다.
@ResponseBody
를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다.
물론 이 경우에도 view를 사용하지 않는다.
@RequestParam
@ModelAttribute
@RequestBody
@Slf4j
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString1(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
@PostMapping("/request-body-string-v2")
public void requestBodyString2(InputStream inputStream, Writer outputStream) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
outputStream.write("ok");
}
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyString3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
@PostMapping("/request-body-string-v4")
public ResponseEntity<String> requestBodyString4(RequestEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new ResponseEntity<String>("ok", HttpStatus.ACCEPTED);
}
@PostMapping("/request-body-string-v5")
public HttpEntity<String> requestBodyString5(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return new ResponseEntity<String>("ok", HttpStatus.ACCEPTED);
}
}
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
// ✔️응답의 경우 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣을 수 있다.
@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";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
// ✔️HttpEntity를 사용해도 된다.
@ResponseBody
@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";
}
// ✔️@RequestBody 요청 → JSON 요청 → HTTP 메시지 컨버터 → 객체
// ✔️@ResponseBody 응답 → 객체 → HTTP 메시지 컨버터 → JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
/static
/public
/resources/
/META-INF/resources/
src/main/resources/template
String
을 반환하는 경우 - View or HTTP 메시지@ResponseBody
가 없으면 뷰 리졸버를 실행하여 뷰를 찾고 뷰를 렌더링한다.@ResponseBody
가 있으면 류 리졸버를 실행하지 않고 HTTP 메시지 바디에 문자가 입력된다.@ResponseBody
, HttpEntity
를 사용하면 뷰 템플릿을 사용하느게 아니라 HTTP 메시지 바디에 직접 응답 데이터를 출력하게끔 한다.@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mv = new ModelAndView("/response/hello").addObject("data", "hello");
return mv;
}
// ✔️반환 타입이 String인 경우
// 뷰 리졸버를 실행해서 뷰를 찾고 뷰를 렌더링한다.
@RequestMapping("/response-view-v2")
public String responseView2(Model model) {
model.addAttribute("data", "hello");
return "response/hello";
}
// ✔️반환 타입이 void인 경우
// 요청 URL과 뷰 템플릿의 경로가 완전히 동일한 경우에만 사용 가능
// 명시성이 너무 떨어지고 딱 맞는 경우도 맞지 않아서 권장하지 않는다.
@RequestMapping("/response/hello")
public void responseView3(Model model) {
model.addAttribute("data", "hello");
}
}
@Slf4j
@Controller
public class ResponseBodyController {
// ✔️서블릿을 직접 다룰 때처럼
// HttpServletReponse 객체를 통해서 HTTP 메시지 바디에 직접 응답 메시지 전달
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("OK");
}
// ✔️HttpEntity 사용
// HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다.
// ResponseEntity는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<String>("ok", HttpStatus.OK);
}
// ✔️@ResponseBody를 사용하면 view를 사용하지 않고 HTTP 메시지 컨버터를 통해서 메시지를 직접 입력할 수 있다.
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
// ✔️ResponseEntity를 반환
// HTTP 메시지 컨버터를 통해서 JSON 형식으로 반환된다.
@ResponseBody
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<HelloData>(helloData, HttpStatus.OK);
}
// ✔️반환 객체 타입으로 지정
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(HelloData helloData) {
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
✔️@RestController
@Controller
대신에 @RestController
어노테이션을 사용하면 해당 컨트롤러에 모두 @ResposeBody
가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라 HTTP 메시지 바디에 직접 데이터를 입력한다. 이름 그대로 REST API를 만들 때 사용하는 컨트롤러이다.@Controller
를 사용할 때 각각의 메서드 레벨에 @ResponseBody
를 두는 것보다 클래스 레벨에 @ResponseBody
를 두면 중복을 제거할 수 있다.ByteArrayHttpMessageConverter
: byte[]
데이터를 처리byte[]
, 미디어 타입 : */*
@RequestBody byte[] data
@ResponseBody return byte[]
application/octet-stream
StringHttpMessageConverter
: String
문자로 데이터를 처리String
, 미디어 타입 : */*
@RequestBody String data
text/plain
MappingJackson2HttpMessageConverter
: application/json
HashMap
, 미디어 타입 : application/json
관련@RequestBody HelloData data
@ResponseBody return hellodata
application/json
@RequestBody
, HttpEntity
파라미터를 사용한다.canRead()
를 호출한다.@RequestBody
의 대상 클래스text/plain
, application/json
@ResponseBody
, HttpEntity
로 값이 반환된다.canWrite()
를 호출한다.byte[]
, String
, HelloData
)text/plain
, application/json
canWrite()
조건을 만족하면 write()
를 호출해서 HTTP 응답 메시지 바디에 데이터 생성✔️RequestMappingHandlerAdapter 동작 방식
@Slf4j
@Controller
public class RequestParamControllerV1 {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
}
컨트롤러를 호출할 때 누군가로부터 HttpServletRequest
를 받아야 하고 HttpServletResponse
를 받아야 하며 또 @RequestParam
로 파라미터 정보를 받아야 하며 @ModelAttribute
로 관련 객체를 만들어 넘겨야 한다.
✔️ArgumentResolver
어노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. HttpServletRequest
, HttpServletResponse
, Model
과 @RequestParam
, @PathVariable
, @ModelAttribute
등의 어노테이션 그리고 @ResponseBody
, HttpEntity
등과 같이 HTTP 메시지를 처리하는 부분까지 처리를 해주는데 이렇게 처리할 수 있는 이유는 바로 ArgumentResolver
덕분이다.
어노테이션 기반 컨트롤러를 처리하는 RequestMapping 핸들러 어댑터
는 이 ArgumentResolver
를 호출해 컨트롤러가 필요로 하는 파라미터 값(객체)을 생성한다. 그리고 이렇게 파라미터 값이 모두 준비되면 컨트롤러를 호출해서 값을 넘겨준다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
ArgumentResolver
의 supportsParameter()
를 호출해서 해당 파라미터를 지원하는지 체크하고 지원한다면 resolveArgument()
를 호출해서 실제 객체를 생성한다. 그리고 이렇게 생성된 객체가 컨트롤러 호출 시 넘어가게 되는 것이다.
✔️ReturnValueHandler
응답 값을 변환하고 처리한다. 컨트롤러에서 String
타입으로 뷰 이름을 반환할 때 동작하는 이유가 바로 이 핸들러 때문이다.