개발자로 후회없는 삶 살기
spring PART.스프링 MVC 기본 기능, API 설계 실전 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/roadmaps/373
본론
- 로깅
앞으로 sout는 안쓸 것입니다. 실무에서는 안 씁니다. 로그를 통해서 콘솔이든 어디든 보고 싶은 결과를 출력해야합니다. 별도의 로깅 라이브러리를 사용해서 로그를 출력합니다.
- 로깅 라이브러리
스프링 부트로 프로젝트를 만들면 기본적으로 starter가 들어가는데 logging이라는 라이브러리가 들어갑니다. logback, slif4j가 들어갑니다.
1) slf4j : 세상에 엄청 많은 로그 라이브러리 가 있는데 그걸 인터페이스화한 것입니다.
2) logback : 그 구현체입니다. 실무에서는 logback을 대부분 사용합니다.
- 클래스 만들기
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@GetMapping("/log-test")
public String logTest() {
String name = "spring";
System.out.println("name = " + name);
log.info("info log = {}", name);
return "OK";
}
}
로그를 쓰려면 컨트가 필요한데 @Rest 컨트를 쓰겠습니다. 그냥 컨트보다 편한 게 있습니다. private final로 Logger를 선언합니다. slf4j꺼를 써야합니다. 롬복을 쓰면 자동으로 넣어줍니다.
> logTest 메서드를 만들어서 @RequestMapping해서 메서드를 호출하게 할 것입니다. Str에 출력할 로그를 만들고 log.info에 로그에 남을 것을 "= {}, 치환" 형식으로 만듭니다.
> RestController를 사용하는 이유는 원래는 그냥 Controller를 쓰면 return에 반환되는게 뷰 이름입니다. 근데 rest를 쓰고 return ok를 하면 rest API의 그 rest이고 body에 str이 들어갑니다. 테스트할 때 간단하게 할때 쓰면 좋겠습니다. rest api 만들 때 굉장히 핵심적인 컨트입니다.
> 실행해보면 시간도 나오고 INFO라고 나오고 프로세스 ID 12514도 나오고 스레프 풀에서 나와서 현재 실행한 nio 스레드도 나오고 현재 나의 컨트 이름도 나오고 메세지도 나옵니다. sout와는 굉장히 큰 차이입니다. log하나 찍었다고 이 많은 정보를 볼 수 있는 것입니다. 로거를 쓰면 이렇게 편리하게 로그 찍힌 것을 보고 버그를 찾을 수 있습니다. > 로거의 진가는 로그를 찍을 때 이 로그가 어떤 상태의 로그인지 지정할 수 있다는 것입니다.
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@GetMapping("/log-test")
public String logTest() {
String name = "spring";
System.out.println("name = " + name);
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);
return "OK";
}
}
1) Info를 하면 현재 로그는 중요한 정보
2) debug를 하면 현재 로그 상태는 개발 서버에서 보는 것
3) warn는 위험한 것
4) error는 에러난 것, 이 로그가 남으면 알람이 오거나 별도의 파일로 저장하거나 해야합니다.
실행해보면 3가지 에러가 남고 2개는 안남습니다.
로컬에서 개발할 때 모든 로그를 보고 싶으면 프로퍼티스에 logging.level.hello.springmvc = trace하면 trace부터 다 보겠다는 것입니다.
debug로 하면 trace가 안나옵니다.
-> 올바른 로그 사용법
log.trace("trace log = " + name);
log.trace("trace log = {}", name);
지금 보면 "" + name으로 해도 아무 문제가 없습니다. 근데 {}로 ★ 치환을 하는 이유는 자바 언어의 특징과 관련있는데 log.()를 하기 전에 +를 먼저해서 trace log spring을 먼저 만들고 가지고 있습니다. 연산을 먼저하는게 문제입니다. 프로퍼티에 debug를 하면 trace가 안 나오는데 연산이 일어나는 것입니다. 쓸모없는 낭비가 발생하는 것이라서 이렇게 쓰면 안됩니다.
> 또한 sout는 무조건 콘솔에 남는데 로그는 파일로 별도로 저장할 수 있습니다. 또한 파일이 다 차서 디스크 터지는 오류가 나는 것이 옛날에 많았는데 이제는 분할 파일 저장에 zip도 해주고 로그를 찍을 때 내부 버퍼링도 하고 멀티 쓰레드 등등 다 성능을 극한으로 최적화를 했습니다. 실무에서는 꼭 로그를 써야합니다. 실제 테스트해보면 성능이 수 십배 차이가 납니다.
- 요청 맵핑
'요청이 왔을 때 어떤 컨트롤러가 호출이 되어야 하는지'를 배웁니다. (생각해보면 이전에 다 따로 있던 컨트롤러를 한 곳에 모은 것이니 메서드가 다 컨트롤러입니다.) 단순하게 url, 여러가지 요소를 조합해서 맵핑 등 많습니다.
- PathVariable
@RestController
@Slf4j
public class MappingController {
@GetMapping("/mapping/{userID}")
public String mappingPath(@PathVariable String userID) {
log.info("mapping variable= {}", userID);
return "OK";
}
}
요즘 이런 스타일의 경로를 많이 씁니다. 메서드는 똑같이 만드는데 @GetMapping에 url에 /mapping/{userID}라고 합니다. 요청에 url 자체에 뭔가 값이 들어간 채로 요청이 옵니다. /mapping/userA 이런식으로 요청이 왔습니다. 메서드의 매개에 @PathVariable {}에 있는 문자열 "userId"를 String data에 꺼낼 수 있습니다. @RequestParam도 질의 파라미터를 바로 꺼내서 username=hsb라면 @RequestParam("username") string username라고 이라고 했는데 url로 뭘 받을 때 이런 모양을 많이 쓰나봅니다.
실행해보면 URL에 값을 ?A=B 쿼리 파라미터 형식이 아니라 데이터만 넣어서 요청할 수 있고 pathvariable이라는 것으로 꺼내 사용할 수 있습니다. 이건 진짜 많이 사용합니다. http api가 이 방식을 선호합니다.
-> 다중 맵핑
@GetMapping("/mapping/users/{userID}/orders/{orderID}")
public String mappingPath(@PathVariable String userID, @PathVariable Long orderID) {
log.info("mapping variable= {}", userID);
return "OK";
}
/mapping/users/{userID}/orders/{orderId} pathvariable 방식을 쓰면 RequestParam과 마찬가지로 컨트롤러 메서드에 (@PathVariable String userID)할 때 (@PathVariable Long orderId)처럼 타입을 지정할 수 있습니다.
실행해보면 userA, 100 등 {}로 되어있는 것이 다 추출됩니다.
-> 조건 맵핑
@GetMapping(value = "/mapping/users/{userID}/orders", params = "mode=debug")
public String mappingParam(@PathVariable String userID) {
log.info("mapping variable= {}", userID);
return "OK";
}
mode=debug가 있을 때만 쓰는 것인데 이는 거의 쓸 일이 없습니다.
-> 미디어 타입 조건 맵핑
@GetMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mapping consumes");
return "OK";
}
만약 내가 content type에 따라서 application/json이면 예를 호출하고 html 텍스트 처리 못 하게 분리할 수 있습니다. consumes = 를 url에 넣으면 됩니다.
postman으로 contenttype을 넣을 것입니다. body에 row JSON이라고 하고 데이터를 넣으면 Headers가 자동으로 content type이 app/json으로 바뀌어있습니다. postman에서 send해보면 잘 나옵니다. consume인 이유는 요청을 받는 입장이라서 소비하는 것입니다.
@GetMapping(value = "/mapping-produce", produce = "application/json")
public String mappingProduce() {
log.info("mapping produce");
return "OK";
}
produce는 accept 헤더가 필요합니다. application/json으로 하면 클라 입장에서 accept 헤더가 있으면 나는 app/json만 받아들일 수 있어라는 것인데 지금 맵핑 url에 produces를 text/html로 하니 에러가 뜹니다. accept를 app/json으로 하니 메서드가 호출됩니다.
postman에서 요청에서 accept 헤더를 text/html이라고 바꾸면 무슨 의미냐면 클라가 나는 contetn type이 html인 것을 받아들일 수 있어라는 의미입니다.
- 요청 맵핑 API 예시
이번에는 지금까지 배운 요청 맵핑을 API로 만드는 예시를 알아보겠습니다. 예를 들어 회원 관리 HTTP API를 만듭니다. 생각하고 매핑을 어떻게 할지 알아볼 것입니다. 데이터가 넘어가는 것은 나중에 배우겠습니다. API 설계 방식은 3가지로 POST기반, PUT기반, HTML FORM 기반이 있습니다.
1. POST 기반
POST 기반과 PUT 기반에서 POST와 PUT 둘 다 등록인데 차이가 있었습니다. ★ URI이 리소스를 식별해야지 행위를 식별하면 안되는 것은 같습니다. 근데 등록할 때 PATH를 /MEMBERS라고만 넘기면 서버에서 새로운 리소스인 /MEMBERS/100을 만들고 응답 메세지에 LOCATION: /MEMBERS/100이라고 줬습니다.
2. PUT 기반
PUT 기반은 새로운 등록을 할 때 클라가 리소스 URI를 다 알고 있습니다. 파일일 경우 /FILES/{FILENAME}으로 클라가 파일을 알고 있는 것이고 멤버로 보면 /MEMBERS/{ID}로 클라가 ID를 알고 있는 것입니다. 파일은 알고 있는 게 말이 되는데 ID는 일련 번호라 알고 있을 수 없습니다. 알 수 있을 때 PUT, 없을 때 POST를 씁니다.
3. FORM 기반
FORM 방식은 등록 폼 페이지에 가는 것은 GET하고 등록은 POST로 하고 수정 폼에 가는 것은 GET하고 수정하는 것은 POST로 하고 둘이 다른 페이지를 넘나들면서 같은 URI를 쓰는 방식이었습니다.
-> 이번 예제 API 스팩
이번 예제는 등록이 POST로 POST 기반 API 설계입니다. 등록이 POST냐 PUT이냐로 방식을 알아볼 수 있겠습니다.
회원 목록 조회 : GET에 /users
회원 등록 : POST에 /users
이렇게 하면 같은 URL인데 HTTP 메서드로 기능을 구분할 수 있습니다.
회원 조회 : GET에 /uesrs/{userId}
회원 수정 : PATCH /uesrs/{userId}
회원 삭제 : DELETE /users/{userId}
이것도 URL은 같은데 메서드로 행위를 기능을 구분할 수 있습니다. 이것을 한 번 만들어 보겠습니다.
-> 구현
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
@GetMapping
public String users() {
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;
}
}
클래스를 새로 만듭니다. @Rest를 씁니다. users는 user 반환하는 것입니다. url을 아까 한 것처럼 /users로 하는 데 다른 예제들과 안 겹치게 prefix를 넣었습니다. 그냥 /users라고 보면 됩니다.
addUser는 회원 등록하는 것으로 url은 같고 post mapping입니다. 회원 1명 조회하는 것은 get으로 url은 /users/{userId} 로 메서드 매개에 path variable을 써야합니다. 수정은 Patch, 삭제는 Delete 메서드를 씁니다.
url을 post, put, html form 기반 중 선택해서 설계합니다. 그리고 이것을 컨트롤러의 맵핑 메서드에 작성합니다. url은 설계한대로 적고 메서드 이름은 당연한 것이지만 기능에 맞춰서 적습니다. 조회, 삭제, 수정 같은 어떤 딱 1개의 리소스만 접근해야할 경우 path variable을 생각할 수 있어야합니다. > 맵핑 url에 중복은 클래스에 requestmapping 만들어서 해줍니다.
5가지 api를 코드로 작성했습니다. postman으로 테스트해보겠습니다. get도 잘 나오고 path variable을 사용한 1명 조회하는 것도 잘 나옵니다.
- 기본, 헤더 조회
앞에서는 api 설계를 알아보느라 url은 제대로 했는데 데이터는 없이 return str을 했습니다. 이번에 데이터를 다뤄보겠습니다. 근데 그 전에 데이터를 주고 받는 것을 하기 전에 서블릿 처음 배웠을 때 생각해보면 서비스 메서드에서 http 요청 메시지의 헤더를 어떻게 쉽게 꺼내는지 부터 봤었습니다. 이번에는 스프링이 더 편하게 제공하는 방법을 보겠습니다. 다음 시간에 진짜 데이터 꺼내는 것을 알아보겠습니다.
-> 구현
패키지를 만들고 새로운 클래스를 만들고 slf4j 롬복과 RestController를 합니다. 헤더 정보를 조회해볼 메서드를 만들 것입니다. RequestMapping을 /headers로 하고 headers 메서드를 만듭니다.
@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) {
스프링은 서블릿이 서비스에서 req, resp만 받을 수 있었던 것에 비해 ★ 엄청나게 다양하고 세세한 것까지 메서드에서 매개로 직접 받을 수 있습니다. HttpReq, Resp을 받습니다. 메서드가 직접 RequestParam으로 질의 파라미터를 받을 수 있습니다고 했는데 req, resp도 받을 수 있습니다., HttpMethod(get, post)도 받을 수 있고 Locale로 언어 정보도 받을 수 있고 헤더도 @RequestHeader로 담을 수 있습니다. 맵으로 헤더 맵을 받습니다.
> 얘는 헤더를 한번에 다 받는 것이고 하나 받으려면 RequestParam 때처럼 ()에 키를 쓰면 됩니다. host는 www.google.com 헤더 였으니 그것을 받아보겠습니다., 쿠키도 받을 수 있습니다. value가 쿠키이름이고 required는 기본이 T인데 F하면 쿠키가 없어도 된다는 것입니다.
+ 이렇게 req의 set으로 다 받는게 아니고 세세하게 메서드 매개변수로 RequestParam 때처럼 직접 받을 수 있습니다.
post 맨으로 실행하고 결과를 보겠습니다. 멀티 벨류 맵을 보면 모든 헤더의 키와 벨류가 다 나옵니다. multi 벨류 맵은 같은 키에 여러 값을 받을 수 있습니다. map.get하면 같은 키인 벨류가 배열로 반환이 됩니다.
- http 요청 파라미터
쿼리 파라미터와 html form으로 데이터가 요청이 오면 서버에서 어떻게 처리해야하나 알아보겠습니다.
-> http 요청 데이터 조회 개요
이제 진짜 잘 기억해야합니다. 서블릿에서 요청 데이터를 조회하는 방법을 떠올려보겠습니다. 3가지입니다. 클라에서 서버로 요청 데이터를 보내는 방법은 딱 3가지입니다.
1. Get 방식의 쿼리 파라미터 ex url에 ?usetname=hsb
2. Post 방식의 HTML FORM 데이터 보내는 것, 이때는 헤더에 content type이 application/x-www-form-ulrencoded입니다. 메세지 바디에 쿼리 파라미터 형식으로 데이터가 전달됩니다. 따라서 req.getParameter로 get, post 방식 둘 다 요청 데이터를 꺼낼 수 있습니다.
3. HTTP message body에 데이터를 직접 담아서 요청하는 것, 주로 HTTP API에 사용되고 json, text 등을 담아서 넘깁니다. 메서드는 post, put, patch를 주로 사용합니다.
-> 요청 파라미터 쿼리 파라미터, html form 1, 2방식
이 둘은 똑같기 때문에 req.getParameter로 두가지 요청 다 조회할 수 있습니다. GET 쿼리 파라미터 전송 방식이든, POST HTML Form 전송 방식이든 둘 다 형식이 같으므로 구분없이 조회할 수 있습니다. 이걸 간단하게 요청 파라미터 조회라고 합니다.
-> 구현
새로운 컨트롤러를 만들고 메서드를 만듭니다.
@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");
}
}
먼저 req, resp을 매개로 받습니다. 이렇게 하면 get 방식으로 오든, post 방식으로 오든 둘 다 똑같이 조회할 수 있었습니다. 이는 서블릿 방식으로 스프링은 더 쉽게 할 수 있습니다. resp에 쓰는 것도 getWriter로 쓰고 메서드가 끝나면 resp가 자동으로 클라에게 갑니다.
<!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>
이번에는 POST로 오는 요청 파라미터 조회를 해보겠습니다. html을 form을 넣어서 만듭니다. name이 중요하다고 했고 action이 요청할 메서드 uri를 해야합니다.
동일한 컨트롤러를 그대로 사용하였는데 같은 결과가 나옵니다. GET, POST 방식의 요청 파라미터 조회는 형식이 같아서 둘 다 getParameter로 조회할 수 있습니다.
- @RequestParam
스프링이 제공하는 것으로 훨씬 편리하게 사용할 수 있습니다. req를 사용하지 않고 메서드에 바로 요청 파라미터를 받을 수 있습니다.
-> 구현
@RequestMapping("/request-param-v2")
@ResponseBody
public String requestParamV2(@RequestParam("username") String username,
@RequestParam("age") int age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
}
새로운 맵핑 메서드를 만듭니다. 매개로 @RequestParam을 하면 됩니다. ()에 들어가는 것이 html에 name과 같습니다. > return을 OK로 할 건데 RestController가 아니라 Controller로 할 것입니다. 그러면 OK가 str이 아니라 뷰 이름으로 스프링이 인식합니다. 그럴 때 @Response body를 쓰면 return 텍스트일 경우 content 바디에 text를 박아버립니다.
-> 여기서 좀 더 개선할 수 있습니다.
@RequestMapping("/request-param-v4")
@ResponseBody
public String requestParamV4(String username, int age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
@RequestParam의 () 사이의 "username" 같은 것을 생략할 수 있습니다. 뒤에 String username과 같으면 생략 가능합니다. 근데 @RequestParam도 없앨 수 있습니다. 대신 매개변수가 요청 파라미터 이름과 맞아야합니다. members 등 객체가 아니면 @RequestParam을 생략할 수 있습니다. 근데 너무 없으면 이해하기 힘들 것 같습니다.
-> 필수 파라미터 여부
@RequestMapping("/request-param-required")
@ResponseBody
public String requestParamRequired(@RequestParam(required = true) String username,
@RequestParam(required = false) int age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
"이 값이 무조건 들어와야 해"를 설정할 수 있습니다. RequestParam에 (required = true)가 기본 값입니다.
이러면 username이 꼭 있어야합니다. false면 url에 없어도 됩니다. (이거 API 스펙 튕길 때 할 수 있겠습니다.)
-> 주의사항 1
> 근데 age는 false여서 없어도 되는데 해보면 500에러로 서버 문제라고 합니다. 그 이유는 age를 매개에 적어는 놨는데 요청 파라미터에 없으면 null이 들어가는데 int에는 null을 넣을 수 없습니다.
@RequestParam(required = false) Integer age)
null은 레퍼에만 넣을 수 있습니다. 따라서 Integer로 바꿔야합니다.
-> 주의사항 2
username은 required가 T라서 꼭 값을 넣어야한다. 근데 url에 =하고 value를 안 넣으면 통과가 됩니다. required은 null만 막습니다. 근데 이렇게 하면 null이 아니고 빈 문자(" ")로 봐서 통과가 됩니다. 빈 문자라는 값이 들어온 것으로 돕니다.
-> default value
@RequestMapping("/request-param-default")
@ResponseBody
public String requestParamdefault(@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") Integer age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
required만 T로 하면 " "는 통과됩니다. 통과 못하게 막아야합니다.
만약에 없으면 서버가 임의로 알아서 넣겠다는 것입니다. 빈 문자까지 다 막아줍니다.
-> 요청 파라미터 MAP 조회
@RequestMapping("/request-param-map")
@ResponseBody
public String requestParamMap(@RequestParam Map<String, String> paramMap) {
log.info("username = {}, age = {}", paramMap.get("username"), paramMap.get("age"));
return "OK";
}
모든 요청을 맵으로 한 번에 다 받아버립니다.
멀티 벨류 맵은 userid = hsb&userid = hsb2로 하나의 키에 여러 값을 받을 때 멀티 벨류 맵을 씁니다. 하지만 보통 하나의 키에 하나의 값만 씁니다.
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
spring PART.타임리프 기본 기능, 스프링 통합 (0) | 2023.04.16 |
---|---|
spring PART.스프링 기본 기능 2, 웹 쇼핑몰 개발 (0) | 2023.04.15 |
spring PART.스프링 MVC 시작 (0) | 2023.04.13 |
spring PART.유연한 컨트롤러 (0) | 2023.04.12 |
spring PART.MVC 프레임워크 만들기 (0) | 2023.04.11 |