스프링MVC - 기본기능

23886 단어 SpringSpring

프로젝트 생성

Jar는 항상 내장 서버(톰캣 등)을 사용하고 webapp경로도 사용하지 않는다. 내장 서버 사용에 최적화 되어있는 기능이고. 주로 이 방식 사용

Welcome 페이지 만들기

로깅 간단히 알아보기

sout을 쓰지 않고 로깅을 이용해보자

로깅 라이브러리

스프링 부트 라이브러를 사용하면 스프링 부트 로깅 라이브러리가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본적으로

SLF4J와 Logback을 사용한다.

SLF4J는 많은 로그 라이브러리를 통합해서 인터페이스를 제공하는 라이브러리이고,
그 구현체로 Logback과 같은 로그 라이브러리를 선택한 것이다.

실무에서는 거의 Logback을 쓴다.

로그 선언

로그 호출

@RestController를 사용하면 컨트롤러에서 반환할때 반환값(ok)를 그냥 띄워준다.

@Controller는 반환값이 String이면 뷰 이름으로 인식된다.
@RestControllerHTTP 메시지 바디에 바로 입력한다.

위 로그 선언처럼 로깅을 띄울 수 있는데 로그 레벨을 지정할 수 있다.

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의 Accepttext/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)가 나온다.
그 이유는 intnull이 들어갈 수 없기 때문이다.

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 미디어 타입을 지원하는가.(더 정확히는 @RequestMappingproduces )

예) 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들이 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.

부모에 HandlerMethodArgumentResolverHandlerMethodReturnValueHandler가 있다.

HttpEntityMethodProcessor가 자신이 처리해야 하는 경우가 오면 HttpEntityMethodProcessor가 처리를 한다.

HandlerMethodReturnValueHandler가 자신이 처리해야 하는 경우가 오면 HandlerMethodReturnValueHandler가 처리를 한다.

응답의 경우에는 @ResponseBodyHttpEntity를 처리하는 ReturnValueHandler가 있고, 여기서 메시지 컨버터를 호출해서 응답 결과를 만든다.

HandlerMethodArgumentResolver
HendlerMethodReturnValueHandler
HttpMessageConverter

위 3개는 모두 인터페이스이므로 확장하기 좋다.
근데 확장할 일이 어... 스프링은 기본적으로 편한게 많아서 경우가 별로 없긴 하다.

확장은 WebMvcConfigurer 를 검색해보자.

좋은 웹페이지 즐겨찾기