2022. 6. 11. 20:22ㆍLanguage`/Spring
서블릿이나 JSP를 통해서 "비즈니스 로직 & 뷰 렌더링" 전부를 처리하면 너무 많은 역할을 담당하게 되고 결국 유지보수가 굉장히 어려워진다
>> MVC 패턴의 탄생
MVC : Model View Controller
Controller
Client의 HTTP Request를 받아서 1) 파라미터를 검증하고, 2) 비즈니스 로직을 실행
그리고 View에 전달할 결과 데이터들을 Model에 담는 역할까지 수행한다
Model
View에서 사용할 데이터를 담아두는 저장소 개념이다
Model덕분에 View는 {비즈니스 로직 / 데이터 접근}을 전혀 상관하지 않고 오직 뷰 렌더링에만 집중할 수 있다
View
Controller가 도출해낸 데이터들을 담은 Model을 활용해서 뷰 렌더링에 집중한다
"여기서 Spring은 Front Controller : DispatcherServlet 개념까지 도입한다"
※ Front Controller : DispatcherServlet
모든 HTTP Request가 직접 Controller로 가는게 아니라 먼저 DispatcherServlet에 도달하고, 이 DispatcherServlet에서 각각의 Request를 처리할 수 있는 "핸들러(컨트롤러)"를 조회하고 이 핸들러에 맞는 핸들러 어댑터를 찾아서 실행시킴으로써 핸들러가 수행된다
Spring MVC 구조
1. DispatcherServlet이 모든 HTTP Request를 받는다
2. Request URL을 처리할 수 있는 "핸들러(컨트롤러)"를 조회한다
3. 이 핸들러를 실행시킬 수 있는 "핸들러 어댑터"를 조회한다
4. 핸들러 어댑터를 실행함으로써 핸들러를 호출한다
5. 핸들러가 return하는 정보를 핸들러 어댑터는 "ModelAndView"로 변환해서 DispatcherServlet에게 전달한다
6. DispatcherServlet은 ModelAndView에 대한 viewResolver를 호출해서 "논리적 View -> 물리적 View"로 변환한다
7. 최종적인 render
스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록해서 모든 경로 : urlPatterns="/"에 대해서 Mapping을 한다
"/"보다 더 자세한 경로가 우선순위를 가진다
핸들러 매핑 & 핸들러 어댑터
<핸들러 매핑>
0순위 : RequestMappingHandlerMapping = 애노테이션 기반 @RequstMapping
1순위 : BeanNameUrlHandlerMapping = "스프링 빈 이름"으로 핸들러를 찾기
....
<핸들러 어댑터>
0순위 : RequestMappingHandlerAdapter = 애노테이션 기반 @RequestMapping
1순위 : HttpRequestHandlerAdapter = HttpRequestHandler 처리
2순위 : SimpleControllerHandlerAdapter = Controller 인터페이스 처리
@RequestMapping
이 애노테이션은 Request되는 URL에 따라서 매핑을 할 수 있는 애노테이션이다
1. value
value는 매핑될 URL을 지정해주는 속성이다
@RequestMapping(value = "/hello")
@RequestMapping("/hello") // value 생략
위와 같이 내부에 "URL 매핑 정보" 하나만 속성 값으로 지정한다면 value를 생략할 수 있다
@RequestMapping(value = {"/hello", "/spring", "/jpa"})
이렇게 여러 URL을 동시에 매핑할 수도 있다
2. method
method는 Request에 대한 method를 구분할 수 있는 속성이다
@RequestMapping(value = "/hello", method = RequestMethod.GET)
이렇게 설정한다면 "/hello"경로에 대한 GET 요청만 처리할 수 있게 된다
3. params
params은 URL뒤에 이어지는 "쿼리 파라미터"에 대한 조건적 매핑을 할 수 있다
@RequestMapping(value = "/hello", params = "name=faker")
public String hello(){
return "ok";
}
이 경우 쿼리 파라미터로 "name=faker"가 있어야지만 매핑이 정상적으로 된다
결과를 확인해보자
"/hello"경로에 이어서 쿼리 파라미터가 "name=faker"가 없는 경우 400 에러가 발생한다
이렇게 쿼리 파라미터로 "name=faker"가 존재해야 정상적인 요청응답이 이루어진다
>> 그러면 name에 대해서 여러 value가 있다면 어떻게 될까?
"name=faker&name=bang"은 정상 흐름으로 반환되었지만, "name=bang&name=faker"는 400 에러가 발생하였다
왜냐하면 동일한 key에 대해서 여러 value가 있을 경우 Servlet의 request방식은 "가장 첫번째로 매핑되는 value"를 return하게 된다
따라서 "name=faker&name=bang"은 결국 "name=faker"로 들어오기 때문에 정상 흐름으로 반환이 되는 것이고, "name=bang&name=faker"는 결국 "name=bang"으로 들어오기 때문에 400 에러가 발생하는 것이다
4. headers
headers는 params와 비슷하지만, 단지 헤더에 대한 조건부 매핑이 되는 것이다
@RequestMapping(value = "/hello", headers = "Cookie=myCustomCookie")
이 경우 헤더에 "Cookie=myCustomCookie"라는 key-value가 있어야 URL매핑이 정상적으로 이루어진다
5. consumes
consumes은 Request Message의 "Content-Type 헤더 정보"를 추가 매핑 조건으로 설정하는 속성이다
@RequestMapping(value = "/hello", consumes = "application/json")
위의 경우 Content-Type: application/json일 경우에만 매핑이 정상적으로 이루어진다
@RequestMapping(value = "/hello", consumes = "!application/json")
위의 경우 Content-Type: application/json만 아니면 매핑이 정상적으로 이루어진다
>> consumes에서 지정한 Content-Type가 맞지 않는 요청이 들어온다면 "415 에러"가 발생하게 된다
6. produces
produces는 Request Message의 "Accept 헤더 정보"를 추가 매핑 조건으로 설정하는 속성이다
▶ consumes과 produces의 차이
consumes은 "Content-Type"에 대한 매핑 조건이고 "Content-Type"은 Message Body에 들어있는 데이터의 형태를 의미한다
반면에 produces는 "Accept"에 대한 매핑 조건이고 "Accept"는 "Client가 선호하는 데이터 형태"를 의마한다
@xxxMapping
@RequestMapping은 method를 지정하지 않는이상 모든 method에 대한 request를 허용한다
반면 @xxxMapping은 "xxx"에 설정된 method에 대한 request만 허용하는 것이다
@GetMapping("/hello")
@PostMapping("/hello")
@PutMapping("/hello")
@PatchMapping("/hello")
@DeleteMapping("/hello")
이렇게 HTTP Method에 대한 제한적 request 허용을 설정할 수 있다
@xxxMapping은 이미 애노테이션 자체로 method가 정해져있기 때문에 @RequestMapping과 다르게 method 속성이 없는 것을 확인할 수 있다
@PathVariable
경로를 정적으로 다 주는게 아니라, 동적으로 변경할 수 있는데 이를 지원하는 애노테이션이 @PathVariable이다
1. value & name
value나 name은 URL의 "동적 Path Variable"의 key를 가져오는 속성이다
@GetMapping("/hello/{userId}")
public String hello(@PathVariable(name = "userId") String id){
return "userId = " + id;
}
이 경우 URL : /hello/{userId}에서 실제로 /hello/1로 request를 할 경우 "userId = 1"이 된다
여기서 name/value는 userId가 되고 이를 binding한 "String id"에는 "1"이 들어가게 된다
@GetMapping("/hello/{userId}")
public String hello(@PathVariable String userId){
return "userId = " + userId;
}
따라서 동적 Path Variable의 이름과 binding할 변수의 이름이 같다면 name이나 value를 생략할 수 있다
2. required
required속성은 PathVariable에 반드시 값이 들어와야 하는가 아니면 없어도 되는가를 표시하는 boolean 속성이다
default는 "required = true"이며 반드시 PathVariable상에 값이 존재해야 한다
@GetMapping("/hello/{userId}")
public String hello(@PathVariable String userId){
return "userId = " + userId;
}
이 경우 "/hello"로만 request를 보내면 어떻게 될까??
이처럼 404 오류가 발생하게 된다
>> 그러면 이제 required = false로 설정한 후 동일하게 "/hello"로 request를 보내보자
@GetMapping("/hello/{userId}")
public String hello(@PathVariable(required = false) String userId){
if(userId == null){
return "userId = null";
} else{
return "userId = " + userId;
}
}
required = false로 설정하였는데도 불구하고 404 오류가 발생하게 된다
>> "/hello"에 대한 추가적 매핑을 넣어줘야 한다
@GetMapping({"/hello/{userId}", "/hello"})
public String hello(@PathVariable(required = false) String userId){
if(userId == null){
return "userId = null";
} else{
return "userId = " + userId;
}
}
이제 제대로 null처리가 되는 것을 확인할 수 있다
- required = false로 설정하고 경로 변수가 존재하지 않는다면 binding되는 값은 "null"이다
Java8에서부터 지원하는 Optional을 통해서 또 다른 null처리를 해보자
@GetMapping({"/hello/{userId}", "/hello"})
public String hello(@PathVariable(required = false) Optional<String> userId){
if(userId.isEmpty()){
return "userId = null";
} else{
return "userId = " + userId;
}
}
Optional에서는 "isPresent()"를 통해서 value가 들어있는지 확인하고, "isEmpty()"를 통해서 value가 비었는지 확인한다
HTTP Request
1. Header 조회 : @RequestHeader
이 애노테이션을 통해서 HTTP Request Message의 헤더 정보들을 조회할 수 있다
@GetMapping("/hello/Header-Check")
public String header(
@RequestHeader("host") String host,
@RequestHeader("content-type") String contentType,
@RequestHeader("user-agent") String userAgent,
@RequestHeader("accept") String accept,
@RequestHeader MultiValueMap<String, String> headerMap
){
StringBuilder sb = new StringBuilder();
sb.append("Host : ").append(host)
.append("\nContent-Type : ").append(contentType)
.append("\nUser-Agent : ").append(userAgent)
.append("\nAccept : ").append(accept);
sb.append("\n\n").append("## MultiValueMap ##");
for (String s : headerMap.keySet()) {
sb.append("\n").append(s).append(" : ").append(headerMap.get(s));
}
return sb.toString();
}
@RequestHeader안에 직접적인 헤더 이름을 지정하면 해당 속성에 대한 value를 얻을 수 있다
그리고 "MultiValueMap"을 이용하면 하나의 헤더에 대한 "여러 Value"를 Map을 통해서 얻을 수 있다
accept-encoding의 경우 하나의 key : accept-encoding에 대해서 여러 value {gzip, defalte, br}을 얻은 것을 볼 수 있다
2. 쿼리 파라미터 조회 : @RequestParam
데이터를 보내는 방식에는 총 3가지가 있다고 했다 {GET / POST / Message Body에 직접(JSON)}
이 3가지 중에서 {GET & POST}는 쿼리 파라미터 형식으로 전달이 되기 때문에 @ReqeustParam을 이용하면 손쉽게 값을 얻을 수 있다
※ @RequestParam : GET
@GetMapping("/hello/request-param")
public String rp(
@RequestParam String username,
@RequestParam Integer age
){
return "username : " + username + "\nage : " + age;
}
※ @RequestParam(required = false)
그리고 @RequestParam내부에는 required라는 속성이 있고 이를 required = false로 지정하면 해당 파라미터 key가 없어도 url이 정상적으로 매핑된다
@GetMapping("/hello/request-param")
public String rp(
@RequestParam String username,
@RequestParam(required = false) Integer age
){
return "username : " + username + "\nage : " + age;
}
여기서 만약에 @RequestParam(required = false) int age라고 하면 어떻게 될까?
일단 파라미터가 존재하지 않으면 웹상에서는 기본적으로 "null"을 대입한다. 하지만 primitive type의 경우 null을 허용하지 않기 때문에 아예 오류가 발생하는 것을 확인할 수 있다
만약 "username=&age=30"처럼 빈 value를 설정하면 어떻게 될까?
현재 username에는 key만 존재하고 value는 없다. null이 들어갈거라고 예상했지만 "" : 빈문자가 들어간다
따라서 이름만 있고 값이 없는 경우 "빈 문자"로 통과한다
※ @RequestParam(defaultValue = "xxx")
파라미터에 값이 없는 경우 defaultValue를 사용하면 기본값을 넣어줄 수 있다
기본값이 있는 경우 사실상 required는 설정하든 말든 의미가 없는 속성이 되어버린다
@GetMapping("/hello/request-param")
public String rp(
@RequestParam(defaultValue = "Hong Gil Dong") String username,
@RequestParam(required = false) Integer age
){
return "username : " + username + "\nage : " + age;
}
아예 username이라는 key자체가 없음에도 불구하고 defaultValue인 "Hong Gil Dong"이 들어간 것을 확인할 수 있다
3. 쿼리 파리미터 조회 (객체화) : @ModelAttribute
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
보통 쿼리 파라미터의 값들을 객체화할때는 위와 같은 방식으로 코딩을 해왔다. 하지만 이 코드도 구현하기 귀찮아졌다
>> @ModelAttribute의 탄생
@ModelAttribute는 위의 과정을 완전 자동화해준다
일단 먼저 Request Parameter를 Binding받을 객체를 만들어야 한다
@Data
public class HelloData {
private String username;
private Integer age;
}
그리고 이제 @ModelAttribute를 사용해보자
@GetMapping("/hello/request-param-json")
public String rpj(
@ModelAttribute HelloData helloData
){
return "username : " + helloData.getUsername() + "\nage : " + helloData.getAge();
}
@ModelAttribute가 있으면 다음 과정을 수행한다
1. Binding할 객체를 생성
2. 요청 파라미터 key로 객체의 프로퍼티를 찾는다
3. 찾으면 setter로 value를 Binding해준다
>> 파라미터 key가 age면 setAge(~~)를 찾아서 값을 입력한다
@GetMapping("/hello/request-param-json")
public HelloData rpj(
@ModelAttribute HelloData helloData
){
return helloData;
}
객체 자체를 메소드 상에서 return할 수도 있다
4. Message Body 조회(단순 텍스트) : HttpEntity / @RequestBody
Message Body를 조회하는것과 쿼리 파라미터를 조회하는 것은 "완전히 다른 문제"이다
(1) HttpEntity (:: RequestEntity & ResponseEntity)
@PostMapping("/request-body-string")
public String bodyString(
HttpEntity<String> httpEntity
){
return httpEntity.getBody();
}
getBody()를 통해서 굉장히 간단하게 가져올 수 있다
@PostMapping("/hello/request-body-string2")
public ResponseEntity<String> bodyString2(
RequestEntity<String> requestEntity
){
String messageBody = requestEntity.getBody();
return new ResponseEntity<>(messageBody, HttpStatus.OK);
}
(2) @RequestBody
@RequestBody는 HttpEntity보다 훨씬 더 간편하게 body를 조회할 수 있다
하지만, 헤더 정보를 조회하고 싶을때는 HttpEntity를 활용해야 한다
@PostMapping("/hello/request-body-string3")
public String bodyString3(
@RequestBody String messageBody
){
return messageBody;
}
5. Message Body 조회(JSON) : @RequestBody
private final ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/hello/request-body-json1")
public HelloData bodyJson1(
@RequestBody String messageBody
) throws IOException {
return objectMapper.readValue(messageBody, HelloData.class);
}
JSON이 잘 매핑된 것을 볼 수 있다
하지만 이제 ObjectMapper도 쓰기 귀찮아진 개발자들은 아예 @ModelAttribute처럼 @RequestBody도 사용하자고 생각했다
@PostMapping("/hello/request-body-json2")
public HelloData bodyJson2(
@RequestBody HelloData helloData
){
return helloData;
}
{JSON <-> 객체}간에 @RequestBody <객체>를 사용하면 "HTTP 메시지 컨버터"라는 것이 알아서 매핑을 해준다
HTTP Response
HTTP API : Message Body에 직접 데이터 입력
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/hello/response-body-string")
public String responseBodyString(){
return "Hello Spring!!";
}
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/hello/response-body-json")
public HelloData responseBodyJSON(){
HelloData helloData = new HelloData();
helloData.setUsername("faker");
helloData.setAge(25);
return helloData;
}