스프링 MVC1

Single Ko·2023년 6월 1일
0

Spring 강의 정리

목록 보기
11/31

스프링 MVC를 보기 전에 로깅에 대해 간단하게 알아보고 가자.

로깅 라이브러리

스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리(spring-boot-starter-logging)가 함께 포함됨.

로그 라이브러리는 Logback, Log4J, Log4J2등 수많은 라이브러리가 있는데, 이를 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.
SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 라이브러리를 선택하면 됨.
스프링 부트가 기본적으로 제공하는 Logback을 보통 사용한다.

// 로그 선언의 3가지 방법 
private final Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
@Slf4j

//로그 사용법
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);

로그가 출력된 콘솔을 보자

  • 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지같은 다양한 정보가 나옴. - - 로그의 레벨을 정해서 콘솔에 남길 수 있다. 운영과 개발 환경에서 필요한 로그 레벨을 설정해 남길 수 있다. (기본은 info레벨)

올바른 로그 사용법

log.info("info log=" + name);

  • 이런 식으로 우리가 String을 사용하듯 사용할 수 있다. 하지만 절대 이렇게 사용하면 안된다

log.trace("trace my log = " + name);

  • 만약 trace를 출력하지 않는 레벨인데 이렇게 코드를 적어 놨다면 연산이 일어나게 된다. 그렇다면 쓸모없는 리소스를 사용하게 되는 것이다.

log.trace("trace my log = {}", name);

  • 이런 식의 방식으로 사용하게 되면 자동으로 {}에 파라미터를 순서에 맞춰 대입해주고, 만약 출력하지 않는 로그라면 쓸모없는 연산 리소스를 잡아 먹지 않고 넘어가게 됨.

로그 사용의 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 출력에 상황을 조절할 수 있다.
  • System Out 콘솔에만 출력하는 것이 아니라, 파일이나, 네트워크 등 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능.
  • 성능도 System.out보다 좋다.(내부 버퍼링, 멀티 쓰레드 등등). 실무에서는 꼭 로그를 사용하도록 !

요청 매핑

@RestController - 반환 값으로 뷰를 찾는 것이 아니라 HTTP 메시지 바디에 바로 입력 한다. @Controller@ResponseBody를 조합해 놓은 애노테이션이다. 이렇게 하면 HTTP API를 만들때 편하게 사용 가능.

HTTP 메서드
@RequestMapping에 method 속성으로 HTTP메서드를 따로 지정하지 않으면 HTTP 메스더와 무관하게 호출된다. 모두 허용 (GET,HEAD, POST, PUT, PATCH, DELETE)

만약 HTTP 메서드를 매핑해놨는데, 다른 HTTP 메서드를 요구하면 405 오류를 낸다.

@GetMapping @PostMapping @DeleteMapping @PutMapping @PatchMapping 의 편리한 축약 애노테이션이 있다. 꼭 @RequestMapping을 사용해야 되는 상황이 아니면 이런 애노테이션을 사용하자.

PathVariable

/**
*@PathVariable("userId") String data -> @PathVariable String userId
*
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
	log.info("mappingPath userId={}" , data);
  	return "ok";
}
  • 최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호한다. 이를 위해 @PathVariable을 사용하는 편.
  • @PathVariable의 이름과 파라미터 이름이 같으면 생략할 수 있다. 보통 같게해서 생략하는 편
  • @RequestMapping 은 url경로를 템플릿화 할 수 있는데, @PathVariable을 사용하면 매칭 되는 부분을 편리하게 조회 가능.
  • @PathVariable 자체는 생략 불가능.

다중 @PathVariable도 사용 가능
@GetMapping("/mapping/users/{userId}/orders/{orderId}")

쿼리 파라미터를 조건으로 사용 가능
@GetMapping(value = "/mapping-param", params="mode=debug")
params="mode" //모드가 있으면
params="!mode" //모드가 없으면
params="mode=debug" //모드파라미터가 debug일때
params="mode!=debug" //모드파라미터가 debug가 아닐때
params ={"mode=debug", "data=good"} // mode가 debug거나, data가 good이거나..

이런식으로 param에 조건을 추가해 놓을 수 있다. 다만 요즘에는 잘 사용하지 않는다.

특정 헤더로 추가 메핑
@GetMapping(value="/mapping-headers", headers="mode=debug")
headers="mode" //모드가 있으면
headers="!mode" //모드가 없으면
headers="mode=debug" //모드파라미터가 debug일때
headers="mode!=debug" //모드파라미터가 debug가 아닐때

이런 기능들이 있다.

Content-Type 헤더 기반 추가 매핑 Media Type

@PostMapping(value="mapping-consume", consumes="application/json")
consumes="application/json"
consumes="!application/json"
consumes="application/"
consumes="
\/"
MediaType.APPLICATION_JSON_VALUE
produces="application/json"
produces="!application/json"
produces="application/
"
produces="\/"

실제로는 문자로 적는것보다는 상수로 정해져있는 MediaType.APPLICATION_JSON_VALUE같은 것으로 사용하자.

consume(클라이언트가 보내는 타입) , product(서버 쪽에서 보내온 데이터 타입)

요청 매핑 - API 예시

회원목록조회 : GET '/users'
회원 등록 : POST '/users'
회원 조회 : GET '/users/{userId}'
회원 수정 : PATCH '/users/{userId}'
회원 삭제 : DELETE '/users/{userId}'

//현재 공통 url path인 "/users"는 클래스 단위에 @RequestMapping("/users")를 해놓으면
//밑의 Mapping들에서 생략 가능
@GetMapping("/users")
public String user() {
	return "get users";
}

@PostMapping("/users")
public String addUser() {
	return "post users";
}

@GetMapping("/users/{userId}")
public String findUser(@PathVariable String userId) {
	return "get userId =" +userId;
}

@PatchMapping("/users/{userId}")
public String updateUser(@PathVariable String userId) {
	return "update userId" + userId;
}

@DeleteMapping("/users/{userId}")
public String deleteUser(@PathVariable String userId) {
	return "delete userId = " + userId;
}

HTTP 요청 - 기본, 헤더 조회

애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.

 @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) {
                      
 	return "ok";
 }
  • HttpMethod : HTTP 메서드를 조회한다. org.springframework.http.HttpMethod

  • Locale : Locale 정보를 조회한다.(가장 우선순위가 높은 locale이 나옴)

  • @RequestHeader MultiValueMap<String, String> headerMap : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.

    • Map과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
    • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
      ex) keyA=value1&keyA=value2
  • @RequestHeader("host") String host

  • 특정 HTTP 헤더를 조회한다.

    • 속성
      - 필수 값 여부: required
      - 기본 값 속성: defaultValue
  • @CookieValue(value = "myCookie", required = false) String cookie

    • 특정 쿠키를 조회한다.
    • 속성
      - 필수 값 여부: required
      - 기본 값: defaultValue

이 외에 여러가지 파라미터들을 확인 하고 싶다면 Spring의 공식문서에 Method Arguments 부분에서 확인가능. Return values도 다양하게 넣을 수 있다. 이도 공식문서에서 확인 가능

HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

  • 3가지 방법
    GET - 쿼리 파라미터, POST - HTML Form, HTTP message body

  • GET이든 POST든 방식이 같기 때문에 "요청 조회 방식" 이라 한다.

@RequestParam

@GetMapping
@ResponseBody
public String requestParam(@RequestParam String username) {
	return "ok";
}
  • 파라미터에 오는 변수들을 바로 쿼리 파라미터들과 연관시켜줌.

  • 변수명과 파라미터 네임이 같으면 @RequestParam("name")에서 name쪽 생략 가능

  • @RequestParam 자체도 단순 타입이고(int, String, Integer long 등등), 요청 파라미터와 이름이 같으면 생략 가능하다. 즉 String username만 써줘도 username이라는 쿼리 파라미터가 들어오면 적용된다

  • 필수 여부도 설정 가능. @RequestParam(required =true), 기본값은 true. false면 필수값x

  • required=false를 설정하고 int age의 값을 보내지 않았을 경우? => 오류가 난다. 왜냐면 값을 넣지 않으면 자동으로 null이 들어가는데 int는 null을 사용하지 못하기 때문에. Integer를 사용하자.

  • null과 ""는 다르다. 빈문자는 값이 들어온 것 과 같다.

  • defaultValue = "설정가능" , 값이 없으면 그냥 기본적으로 defaultValue의 값이 쿼리 파라미터에 나오게 하겠다는 뜻.

  • defaultValue의 특이한점은 null뿐만 아니라 빈문자열도 처리해준다는 것이다. 빈문자열을 넣었을 경우에는 값이 없는것으로 생각해서 defaultValue를 출력해준다는 뜻이다.

  • 파라미터를 Map, MultiValueMap으로 조회할 수 있다. Map은 key=value 값, MultiValueMap은 key=value1, value2, ...

@ModelAttribute

실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어야 된다. 이때 스프링은 @ModelAttribute 라는 기능을 제공해 객체와 파라미터의 매핑을 자동으로 쉽게 할 수 있도록 도와준다.

@ResponseBody
@GetMapping
public String modelAttribute(@ModelAttribute User user) {
	log.info("user name = {}", user.userName);
    log.info("user name = {}", user.userId);
	return "ok";
}
  • @ModelAttribute를 사용하면 일일이 String userName, Integer userId를 @RequsetParam으로 안받아도 된다.

  • 요청 파라미터의 이름으로 User객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 Setter를 호출해서 파라미터의 값을 입력(바인딩)한다.

  • 잘못된 타입을 입력하면BindException이 발생한다.

  • @ModelAttribute도 생략 가능하다. 앞에서 단순 타입들은 @RequestParam이 붙는다고 했다. 이런 단순 타입을 제외한 나머지에는 @ModelAttribute가 붙는다.(argument resolver로 지정해둔 타입 외)

  • @ModelAttribute는 Model 파라미터도 자동으로 넘겨준다.
    ex) model.addAttribute("user", user); 안적어줘도 알아서 넘어감.

번외 = PRG
RedirectAttributes 클래스 사용.
redirect시에는 정보가 유지되지 않는다. 따라서 정보를 주기 위해서는 다른 방식을 사용 해야되는데 이때 사용 할 수 있는 클래스가 RedirectAttributes이다.
redirectAttribute.addAttribute("userId", user.getUserId);
이런식으로 넘겨준다면 redirect url에 pathvariable을 사용하고 있다면 그곳의 값으로 넘어가고, 없는 것은 쿼리 파라미터로 자동으로 넘어간다.
redirect 시에 return "redirect:/item/" + user.getUserId; 이런식으로 문자열 연산을 하면 안되고, redirectAttribute를 사용하도록 하자.
return "redirect:/item/{userId}"; 이런식으로 사용하도록 하자.

HTTP 요청 메시지 - 단순 텍스트

HTTP message body에 데이터를 담아 요청, 주로 JSON 사용한다.

요청 파라미터와 다르게 HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.

// HttpEntity<T>를 이용하는 방법
@PostMapping
public HttpEntity<String> requestBodyString(HttpEntity<String> httpEntity) {
	String messageBody = httpEntity.getBody();
   return new HttpEntity<>("ok");
}

//@RequestBody를 사용한 방법

@PostMapping
@ResponseBody
public String requesetBodyString(@RequestBody String messageBody) {
	log.info("messageBody={}", messageBody);
   return "ok";
}
  • Spring MVC는 다음 파라미터를 지원한다.
  1. HttpEntity : HTTP header, body 정보를 편리하게 조회.
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계없음.
  2. HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 변환
    • 헤더 저보 포함 가능

HttpEntity를 상속받은 RequestEntity, ResponseEntity도 있다. HttpEntity<> 대신 사용 가능. ResponseEntity는 ResponseEntity<>("ok", HttpStatus.CREATED); HTTP의 상태도 보낼 수 있다.

  1. @RequestBody : HTTP 메시지 바디 정보를 편리하게조회할 수 있다. 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용하면 된다.

  2. @ResponseBody : 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달 할 수 있다.

HTTP 요청 메시지 - JSON

HTTP API에서 주로 사용하는 JSON을 알아보자

// JSON 형식
{
	"username":"hello"
    "age":20
}
content-type: application/json

@PostMapping
@ResponseBody
public String requestBodyJson(@RequestBody User user) {
	log.info("username = {}, age={}", user.getUsername(), user.getAge());
	return "ok";
}
// 위의 JSON형식에 맞춰서 정보가 가게 된다. 사실 위의 단순 텍스트와 형식상은 크게 다를게 없다.
// JSON 형식을 리턴 타입으로 줄 수도있다.
    
@PostMapping
@ResponseBody
public User requestBodyJson(@RequestBody User user) {
	log.info("username = {}, age={}", user.getUsername(), user.getAge());
	return user;
}
// 리턴 타입으로 위의 JSON과 같은 타입을 준다.
    
    // HttpEntity를 사용해도 된다.

@ResponseBody
@PostMapping
public String requestBodyJson(HttpEntity<User> httpEntity) {
	User user = httpEntity.getBody();
   	log.info("username = {}, age={}", user.getUsername(), user.getAge());
	return "ok"
}

HTTP 응답 - 정적 리소스, 뷰 템플릿

스프링에서 응답 데이터를 만드는 방법은 크게 3가지

  1. 정적 리소스
    • 스프링 부트는 다음과 같은 디렉토리에 있는 정적 리소스를 제공한다.
      /static, /public, /resources, /META-INF/resources
    • src/main/resources는 리소스를 보관하는 곳. 클래스패스의 시작 경로이다.
  2. 뷰 템플릿 사용
    - 뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
    - 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것도 가능하다. 뷰 템플릿이 만들 수있는 것이라면 다 가능
    - 스프링 부트는 기본 뷰 템플릿 경로를 제공한다. src/main/resources/templates
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    	<head>
    		<meta charset="UTF-8">
    		<title>Title</title>
    	</head>
    	<body>
    		<p th:text="${data}">empty</p>
    	</body>
    </html>
    간단하게 만들어본 템플릿. Thymeleaf 사용.
@GetMapping
public String responseView(Model model) {
	model.addAttribute("data", "hello");
    return "hello";  // 실제 경로 template/hello.html
}	

//void일시 path와 일치하는 templates을 찾아서 화면에 보여줌.
//명시성이 너무 떨어지고 , 딱 맞아 떨어지는 상황이 별로 없어 권장하지 않음.
@GetMapping("/hello")
public void responseVieww(Model model) {
	model.addAttribute("data", "hello");
}

Thymeleaf 라이브러리를 추가 해뒀을 시 스프링 부트가 자동으로 ThymeleafViewResolver와 필요한 스프링 빈들을 등록. 그리고 다음과 같은 설정도 사용한다.
application.properties

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

위의 설정은 바꿀수 있다.

  1. HTTP API, 메시지 바디에 직접 입력
    - HTTP API를 제공하는 경우에는 데이터를 전달해야 하므로 HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
    - 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우를 말한다.

    @GetMapping
    public ResponseEntity<String> responseBody() {
    	return new ResponseEntity<>("ok", HttpStatus.OK);
    }
    
    @ResponseBody
    @GetMapping
    public String responseBody() {
    	return "ok";  
    }
    // 위의 두 게는 단순하게 텍스트 보내는 것.
    
    @GetMapping
    public ResponseEntity<User> responseBodyJson() {
    	    User user = new User("userA", 20);
    		return new ResponseEntity<>(user, HttpStatus.OK);
    }
    
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping
    public User responseBody() {
     	User user = new User("userA", 20);
    	return user;  
    }
  • ResponseEntity의 경우 HTTP 상태 메시지를 동적으로 바꿀 수 있다. 하지만 @ResponseStatus를 사용하는 경우에는 그런 방법은 불가능 하다. 따라서 일반적으로는 애노테이션을 쓰다가 동적인 상태변화가 필요하다면 ResponseEntity를 사용하면 된다.

  • 위의 코드들을 살펴보면 계속 @ResponseBody를 적고있다. 이때 클래스에 @RestController를 붙이면 된다. @Controller + @ResponseBody를 바로 클래스단위에 전부 적용 시켜 주는 것이다.

HTTP 메시지 컨버터

뷰 템플릿으로 HTMl을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

@ResponseBody 사용시

  • viewResolver 대신에 HttpConverter가 동작

  • 기본 문자 처리 : StringHttpMessageConverter

  • 기본 객체 처리 : MappingJackson2HttpMessageConverter

  • byte 처리 등등 기타 여려 HttpMessageConverter가 기본적으로 등록되어 있음.

    스프링 MVC의 경우에는 다음과 같은 경우에 HTTP 메시지 컨버터를 적용한다.
    HTTP 응답 : @ReqeustBody , HttpEntity(RequestEntity)
    HTTP 요청 : @ResponseBody, HttpEntity(ResponseEntity)

    HTTP 메시지 컨버터는 요청과 응답에 둘 다 사용된다.

    스프링 부트의 대표적인 기본 메시지 컨버터
    0 = ByteArrayMessageConverter
    1 = StringHttpMessageConverter
    2 = MaapingJackson2HttpMessageConverter

ByteArrayMessageConverter

  • byte[] 데이터를 처리한다.
  • 클래스 타입 byte[], 미디어 타입 /;
  • 요청 ex) @ReqesutBody byte[] data
  • 응답 ex) @ResponseBody return byte[] , 쓰기 미디어 타입 : application/octet-stream

StringHttpMessageConverter

  • String 문자로 데이터 처리.
  • 클래스 타입 String, 미디어 타입 /;
  • 요청 ex) @RequestBody String data
  • 응답 ex) @ResponseBody return "ok" , 쓰기 미디어 타입 : text/plain

MaapingJackson2HttpMessageConverter :

  • application/json
  • 클래스 타입 : 객체 또는 HaspMap, 미디어 타입 : application/json 관련
  • 요청 ex) @RequestBody User user
  • 응답 ex) @ResponseBody return user , 쓰기 미디어 타입 : application/json 관련

HTTP 요청 데이터 읽기 - @ReuqestBody, HttpEntity 파라미터 사용

  • canRead()호출 ->{클래스타입 지원 체크 , Content-Type체크}, 만족시 read()호출해 객체 생성 및 반환

HTTP 응답 데이터 생성 - @ResponseBody, HttpEntity로 값이 반환

  • Http 요청의 Accept 미디어 타입을 지원하는가(@RequestMapping의 produces)
  • canWrite() 조건을 만족하면, write()를 호출 HTTP 응답 메시지 바디에 데이터를 생성

요청 매핑 헨들러 어뎁터 구조

HTTP 메시지 컨버터는 스프링 MVC 어디에서 사용되는 것일까?

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터 사용 가능.(HttpServletRequest, Modele, @RequestParam, @ModelAttribute, @RequestBody, HttpEntity등..)
이렇게 다양하게 처리가 가능한 이유는 바로 Argument Resolver 덕분이다.

Argument Resolver

컨트롤러를 처리하는 RequestMappingHandlerAdaptor가 바로 이 ArgumentResolver를 호출해서 컨트롤러가 필요로하는 다양한 파라미터 값을 생성. 이렇게 준비된 값을 컨트롤러를 호출하면 값을 반환해 넘겨줌

스프링은 30개가 넘는 Argument Resolver를 기본적으로 제공한다
(풀 네임 : HandlerMethodArgumentResolver)

ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.

ReturnValueHandler

HandlerMethodReturnValueHandler를 줄여서 부르는 것. ArgumentResolver와 비슷한데, 이것은 응답 값을 반환하고 처리.

String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 이 ReturnValueHandler 덕분이다.

마찬가지로 ReturnValueHandler가 응답값을 반환하고 처리하기위해 이용하는것이 HTTP 메시지 컨버터 이다.

스프링은 ArgumentResolver와 ReturnValueHandler, HTTPMessageConverter를 인터페이스로 제공한다. 즉 우리가 기능을 확장 할 수있다는 것이다. (다만 필요한 기능은 거의 다 구현되있어서 확장 할 일이 거의 없다고 보면 된다.)
기능 확장은 WebMvcConfigurer을 상속받아 스프링 빈으로 등록하면 된다.

참고 : 본 글은 김영한님의 스프링 강의를 정리한 것이다.

profile
공부 정리 블로그

0개의 댓글