Spring MVC (6) - 스프링 MVC 기본 기능
1. 들어가기 전에
- 프로젝트 생성 주의점
Packaging War가 아닌, Jar를 선택 -> JSP를 사용하지 않기 때문에
(스프링 부트를 사용하면 주로 위 방식을 사용하게 된다)
Jar+내장 톰캣+ webapp경로x
War+ 주로 외부 웹 서버 + webapp경로 o
- Welcome페이지 만들기
스프링 부트에 Jar를 사용하면, /resoureces/static 위치에 파일을 두면, Welcome페이지로 처리해준다.
정적컨텐츠는 주로 저 위치에 두면된다.
1.2 로깅 알아보기
- 운영시스템에서는 System.out.println()같은 시스템 콘솔을 사용해서 필요한 정보 출력x
> 별도의 로깅 라이브러리를 사용해서 로그를 출력한다.
- 로깅 라이브러리 : 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
SLF4J, Logback
- SLF4J: 많은 로그 라이브러리를 통합해서 인터페이스 제공 Logback는 구현체임 (이런 구현체 선택하면됨)
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
//@Slf4j : 롬복 사용 가능
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/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);
//로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X
log.debug("String concat log=" + name);
return "ok";
}
}
- @RestController는 메서드 String 반환 값을 HTTP 메시지 바디에 바로 입력함
*@ResponseBody와 연관이 있다.
- 로그 레벨을 설정하여 출력을 하면
시간,로그레벨,프로세스 ID, 스레드명, 클래스명, 로그 메시지 쭉 나온다
TRACE,DEBUG,INFO,WARN,ERROR순서
개발 서버는 주로 DEBUG사용, 운영서버는 INFO 사용
*로그 레벨 설정
application.properties
전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
- 로그를 찍을 때 +연산말고, {}와 같은 format을 사용하자 -> 의미없는 연산이 발생하지 않음
- 로그를 사용하면, 쓰레드 정보 클래스 이름 같은 부가 정보 함께 볼 수 있고, 출력 모양 조정 가능
로그 레벨에 따라 개발 서버에서는 모든 로그 출력, 운영서버에서는 출력하지 않는 등 상황에 맞게 조절
콘솔 뿐 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다. (파일로 남길 시 일별, 용량별 로그 분할 가능)
2. 요청 매핑
- 서버는 받을 수 있는 요청들을 미리 지정해두고, 클라이언트는 이에 맞춰서 요청을 날리는 것
아무 요청이나 날릴 수 있고, 이를 해석해서 서버가 작동하는게 아님! ->
서버에서 원하는 방향대로만 애초에 요청하도록 만들어야함 ->
서버에서 애초에 요청을 날리는 방식을 정해두고, 클라이언트는 이 중 선택
2.1 기본 매핑
@RequestMapping("/hello-basic") // {"/hello-basic", "/hello-go"} 배열 가능
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
- 기본 요청 매핑임 HTTP 메서드 모두 허용하는 특징이 있음
(GET,HEAD,POST,PUT,PATCH,DELETE)
- 물론 method를 지정할 수 있다.
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
- 특정 HTTP 메서드만 받을 수 있도록 축약형 어노테이션도 제공한다
어노테이션 내부에보면 @RequestMapping에 method를 지정해서 사용한 것을 볼 수 있다.
2.2 PathVariabel(경로 변수) 사용
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
- 최근 HTTP API에서 다음과 같이 리소스 경로에 식별자 넣는 스타일 선호함
/mapping/userA, /users/1 등
- @RequestMapping은 URL경로를 템플릿({} <- 포맷 )화 할 수 있는데 @PathVaridable을 사용하면,
매칭 되는 부분을 편리하게 조회할 수 있다.
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId){
// PathVariable 다중 사용
* 쿼리 스트링 날리는 거랑 동일한 효과 다른 방식임!
2.3 기본 요청 + 추가 매핑 (파라미터, 헤더 등)
- 특정 파라미터 조건 매핑
/**
* 파라미터로 추가 매핑
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
> 특정 파라미터가 있거나 없는 조건을 추가하여 매핑할 수 있음 (잘 사용하지 않음)
- 특정 헤더 조건 매핑
/**
* 특정 헤더로 추가 매핑
* headers="mode",
* headers="!mode"
* headers="mode=debug"
* headers="mode!=debug" (! = )
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
> 파라미터 매핑과 비슷 요청 헤더에 특정 값이 있는지 확인
- 미디어 타입 조건 매핑 (Content-Type 헤더 기반 추가 매핑)
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
* consumes = {"text/plain","application/*"}
consumes = "text/plain"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE
- 미디어 타입 조건 매핑 (HTTP 요청 Accept 기반 추가 매핑)
/**
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"
- Consume : 클라이언트 요청 메시지 타입 중 이것만 받는다 (content-type과 consume 매핑)
- Produce : 서버 응답 결과는 이런 타입이다 (요청 메시지에 클라이언트 accept와 produce 매핑)
2.4 요청 매핑 - API 예시
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/**
* GET /mapping/users
*/
@GetMapping
public String users() {
return "get users";
}
/**
* POST /mapping/users
*/
@PostMapping
public String addUser() {
return "post user";
}
/**
* GET /mapping/users/{userId}
*/
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
/**
* PATCH /mapping/users/{userId}
*/
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
/**
* DELETE /mapping/users/{userId}
*/
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
회원 관리 API
회원 목록 조회: GET /users
회원 등록: POST /users
회원 조회: GET /users/{userId}
회원 수정: PATCH /users/{userId}
회원 삭제: DELETE /users/{userId}
3. HTTP 요청 파라미터 (데이터 조회)
3.1 기본/헤더
> 애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원함
> 헤더정보를 조회하는 방법을 알아보자
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String>
headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", 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";
}
}
- Request,Response
- HttpMethod : HTTP 메서드 조회 , Locale : Locale 정보 조회
- @RequestHeader MultiValueMap<String,String> headerMap : 모든 헤더 조회
- @RequestHeader("host") String host : 특정 헤더 값 조회
required와 defaultValue 설정 가능
- @CookieValue: 특정 쿠기 조회 기능
* 지원가능한 파라미터 적으면, 핸들러 어답터가 처리해줌! (기본은 HTTP 요청메시지)
* MultiValueMap : MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
HTTP header, HTTP 쿼리 파라미터 같이 하나의 키에 여러 값을 받을 때 사용
keyA=value1&keyA=value2
*Controller에 사용 가능한 파라미터 + 응답 가능한 값은 공식 메뉴얼에서 확인 가능
3.2 쿼리 파라미터, HTML Form
GET 쿼리 파라미터 전송 방식/ POST(HTML Form) 방식은 둘다 형식이 같아서 구분 없이 조회 가능
-> 간단히 요청 파라미터 조회라고 함
- request.getParameter()
/**
* 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
*/
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse
response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
//
}
-> 서블릿 방식 request.getParameter()사용해서 조회 가능
- @RequestParam
/**
* @RequestParam 사용
* - 파라미터 이름으로 바인딩
* @ResponseBody 추가
* - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
*/
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
//
}
/**
* @RequestParam 사용
* HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age)
/**
* @RequestParam 사용
* String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age)
> @RequestParam : 파라미터 이름 바인딩 (request.getParameter와 같음)
> @ResponseBody : String return값 message body에 바로 넣기
*@RequestParam을 생략하면, 스프링 MVC 내부에서 required=false를 적용함
기본형일 경우 애노테이션 완전 생략 가능한데 그냥 적어주는게 나음
- 파라미터 필수 여부 및 기본값
/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 예외
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*
* 주의!
* /request-param-required
* int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용)
*/
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age)
/**
* @RequestParam
* - defaultValue 사용
*
* 참고: defaultValue는 빈 문자의 경우에도 적용
* /request-param-default?username=
*/
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age)
> 파라미터 필수 여부 지정 가능 (기본값은 true)
> 필수 파라미터를 지정하지 않으면, 400예외 발생
> 파라미터 이름만 사용 (/param?username=)
- 이럴 경우 빈 문자로 통과함
> 기본형에 null 입력 (@RequestParam(required=false) int age)
- int형에 null 입력 불가능 (500예외)
- Integer로 타입 변경 혹은 defaultValue사용하자
> 기본 값이 있는 경우 사실 required는 딱히 의미가 없긴함!
- 파라미터 Map으로 조회하기 (requestParam Map<>)
/**
* @RequestParam Map, MultiValueMap
* Map(key=value)
* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
*/
@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";
}
> 모든 파라미터 키와 값을 map으로 반환 받는다.
3.3 요청 파라미터 to 객체
- 실제 개발을 하면 요청 파라미터들을 통해 필요한 객체를 만들고 값을 넣어줌
- 이를 쉽게 만들어주는 어노테이션이 있음
@Data
public class HelloData {
private String username;
private int age;
}
> 요청파라미터 값 담을 객체 (@Data하면 롬복이 Getter,Setter,ToString 등등 다해줌)
/**
* @ModelAttribute 사용
* 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때
자세히 설명
*/
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData)
- 스프링 MVC는 @ModelAttribute가 있으면 요청 파라미터 이름과 객체의 프로퍼티 찾음
- 해당 프로퍼티의 setter를 호출해서 파라미터 값을 바인딩한다.
* 프로퍼티
- 객체에 getUsername(),setUsername()메서드가 있으면, 객체는 username이라는 프로퍼티 가짐
* 바인딩 오류
age=abc처럼 숫자 들어가야할 자리에 문자 넣으면 BindException이 발생 (처리 방법은 검증에서 다룸)
* @ModelAttribute 생략 가능
* String, int 같은 단순 타입 = @RequestParam
* argument resolver 로 지정해둔 타입 외 = @ModelAttribute
*/
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData)
- @ModelAttribute 생략 가능 (@RequestParam도 생략 가능)
> 단순타입은 RequestParam적용, 나머지는 ModelAttribute적용
*argument resolver는 뒤에서 학습
*여기까지 get과 html form데이터 조회 방법 알아봄
크게 @RequestParam(데이터 조회)
@ModelAttribute(데이터 to 객체)
필수 값이나 기본 값 설정 등을 다룸
4. HTTP 요청 메시지 - HTTP API
- HTTP message body에 데이터 직접 담아서 요청
> HTTP API에서 주로 사용 JSON,XML,TEXT (주로 JSON)
* 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우
@RequestParam, @ModelAttribute 사용 못함!
4.1 단순 텍스트
- 기존 서블릿 방식
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request,
HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
> request를 통해 InputStream을 열고, StreamUtils를 통해 String으로 만듦
- Input/OutputStream 파라미터
/**
* InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
* OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
*/
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter)
throws IOException {
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
> 파라미터로 inputStream과 Writer 등도 쓸 수 있음! (요청 파라미터 말고, body 읽기 용도)
InputStream(Reader) : HTTP 요청 메시지 바디의 내용 직접 조회
OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력
- HttpEntity 방식
/**
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
> HttpEntity: HTTP header, body내용 편리하게 조회하는 기능함
- 메시지 바디 직접 조회
- 요청 파라미터 조회 기능과는 관계 없다.
- request에서 헤더 조회기능과, InputStream얻어서 바디 조회 기능만 가짐
- 응답(return값)으로 사용할 수도 있다.
- 이때 헤더 정보 포함 가능하고, view조회 안하고 응답 메시지 대신 HttpEntity를 준다.
> RequestEntity: HttpMehod,url정보 추가 (요청에서 사용)
> ResponseEntity : HTTP 상태코드 설정 가능, 응답서 사용
return new ResponseEntity<String>("Hello World", responseHeaders,
HttpStatus.CREATED)
* 스프링 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해 줌
이때 HTTP 메시지 컨버터라는 기능을 사용
- @RequestBody 방식
/**
* @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";
}
- @RequestBody를 사용하면, HTTP 바디 정보 편리하게 조회 가능하다
헤더 정보가 필요하면 HttpEntity를 사용하거나, @RequestHeader를 사용하자
4.2 JSON
- HTTP API에서 주로 사용되는 JSON 형식을 조회해보자
- 기존 방식
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request,
HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream,
StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
}
> 읽는 방식 동일, ObjectMapper로 Body의 내용을 특정 클래스로 변경 (readValue)
- @RequestBody사용
/**
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 모든 메서드에 @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@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";
}
> HTTP 바디 내용을 읽는 부분을 줄일 수 있음!
> 여전히 objectMapper를 사용해서 자바 객체로 변환
> 문자 to json 변환 과정에서 @ModelAttribute와 같이 한번에 객체로 변환 안되나?
- @RequestBody + 객체
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype: application/json)
*
*/
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data)
> @RequestBody생략 없이 String이 아닌, 객체를 넣으면 변환해준다.
> HttpEntity<특정클래스>, @RequestBody를 사용하면, HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을
우리가 원하는 문자나 객체로 변환해 준다.
> @RequestBody는 생략 불가능이다. -> 생략하면 요청 파라미터로 처리함
* HTTP 요청시에 content-type이 application/json인지 꼭 확인해야함 그래야 json으로 처리해주는 메시지 컨버터가 실행된다.
- ResponseBody
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype: application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
(Accept: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
> 응답에도 객체를 집어 넣을 수 있다! (HttpEntity도 사용가능)
> 메시지 컨버터가 객체를 JSON으로 바꿔서 응답해준다.