스프링MVC - 기본기능
프로젝트 생성
Jar는 항상 내장 서버(톰캣 등)을 사용하고 webapp
경로도 사용하지 않는다. 내장 서버 사용에 최적화 되어있는 기능이고. 주로 이 방식 사용
Welcome 페이지 만들기
로깅 간단히 알아보기
sout을 쓰지 않고 로깅을 이용해보자
로깅 라이브러리
스프링 부트 라이브러를 사용하면 스프링 부트 로깅 라이브러리가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본적으로
SLF4J와 Logback을 사용한다.
SLF4J는 많은 로그 라이브러리를 통합해서 인터페이스를 제공하는 라이브러리이고,
그 구현체로 Logback과 같은 로그 라이브러리를 선택한 것이다.
실무에서는 거의 Logback을 쓴다.
로그 선언
로그 호출
@RestController
를 사용하면 컨트롤러에서 반환할때 반환값(ok)를 그냥 띄워준다.
@Controller
는 반환값이 String
이면 뷰 이름으로 인식된다.
@RestController
는 HTTP 메시지 바디에 바로 입력한다.
위 로그 선언처럼 로깅을 띄울 수 있는데 로그 레벨을 지정할 수 있다.
properties에서 관리할 수 있는데
Level - TRACE > DEBUG > INFO > WARN > ERROR
trace - trace, debug, info, warn, error 출력
debug - debug, info, warn, error 출력
info - info, warn, error 출력
등등이 있다.
보통 개발단계에서 trace로 레벨 지정을 한 뒤, 서버를 운영할대 info로 바꾸는 방식을 사용한다고 한다.
sout은 그냥 무조건 출력시키므로 그것보다는 로거를 사용하는 것을 권장한다.
참고로 root는 모든 경로인데 info
가 기본값으로 설정되어 있다. 그래서 properties
에 아무 설정도 하지 않으면 info
레벨로 자동 설정이 된다. (debug같은 걸로 바꾸면 모든 경로를 뒤지기 때문에 엄청 많이 출력된다.)
밑에 springmvc로 깊이 들어가 trace
로 설정하면 다른 경로는 info
로 모두 출력하다가 springmvc 부분은 trace
로 레벨이 맞춰지는 방식이다.
@Slf4j
이거 쓰면 그냥 로그 바로 접근 가능하다. (롬복꺼다)
참고
위의 두 메서드는 큰 차이가 있다. 위는 +
연산이 일어나고 밑은 그냥 2개의 매개변수
를 넣어 실행되는 것이다.
위처럼 쓰면 혼난다. 그 이유는 trace
가 나올지 안나올지 잘 모르는데 trace
안에 +
연산이 일어난 후에 이 메모리를 갖고 있다는 것이 너무 큰 낭비가 될 수 있다는 점이다.
그러므로 log.trace("trace log={}", name);
이렇게 쓰자.
로그 사용시 장점
- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함게 볼 수 있고, 출력 모양 조정 가능
- 로그 레벨에 따라 개발 서버는 모든 로그를 출력하고, 운영서버는 출력하지 않도록 하는 등 조정이 가능하다.
- 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
- 성능도 sout보다 좋다. 그러므로 실무에서는 로그를 꼭 사용해야 한다.
요청 매핑
@RequestMapping
배열을 제공하므로 {"/hello-basic", "hello-go"}
이런 식으로 작성하면 두개의 URL을 설정할 수도 있다.
또, "/hello-basic"
을 설정했을때
"/hello-basic"
, "/hello-basic/"
둘 다 요청을 허용해준다.
RequestMapping에서 method 설정을 안하면 get, post, put, 등등 모든 종류의 호출이 된다.
@PathVariable
PathVariable
은 말 그대로 경로 뒤에 붙은 {userId}를 data로 갖고 와주는 역할을 한다.
최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
/mapping/userA
/userA/1
@RequestMapping
은 URL 경로를 템플릿화할 수 있는데 이를 @PathVariable
이 매칭을 ㅍ편리하게 조회해준다.
참고로 변수명이 파라미터 정보랑 같으면 생략도 가능하다.
다중으로 사용할 수도 있다.
위는 조건식을 추가한 것이다. /mapping-param?mode=debug
이런식으로 작성해야 Get으로 매핑이 된다.
이건 헤더에 mode=debug
설정이 되어있어야 매핑이 되는 것이다.
이건 컨텐트 타입이 application/json
인 경우에만 호출한다는 뜻이다.
이건 Http의 Accept
가 text/html
이거나 그 상위 (*/*
같은거) 인 경우에만 호출받는다는 뜻이다.
다른게 들어오면 미디어 타입이 맞지 않으므로 406 에러가 나온다.
지금보니 HTTP 강의가 도움이 되는 것 같기도 하고
위의 로거와 PathVariable을 이용하면 간단하게 html 파일에서 post로 보낼때 잘 들어오는지 미리 테스트하는 방법도 가능할 것 같다.(당근)
요청 매핑 API
요즘에는 이런 방식으로 많이 사용한다고 한다.
딱 사람이 봐도 깔끔하고 좋은 코드로 보인다. 개멋져
당근할때 참고해도 좋을 것 같다.
HTTP 요청 - 기본, 헤더 조회
컨트롤러는 매개변수를 유연하게 받을 수 있기 때문에 원하는 헤더 정보를 편하게 가져올 수 있다.
MultiValueMap
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");
//[value1,value2]
List<String> values = map.get("keyA");
MultiValueMap은 같은 쿼리 파라미터에 여러 값이 들어올때 사용한다.
- keyA=value&keyA=value2
@Controller가 받을 수 있는 매개변수와 반환할 수 있는 타입
HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form
위와 같이 코드를 짰을때 request는
Get에서 넣은 쿼리 파라미터, /request-param-v1?username=asdf&age=123
Post에서 넣은 파라미터,
둘다 .getParameter()메서드로 값을 받을 수 있다.
HTTP 요청 파라미터 - @RequestParam
인간의 욕심은 끝이 없다.
밑으로 내려갈수록 점점 간편해진다.
맨 밑의 v4버전은 @RequestParam
조차 생략이 가능하다.
하지만, 강사님은 이것까지 빼는건 좀 그렇다면서 웬만하면 V3처럼 @RequestParam
을 넣는 것이 좋다고 하셨다.
required 옵션
만약
required=true이다. (true가 기본값이다.)
파라미터가 무조건 값이 있어야 한다.
required=false이다.
파라미터가 없으면 null을 넣어준다.
또, 위의 그림에서 int age부분의 param 옵션에 required=false
를 넣고 파라미터에 값을 넣지 않을 경우에 서버 에러(5xx)가 나온다.
그 이유는 int
는 null
이 들어갈 수 없기 때문이다.
null
을 넣어주려면 객체인 Integer
를 사용해줘야 한다.
이 옵션은 다른 클라이언트와 주고받는 데이터에는 true
옵션을 쓸 것 같고,
회원가입이나 다른 소비자들이 사용하는 문구에서는 빈칸도 들어올 수 있도록 false
로 쓸것 같은 느낌...?
defaultValue
defaultValue
는 말 그대로 기본값
이다. 즉, 파라미터 값이 안들어오면 defaultValue
로 설정해준다.
그렇기 때문에 defaultValue
를 설정해주면 파라미터값이 들어오지 않아도 설정해주기 때문에
required
옵션을 넣으나 안넣으나 값이 생성되기 때문에 required
옵션을 신경쓰지 않아도 된다.
또, 특징이 하나 있는데 파라미터에 ?username=
와 같이 빈 문자를 넣어도 defaultValue
로 처리해준다는 신기한 점이 있다.(null
이 아닌 ""
)
참고로, required는 빈문자가 들어오면 파라미터 값이 들어온 걸로 생각한다.
Map
모든 파라미터 값을 받고 싶을때 Map
을 이용해서 가져올 수 있다.
참고로 @RequestParam MultiValueMap
을 이용할 수도 있다.
파라미터의 값이 1개가 확실하면 Map
, 그렇지 않으면 MultiValueMap
을 사용하면 된다.
근데 파라미터 값이 2개인 경우는 사실 별로 없다.
HTTP 요청 파라미터 - @ModelAttribute
보통 요청 파라미터를 받으면 객체로 만들어서 값을 넣어주는 것이 보통이다. 아래처럼
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
근데 이걸 @ModelAttribute
를 쓰면 자동으로 해준단다. 와 레전드 난 일일이 다했는데
HelloData
파라미터를 넣을 데이터를 만들었다.
롬복에 있는 @Data
를 사용하면 @Getter @Setter
를 전부 자동으로 해준다. 이런게 있었어?
원래 로직
원래라면 위처럼 파라미터를 직접 받아 HelloData
객체를 생성 후에 set으로 세팅해준다.
@ModelAttribute 사용
위의 복잡한 로직이 저렇게 끝난다.
스프링MVC는 @ModelAttribute
가 있으면 다음을 실행한다.
HelloData
객체를 생성한다.- 요청 파라미터의 이름으로
HelloData
객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다. - 예) 파라미터 이름이
username
이면setUsername()
메서드를 찾아서 호출하면서 값을 입력한다.
프로퍼티가 머냐
간단하게 객체에 getXXX
또는 setXXX
가 있으면 이 객체는 XXX
라는 프로퍼티를 가지고 있는다.
그 후에 XXX
프로퍼티의 값을 변경하면 setXXX
가 호출되고, 조회하면 getXXX
가 호출된다.
바인딩 오류
age=abc
처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException
이 발생한다. 이런 바인딩 오류를 처리하는 방법은 검증 부분에서 다룬다.
더 충격적인 사실
@ModelAttribute
생략도 된다.
@RequestParam
도 생략이 가능한데 스프링은 어떻게 이 둘을 구분하는 걸까?
String
,int
,Integer
같은 단순 타입 ->@ReqestParam
- 나머지 ->
@ModelAttribute
(argument resolver로 지정해둔 타입 제외)
HTTP 요청 메시지 - 단순 텍스트
서블릿의 내용
HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 주로 JSON 사용
- POST, PUT, PATCH
인간의 욕심은 끝이 없다2
아래로 갈수록 편리한 기능이 나온다. 그중에서 HttpEntity
를 살펴보자
HttpEntity
HTTP header, body 정보를 편리하게 조회할 수 있을 뿐만 아니라, 응답 기능도 갖고 있다.
그리고 요청 파라미터를 조회하는 기능과 관련이 없다.
즉, Get
메서드의 쿼리 파라미터, Post
방식의 폼 데이터 전송
두 개의 경우를 제외하면 HttpEntity
같은 것으로 데이터를 직접 꺼내야 한다.
참고로 HttpEntity
를 상속하는 RequestEntity
, ResponseEntity
를 사용해서 위와 같이 나타낼 수도 있다.
실무에서는 이렇게 쓴다.
@ResponseBody
는 많이 사용해봤으므로 알 것이다. 그렇게 @RequestBody
에노테이션도 사용할 수 있다.
메시지 바디를 직접 조회하는 이 기능은 파라미터를 조회하는 @RequestParam
, @ModelAttribute
와는 전혀 관계가 없다. 전혀 다른 로직으로 동작한다.
요청 파라미터 vs HTTP 메시지 바디
- 요청 파라미터 조회 :
@RequestParam
,@ModelAttribute
- HTTP 메시지 바디를 직접 조회 :
@RequestBody
HTTP 요청 메시지 - JSON
인간의 욕심은 끝이 없다3
역시 밑으로 갈수록 편하게 사용가능한 기능들이 등장한다.
@RequestBody
에 직접 만든 객체를 지정할 수 있다.
단, @RequestBody
는 생략하면 안된다.
생략할 시에
String
,int
,Integer
와 같은 단순 타입은@RequestParam
- 그 외는
@ModelAttribute
이므로
생략할 시에 @ModelAttribute
로 인식되기 때문에 어떠한 값도 들어가지 않는다.
HttpEntity를 사용할 수도 있다.
Json 객체를 받아 Json 객체로 보낼 수도 있다.
HTTP 응답 - 정적 리소스, 뷰 템플릿
스프링에서 응답 데이터를 만드는 방법은 크게 3가지 이다.
- 정적 리소스
- 뷰 템플릿
- HTTP 메시지 사용
정적 리소스
/static
,/public
,/resources
,/META-INF/resources
/
src/main/resources
는 리소스를 보관하는 곳이고 또, 클래스패스의 시작 경로이다.
따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
/static/basic/
에 hello-form.html
이 있다면
http://localhost:8080/basic/hello-form.html
뷰 템플릿
뷰 템플릿 경로
src/main/resources/templates
위와 같이 만들었을때 컨트롤러에 뷰 템플릿으로 부를 수 있는 3가지 방법이 제시되어 있다.
첫 번째 방법은 ModelAndView
를 사용하는 방법이다.
두 번째 방법은 Model
을 사용하는 방법이다.
세 번째 방법은 Model
을 사용하지만 반환을 void로 하는 방법이다.
세 번째 방법은 void로 할 경우 스프링이 @RequestMapping
으로 받았던 URL로 다시 보내주는 관례를 이용한 방법인데, 명시적이지 않고 이와 같은 상황이 많지 않아 추천하지 않는 방법이다.
첫 번째와 두 번째 방법을 추천한다.
Thymeleft 스프링 부트 설정
application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
자동으로 위와 같이 설정이 되고 properties
에서 위 값을 바꾸면 바꾸는 값대로 설정이 된다.
근데 굳이 바꿔야될까?
HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
V1
response
를 이용해 body를 입력하는 방법
V2
ResponseEntity
에 메시지마디와 상태코드를 넣는 방법
V3
@ResponseBody
를 사용하는 방법
JsonV1
@ResponseEntity
를 사용해서 Json
구조로 메시지 바디를 보내는 방법
JsonV2
@ResponseBody
를 사용하여 Json
구조로 메시지 바디를 보내는 방법(상태코드는 @ResponseStatus
사용)
Json1
의 경우는 상태코드를 if
문을 사용해 원하는 상태코드를 반환하고 싶을때 사용하고,
그 외는 Json2
를 사용하면 된다.
그런데 위와 같이 일일이 @ResponseBody
를 붙이는 것이 귀찮을때는 클래스 자체에 @ResponseBody
를 붙이면 된다.
그리고 @Controller
와 @ResponseBody
를 합친 것이
@RestController
이다.
HTTP 메시지 컨버터
@ResponseBody
를 사용
- HTTP의 BODY에 문자 내용을 직접 반환
viewResolver
대신에HttpMessageConverter
가 동작- 기본 문자처리:
StringHttpMessageConverter
- 기본 객체처리:
MappingJackson2HttpMessageConverter
요청 : @RequestBody
, HttpEntity(RequestEntity)
응답 : @ResponseBody
, HttpEntity(ResponseEntity)
위의 경우에 메시지 컨버터를 사용한다.
canRead()
, canWrite()
로 읽을 수 있는지 쓸 수 있는지 체크 후
읽을 수 있으면 read()
쓸 수 있으면 write()
로 넘어간다.
스프링 부트 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
ByteArrayHttpMessageConverter : byte[]
데이터를 처리한다.
- 클래스 타입:
byte[]
, 미디어타입:*/*
- 요청 예)
@RequestBody byte[] data
- 응답 예)
@ResponseBody return byte[]
쓰기 미디어타입 application/octet-stream
StringHttpMessageConverter : String
문자로 데이터를 처리한다.
- 클래스 타입:
String
, 미디어타입:*/*
- 요청 예)
@RequestBody String data
- 응답 예)
@ResponseBody return "ok"
쓰기 미디어타입 text/plain
MappingJackson2HttpMessageConverter : application/json
- 클래스 타입:
객체
또는HashMap
, 미디어타입application/json
관련 - 요청 예)
@RequestBody HelloData data
- 응답 예)
@ResponseBody return helloData
쓰기 미디어타입application/json
관련
HTTP 요청 데이터 읽기
@RequestBody
, HttpEntity
파라미터를 사용한다면,
canRead()
를 호출한다.
대상 클래스 타입을 지원하는가.
예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
HTTP 요청의 Content-Type 미디어 타입을 지원하는가.
예) text/plain , application/json , */*
canRead()
조건을 만족하면 read()
를 호출해서 객체 생성하고, 반환한다
HTTP 응답 데이터 생성
@ResponseBody
, HttpEntity
canWrite()
를 호출한다.
대상 클래스 타입을 지원하는가.
예) return의 대상 클래스 ( byte[] , String , HelloData )
HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping
의 produces
)
예) text/plain , application/json , */*
canWrite()
조건을 만족하면 write()
를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.
content-type: application/json
@ReqeustMapping
void hello(@RequestBody String data) {}
위의 경우
처음에 0번의 canRead()
를 통해 객체 클래스와 미디어 타입을 체크한다.
객체가 byte[]
를 지원하는데 String
이므로 패스된다.
그 다음 1번의 canRead()
로 간다. 1번은 String
을 지원하고 미디어 타입이 */*
이므로 이 메시지 컨버터의 read()
가 실행된다.
content-type: text/html
@RequestMapping
void hello(@RequetsBody HelloData data) {}
이 경우는 탈락된다.
요청 매핑 핸들러 어뎁터 구조
여기서 컨버터가 어디서 사용되는거지?
여기서 관련이 있다.
ArgumentResolver
컨트롤러에서 매개변수를 엄청 유연하고 자유롭게 가져다가 쓸 수 있는데 이는 ArgumentResolver 덕분이라고 할 수 있다.
애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor
는 바로 이
ArgumentResolver
를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.
그리고 이렇게 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다
스프링은 30개가 넘는 ArgumentResolver 를 제공한다.
supportsParameter
를 통해 사용할 수 있는지 물어보고 사용할 수 있으면
resolveArgument
를 통해 오브젝트가 생성된다.
어.. 그리고 우리가 원한다면 이 인터페이스를 확장해서 원하는 ArgumentResolver
를 만들 수 있다. 헉
ReturnValueHandler
이건 컨트롤러에서 반환값이 반환할 수 있는지 확인하고 할 수 있으면 반환해주는 핸들러이다.
동작이 비슷하다.
그래서 메시지 컨버터는 어디에
쉽게 말하자면, @RequestBody
를 처리하는 ArgumentResolver
가 있고, HttpEntity
를 처리하는 ArgumentResolver
가 있는데 이 ArgumentResolver
들이 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
부모에 HandlerMethodArgumentResolver
와 HandlerMethodReturnValueHandler
가 있다.
HttpEntityMethodProcessor
가 자신이 처리해야 하는 경우가 오면 HttpEntityMethodProcessor
가 처리를 한다.
HandlerMethodReturnValueHandler
가 자신이 처리해야 하는 경우가 오면 HandlerMethodReturnValueHandler
가 처리를 한다.
응답의 경우에는 @ResponseBody
와 HttpEntity
를 처리하는 ReturnValueHandler
가 있고, 여기서 메시지 컨버터를 호출해서 응답 결과를 만든다.
HandlerMethodArgumentResolver
HendlerMethodReturnValueHandler
HttpMessageConverter
위 3개는 모두 인터페이스이므로 확장하기 좋다.
근데 확장할 일이 어... 스프링은 기본적으로 편한게 많아서 경우가 별로 없긴 하다.
확장은 WebMvcConfigurer
를 검색해보자.
Author And Source
이 문제에 관하여(스프링MVC - 기본기능), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@seungju0000/스프링MVC-기본기능저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)