📌 왜 꼭 Jar를 쓰는지 ?
JSP를 사용하지 않기 때문에 Jar를 사용하는 것이 좋다.
Jar를 사용하면 항상 내장 서버 톰캣등을 사용하고 webapp 경로도 사용하지 않는다. 내장 서버 사용에 최적화 된 기능이기 때문이다.War를 사용해도 내장서버 사용이 가능하지만 이는 주로 외부 서버에 배포하는 목적으로 사용한다.
생성한 프로젝트의 압축을 풀어서 열고 아래의 설정을 해준다.
그리고 프로젝트를 생성했으면 한 번 메인 메소드가 있는 파일을 돌려보자.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>로그 출력
<ul>
<li><a href="/log-test">로그 테스트</a></li>
</ul>
</li>
<!-- -->
<li>요청 매핑
<ul>
<li><a href="/hello-basic">hello-basic</a></li>
<li><a href="/mapping-get-v1">HTTP 메서드 매핑</a></li>
<li><a href="/mapping-get-v2">HTTP 메서드 매핑 축약</a></li>
<li><a href="/mapping/userA">경로 변수</a></li>
<li><a href="/mapping/users/userA/orders/100">경로 변수 다중</a></li>
<li><a href="/mapping-param?mode=debug">특정 파라미터 조건 매핑</a></li>
<li><a href="/mapping-header">특정 헤더 조건 매핑(POST MAN 필요)</a></li>
<li><a href="/mapping-consume">미디어 타입 조건 매핑 Content-Type(POST MAN 필요)</a></li>
<li><a href="/mapping-produce">미디어 타입 조건 매핑 Accept(POST MAN 필요)</a></li>
</ul>
</li>
<li>요청 매핑 - API 예시
<ul>
<li>POST MAN 필요</li>
</ul>
</li>
<li>HTTP 요청 기본
<ul>
<li><a href="/headers">기본, 헤더 조회</a></li>
</ul>
</li>
<li>HTTP 요청 파라미터
<ul>
<li><a href="/request-param-v1?username=hello&age=20">요청 파라미터 v1</a></li>
<li><a href="/request-param-v2?username=hello&age=20">요청 파라미터 v2</a></li>
<li><a href="/request-param-v3?username=hello&age=20">요청 파라미터 v3</a></li>
<li><a href="/request-param-v4?username=hello&age=20">요청 파라미터 v4</a></li>
<li><a href="/request-param-required?username=hello&age=20">요청 파라미터 필수</a></li>
<li><a href="/request-param-default?username=hello&age=20">요청 파라미터 기본 값</a></li>
<li><a href="/request-param-map?username=hello&age=20">요청 파라미터 MAP</a></li>
<li><a href="/model-attribute-v1?username=hello&age=20">요청 파라미터 @ModelAttribute v1</a></li>
<li><a href="/model-attribute-v2?username=hello&age=20">요청 파라미터 @ModelAttribute v2</a></li>
</ul>
</li>
<li>HTTP 요청 메시지
<ul>
<li>POST MAN</li>
</ul>
</li>
<li>HTTP 응답 - 정적 리소스, 뷰 템플릿
<ul>
<li><a href="/basic/hello-form.html">정적 리소스</a></li>
<li><a href="/response-view-v1">뷰 템플릿 v1</a></li>
<li><a href="/response-view-v2">뷰 템플릿 v2</a></li>
</ul>
</li>
<li>HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
<ul>
<li><a href="/response-body-string-v1">HTTP API String v1</a></li>
<li><a href="/response-body-string-v2">HTTP API String v2</a></li>
<li><a href="/response-body-string-v3">HTTP API String v3</a></li>
<li><a href="/response-body-json-v1">HTTP API Json v1</a></li>
<li><a href="/response-body-json-v2">HTTP API Json v2</a></li>
</ul>
</li>
</ul>
</body>
</html>
이는 앞으로 배울 내용에 대해 좀 더 편리하게 참고하기 위함
스프링 부트의 Jar를 이용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다. (스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html 이 있으면 됨.)
별도의 로깅 라이브러리를 사용해 로그를 출력해보자.
스프링부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리 spring-boot-starter-logging 가 함께 포함된다.
로그 라이브러리는 여러 가지가 있는데 이것들을 통합해 인터페이스로 제공하는 것이 SLF4J 라이브러리이다. 그리고 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다. (대부분 Logback 사용)
로그 선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
@Slf4j : 롬복 사용 가능
로그 호출
log.info("hello")
package hello.springmvc.basic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@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
를 사용하는 이유
: 그냥@Controller
를 사용하지 않는 이유는 이것은 뷰를 찾고 뷰가 랜더링되는 반면,
@RestController
는 HTTP 메시지 바디에 바로 입력되어 실행 결과로 메시지를 바로 받을 수 있다.
로그 레벨 설정
LEVEL: TRACE
> DEBUG
> INFO
> WARN
> ERROR
(개발 서버는 debug 출력, 운영 서버는 info 출력)
📌 올바른 로그 사용법
log.debug("data="+data) ❌
: 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제로 실행 되어버린다.
즉, 쓰지도 않는데 쓸모없는 연산이 발생해버린다.log.debug("data={}", data) ⭕️
: 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않아 의미 없는 연산이 발생하지 않는다.
: "요청이 왔을 때 어떤 컨트롤러가 매핑이 되어야 하지?"
package hello.springmvc.basic.requestmapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
private String helloBasic() {
log.info("helloBasic");
return "ok";
}
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
*
* @PathVariable("userId") String userId -> @PathVariable userId
* /mapping/userA
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
/**
* 파라미터로 추가 매핑
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
/**
* 특정 헤더로 추가 매핑
* 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 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
: URL 호출이 오면 이 메소드를 실행하도록 매핑.
대부분의 속성을 배열[]로 제공하므로 다중 설정가능.
: @RequestMapping
에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.
모두 허용 → GET, HEAD, POST, PUT, PATCH, DELETE
: HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다. 코드를 보면 내부에서 @RequestMapping 과 method 를 지정해서 사용하는 것을 확인할 수 있다.
: 최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
@RequestMapping
은 URL 경로를 템플릿화 할 수 있는데, @PathVariable
을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
@PathVariable
의 이름과 파라미터 이름이 같으면 생략할 수 있다. (단, 이 자체를 생략하는 건 불가능)
📌 회원 관리 API
회원 목록 조회: GET
/user
회원 등록: POST/users
회원 조회: GET/users/{userId}
회원수정: PATCH/users/{userId}
회원 삭제: DELETE/users/{userId}
➡️ URL은 똑같지만 HTTP 메소드로 동작을 구분할 수 있음.
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mapping/usrs")
public class MappingClassController {
@GetMapping
public String user() {
return "Get users";
}
@PostMapping
public String addUser() {
return "post user";
}
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId = " + userId;
}
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "patch userId = " + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId = " + userId;
}
}
(/mapping: 강의의 다른 예제들과의 구분을 두기 위함)
애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원
package hello.springmvc.basic.request;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
@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";
}
}
HttpServletRequest
,HttpServletResponse
HttpMethod
: HTTP 메서드를 조회
Locale
: Locale 정보를 조회
@RequestHeader MultiValueMap<String, String> headerMa
: 모든 HTTP 헤더를 MultiValueMap 형식으로 조회
@RequestHeader("host") String host
: 특정 HTTP 헤더를 조회
- 속성
필수 값 여부: required, 기본 값 속성: defaultValue
@CookieValue(value = "myCookie", required = false) String
: 특정 쿠키를 조회
- 속성
필수값 여부: required, 기본 값 속성: defaultValue
: Map과 유사하지만 하나의 키에 여러 값을 받을 수 있다.
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러개의 값의 받을 때 사용
(keyA=value1&keyA=value2)
: 코드를 자동으로 생성해 로그를 선언해줘, 개발자는 편리하게 log라고 사용하면 된다.
클라이언트에서 서버로 요청 데이터를 전달할 때 주로 사용하는 세 가지 방법
- GET - 쿼리 파라미터
: 메시지 바디 없이 URL의 쿼리 파라미터에 데이터를 포함해 전달- POST - HTML Form
: 메시지 바디에 쿼리 파라미터 형식으로 전달- HTTP message body에 직접 데이터를 담아 요청
: HTTP API에 주로 사용 (데이터 형식은 대부분 JSON)
package hello.springmvc.basic.request;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Controller
public class RequestParamController {
@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"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/request-param-v1" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
package hello.springmvc.basic.request;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Slf4j
@Controller
public class RequestParamController {
@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"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
// 위에서 그냥 controller 어노테이션을 썼기 때문에 뷰이름으로 찾게 됨.
// 우리가 원하는건 http 응답 메시지로 바로 받길 원하므로 붙여주는 애노테이션
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) { // int=null은 안되지만 Integer에는 null이 들어갈 수 있음
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
@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";
}
}
✔️ requestParamV2
@RequestParam
: 파라미터 이름으로 바인딩
@ResponseBody
: View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력@RequestParam의 name(value) 속성이 파라미터 이름으로 사용
@RequestParam("username") String memberName ➡️ request.getParameter("username")
✔️ requestParamV3
HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
✔️ requestParamV4
String, int, Integer 등 단순 타입이면 @RequestParam 생략 가능@RequestParam을 생략하면 스프링 MVC는 내부에서 required=false를 적용.
🙋🏻♀️ 그런데 강의 선생님은 너무 애노테이션을 이렇게 막 생략하는 건 좀 과하다고,,^^
@RequestParam이 있으면 명확히 요청 파라미터에서 데이터를 읽는다는걸 알 수 있으니까 !
✔️ 파라미터 필수 여부 - requestParamRequired
@RequestParam.required
: 파라미터 필수 여부. 기본값이 true.‼️ 파라미터 이름만 사용
파라미터 이름만 있고 값이 없는 경우 빈 문자로 통과됨
(null과 ""는 다름! ""는 통과 가능)‼️ 기본형 (primitive)에 null 입력
int에 null을 입력할 수 없음 (500예외)
Integer에는 null이 들어갈 수 있으므로 Integer 사용
✔️ 기본 값 적용 - requestParamDefault
파라미터에 값이 없는 겨우 defaultValue를 사용하면 기본 값을 적용할 수 있음.
이미 기본 값이 있기 때문에 required는 필요 없음.
✔️ 파라미터를 Map으로 조회하기 - requestParamMap
파라미터를 Map, MultiValueMap(하나의 키에 여러가지 값)으로 조회 가능
너무 기계적으로 공부하고 있어서 헷갈리는 부분부터 다시 강의 듣고 이어서 _2부터 쓸게요,,😸