일단 프로젝트를 생성하자. JSP를 사용하지 않기 때문에 Jar로 생성한다.
Jar를 사용하면 내장 서버를 사용하고 webapp 경로도 사용하지 않는다. 내장 서버에 최적화 되어있으며, 최근에는 주로 사용한다.
반면 War는 내장 서버도 사용가능 하지만 주로 외부 서버에 배포하는 목적으로 사용된다. 톰캣을 별도로 설치하거나 JSP를 사용할 때 사용한다.
우리는 이제까지 System.out.println()
을 사용해 필요한 정보들을 출력했다. 하지만 이제부턴 log를 사용하여 필요한 로그들을 출력해보자.
스프링 부트 라이브러리를 사용하면 sping-boot-starter-logging
이 함께 포함되는데 이것은 기본으로 다음의 로깅 라이브러리를 사용한다.
로그의 선언은 다음과 같이 세가지 방법으로 할 수 있다.
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(XXX.class)
@Slf4j
로그는 log.info("hello")
와 같이 호출할 수 있다.
예제를 확인해보자.
//@Slf4j
//@Controller 뷰 이름 반환
@RestController //문자를 반환하면 HTTP 바디에 문자열 그대로 넣음
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest(){
String name = "Spring";
System.out.println("name = " + name);
log.trace("t race log={}",name);
log.debug("debug log={}",name);
log.info("info log={}", name);
log.warn("warn log={}",name);
log.error("error log={}",name);
return "ok";
}
}
@RestController
@Controller
를 사용하면 반환값이 String일 경우 view이름으로 인식된다. 따라서 뷰를 찾고 뷰가 렌더링된다.@RestController
는 반환 값이 String일 경우 HTTP 메시지 바디에 입력된다.log.info
의 출력결과는 다음과 같다.
2022-07-30 19:14:43.719 INFO 14252 --- [nio-8080-exec-3] hello.springmvc.basic.LogTestController : info log=Spring
[ 시간 ] [로그레벨][process id][쓰레드 명] [클래스 명] [로그 메세지]
위의 예제에선 info 말고도 trace, debug, warn, error 등 다양한데, 로그레벨은 아래와 같다.
TRACE > DEBUG > INFO > WARN > ERROR
예를 들어 로그레벨을 ERROR로 지정하면 ERROR만 출력되고, 로그레벨을 TRACE로 지정하면 모든 레벨이 다 출력된다.
보통 개발 서버는 debug 출력이고, 운영서버는 info 출력으로 운영에 꼭 필요한 것만 출력한다.
로그레벨 설정을 application.properties
에서 할 수 있다.
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
로그 호출 방법엔 두가지가 있는데 올바른 사용법은 파라미터를 사용하는 것이다. 로그 출력 레벨이 info라고 가정하자. 그렇다면 debug는 출력되지 않는다.
log.debug("data="+data)
log.debug("data={}", data)
요청이 왔을 때 어떤 컨트롤러가 와야하는지 매핑 하는 법을 알아보자.
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
Http 메서드를 따로 지정해주지 않았기 때문에 모든 Http 메서드를 허용한다.
참고로 /hello-basic과 /hello-basic/은 다른 URL이지만 스프링은 같은 요청으로 매핑한다.
또한 {"/hello-basic", "hello-go"}를 넣어 다중 설정이 가능하다.
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mapping-get-v1");
return "ok";
}
Http 메서드를 Get으로 지정해준다. 따라서 /mapping-get-v1를 POST로 요청하면 405(Method Not Allowed) 를 반환한다.
@GetMapping ("/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
를 @GetMapping ("/mapping-get-v2")
와 같이 변경할 수 있다.
/**
* PathVariable 사용
* 변수명이 같으면 생략가능
* @PathCariable("userId") String userId -> @PathVariable userID
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
리소스 경로에 식별자를 넣는 스타일을 선호한다. @RequestMapping
은 URL 경로를 템플릿화 ({userId})할 수 있는데, @PathVariable
을 사용하면 매칭 되는 부분을 편리하는 조회할 수 있다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
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";
}
특정 파라미터 정보가 있으면 호출하고 없다면 400 코드를 반환하며 매칭 자체가 안된다.
/**
* 특정 헤더로 추가 매핑
* 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";
}
헤더 조건을 넣지 않으면 404 코드를 반환한다.
/**
* 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";
}
POST이면서 JSON형식으로 요청이 와야 위의 메서드가 호출된다.
consume="application/json"
을 사용할 수도 있지만 위와 같이 MediaType.APPLICATION_JSON_VALUE
를 사용하는 것이 더 좋다.
import org.springframework.http.MediaType;
를 import 해야한다.
만약 형식이 맞지 않으면 415(Unsupported Media Type)을 반환한다.
/**
* 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";
}
produce 는 생산해내는 것이다. 따라서 받아들일 수 있는 것인 Accept와 produce가 맞아야한다.
만약 맞지 않으면 406(Not Acceptable)을 반환한다.
Accept = "application/json"이고 produce="text/plain"일 경우 Accept는 text/plain을 받아들일 수 없으므로 매핑되지 않는다.
그렇다면 Content-Type과 Accept의 차이점은 무엇일까.
Content-Type은 HTTP 메시지(요청과 응답 모두)에 담겨 보내는 데이터의 형식을 알려주는 헤더이다. Content-Type의 내용과 consumes가 일치하지 않으면 서버에서 거절한다.
Accept는 브라우저(클라이언트) 에서 웹서버로 요청시 요청메시지에 담기는 헤더이다.Accept와 produce가 일치하지 않으면, 클라이언트에서 거절한다.
관련 Q&A를 참고하자.
/**
* 회원 목록 조회: GET /users
* 회원 등록: POST /users
* 회원 조회: GET /users/{userId}
* 회원 수정: PATCH /users/{userId}
* 회원 삭제: DELETE /users/{userId}
*/
위와 같은 설계를 바탕으로 코드를 짜보자.
/users
가 반복되므로 @RequestMapping("/users")
를 사용하여 중복을 제거하자.
@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 "update userId=" + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}