'스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 수업을 듣고 정리한 내용입니다.
스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트 생성
https://start.spring.io
프로젝트 선택
- Project: Gradle Project
- Language: Java
- Spring Boot: 2.4.x
Project Metadata
- Group: hello
- Artifact: springmvc
- Name: springmvc
- Package name: hello.springmvc
- Packaging: Jar (주의!)
- Java: 11
의존성 관리
Spring Web
Thymeleaf
Lombok
주의!
Packing
는War
가 아니라Jar
를 선택해주어야 한다.
JSP
를 사용하지 않기 때문에Jar
를 사용하는 것이 좋다.
앞으로 스프링 부트를 사용하면 이 방식을 주로 사용하게 된다.
Jar
를 사용하면 항상 내장 서버(톰캣등)을 사용하고,webapp
경로도 사용하지 않는다.
내장 서버 사용에 최적화 되어 있는 기능이다.
최근에는 주로 이 방식을 사용한다.
War
를 사용하면 내장 서버도 사용가능 하지만, 주로 외부 서버에 배포하는 목적으로 사용한다.
스프링 부트에 Jar
를 사용하면 /resources/static/index.html
위치에 index.html
파일을 두면 Welcome
페이지로 처리해준다.
(스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html
)이 있으면 된다.)
<!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>
v1</a></li>
<li><a href="/request-param-v1?username=hello&age=20">요청 파라미터 <li><a href="/request-param-v2?username=hello&age=20">요청 파라미터
v2</a></li>
v3</a></li>
v4</a></li>
<li><a href="/request-param-v3?username=hello&age=20">요청 파라미터
<li><a href="/request-param-v4?username=hello&age=20">요청 파라미터
<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">요청 파라미터 <li><a href="/model-attribute-v1?username=hello&age=20">요청 파라미터
MAP</a></li>
@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>
💡 참고
- 스프링 부트 Welcome 페이지 지원
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot- features.html#boot-features-spring-mvc-welcome-page
- 지금까지 공부하며 콘솔창에 실행결과나 기대값을
System.out.println()
을 통해 출력하였다.- 실제 운영을 할 때는 시스템 콘솔이 아닌 별도의 로깅 라이브러리를 사용해 출력을 한다.
- 많은 로깅 라이브러리가 있지만, 그 중
SLF4J
,Logback
을 알아보자!
스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리 (
spring-boot-starter-logging
)가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
SLF4J
=http://www.slf4j.org
Logback
:http://logback.qos.ch
Logback
, Log4J
, Log4J2
등등 수많은 라이브러기가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 SLF4J
라이브러리이다.SLF4J
는 인터페이스이고, 그 구현체는 Logback
과 같은 로그 라이브러리를 선택하면 된다.Logback
을 대부분 사용한다.
로깅을 사용하기위해 먼저 로깅 객체를 생성해야 한다.
// getClass()메서드를 통해 사용되는 클래스 타입 변환하여 삽입
private final Logger log = LoggerFactory.getLogger(getClass());
// 직접적으로 해당 클래스타입을 입력해주어도 된다.
private static final logger log = LoggerFactory.getLogger(Xxx.class);
@Slf4j
public class LogTestController {
...
}
@Slf4j
를 넣어두면
private final Logger log = LoggerFactory.getLogger(getClass());
와 같이 log 생성자를 호출할 필요가 없다.
(자동 등록)
log.info("hello")
main.java.springmvc.basic.LogTestController
package hello.springmvc.basic;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@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
// 문자 반환시 String이 반환된다.
}
}
@RestController
➡️ @Controller
는 반환 값이 String이면 뷰 이름으로 인식하기에 뷰를 찾고 뷰가 렌더링된다.
➡️ @RestController
는 반환 값으로 뷰를 찾는게 아니라 HTTP 메세지 바디
에 바로 입력한다.
(클래스 레벨이 아닌 메서드 레벨에서 @ResponseBody
를 사용하면 @Controller
를 사용하더라도 바로 HTTP 메세지 바디
에 입력해서 반환을 해준다.)
로그 출력 포맷
➡️ 시간, 로그 레벨, 프로세스 ID(PID), 쓰레드 명, 클래스 명, 로그 메세지
5개의 로그를 출력시도 하였지만, 실행결과에서는 3개가 나왔다.
이유
- 로그에는 레벨이 있다. 로그레벨을 설정하면 그 로그 보다 우선순위가 높은 것만 출력된다.
- 스프링 부트에서 기본으로 설정되어 있는 로그레벨은
info
이다.- 그렇기에 info보다 우선순위가 낮은
debug
,trace
는 출력되지 않는다.info
를 줄시info
,warn
,error
가 출력된다.
원하는대로 로그 레벨을 설정할 수 있다.
application.properties
에서 레벨을 설정할 수 있다.
# 전체 로그 레벨 설정(기본 info)
logging.level.root=info
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
#logging.level.hello.springmvc=[변경을 원하는 로그 레벨]
logging.level.hello.springmvc=debug
- LEVEL :
TRACE
>DEBUG
>INFO
>WARN
>ERROR
- 개발 서버는
debug
출력- 운영 서버는
info
출력
log.debug("data"+data)
: 올바르지 않은 로그 사용
- 로그 출력 레벨을
info
로 설정해도 해당 코드에 있는"data="+data
가 실제 실행이 되어 버린다.- 문자 더하기 연산이 발생한다.
- 리소스 낭비이다.
log.debug("data={}", data)
: 올바른 로그 사용
- 이와 같이 작성시 로그 출력 레벨을
info
로 설정하면 아무 일도 발생하지 않는다. (DEBUG > INFO
)- 앞과 같은 의미없는 연산이 발생하지 않는다.
System.out
보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.
💡 참고
스프링 부트가 제공하는 로그 기능은 다음을 참고하자 :https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot- features.html#boot-features-logging
요청 매핑 : 요청이 왔을 때 어떤 컨트롤러에서 매핑을 할지 조사해서 매핑을 진행하는 것
MappingController
package hello.springmvc.basic.requestmapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
/**
* 기본 요청
* 둘다 허용 /hello-basic, /hello-basic/
* HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE */
// @RequestMapping({"/hello-basic", "/hello-go"})
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
...
}
@RestController
➡️ @Controller
는 반환 값이 String
이면 뷰 이름으로 인식된다. 그래서 뷰를 찾고 뷰가 렌더링 된다.
➡️ @RestController
는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. (따라서 실행 결과로 ok 메세지를 받을 수 있다. @ResponseBody
)와 관련이 있다.
@RequestMapping("/hello-basic")
➡️ /hello-basic
URL 호출이 오면 이 메서드가 실행되도록 매핑한다.
➡️ 대부분의 속성을 배열[]
로 제공하므로 다중 설정이 가능하다. (ex. {"/hello-basic", "/hello-go"}
)
둘다 허용
다음 두가지 요청은 다른 URL이지만, 스프링은 다음 URL 요청들을 같은 요청으로 매핑한다.
- 매핑 :
/hello-basic
- URL 요청 :
/hello-basic
,/hello-basic/
HTTP 메서드
@RequestMapping
에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.- 모두 허용 :
GET
,HEAD
,POST
,PUT
,PATCH
,DELETE
/**
* method 특정 HTTP 메서드 요청만 허용
* GET, HEAD, POST, PUT, PATCH, DELETE
*/
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
POST
요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)
를 반환한다.
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
GetMapping
, PostMapping
, PatchMapping
, DeleteMapping
@RequestMapping
과 method
를 지정해서 사용하는 것을 확인할 수 있다.
/**
* PathValuer
* PathVariable 사용
* 변수명이 같으면 생략 가능
*
* @PathVariable("userId") String userId -> @PathVariable userId
* /mapping/userA
*/
@GetMapping
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
실행
http://localhost:8080/mapping/userA
/mapping/userA
/users/1
@RequestMapping
은 URL 경로를 템플릿화 할 수 있는데, @PathVariable
을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.@PathVariable
의 이름과 파라미터 이름이 같으면 생략할 수 있다.@PathVariable("data") String data
→ @PathVariable String data
/**
* 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";
}
실행
http://localhost:8080/mapping/users/userA/orders/100
PathVariable
도 사용이 가능하다.
/**
* 파라미터로 추가 매핑
* 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";
}
실행
http://localhost:8080/mapping-param?mode=debug
/**
* 특정 헤더로 추가 매핑
* 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 = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
Content-Type
헤더를 기반으로 미디어 타입으로 매핑한다.consumes = "text/plain"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE
/*
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
HTTP 406 상태코드(Not Acceptable)
을 반환한다.produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"
회원관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자!
HTTP API 구조만 만들자!
(실제 데이터가 넘어가는 부분은 생략하고 URL 매핑만)
회원 관리 API
- 회원 목록 조회 :
GET
/users
- 회원 등록 :
POST
/users
- 회원 조회 :
GET
/users/{userId}
- 회원 수정 :
PATCH
/users/{userId}
- 회원 삭제 :
DELETE
/users/{userId}
MappingClassController
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/**
* GET /mapping/users
*/
@GetMapping
public String users() {
return "get users";
}
/**
* POST /mapping/users
*/
@PostMapping
public String addUser() {
return "post user";
}
/**
* GET /mapping/users/{userId}
*/
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
/**
* PATCH /mapping/users/{userId}
*/
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
/**
* DELETE /mapping/users/{userId}
*/
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
/mapping
: 강의의 다른 예제들과 구분하기 위해 사용@RequestMapping("/mapping/users")
: 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.
- 회원 목록 조회 :
GET
/users
- 회원 등록 :
POST
/users
- 회원 조회 :
GET
/users/userA
- 회원 수정 :
PATCH
/users/userA
- 회원 삭제 :
DELETE
/users/userA
매핑 방법을 이해했으니, 이제부터 HTTP 요청이 보내는 데이터들을 스프링 MVC로 어떻게 조회하는지 알아보자!
참고