๐โโ๏ธ RestAPI๋,
ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๊ธฐ ์ํ ์ธํฐํ์ด์ค๋ก, ์น ์ดํ๋ฆฌ์ผ์ด์ ์์ ์๋ฒ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๊ณ ๋ฐ์ ์ ์๋๋ก ๋๋ ๊ธฐ์
- REST API๋ ๋ฐ์ดํฐ ํํ๋ฅผ ์ฃผ๋ก JSON ํน์ XML ํ์์ผ๋ก ๋ฐํ
- ํด๋ผ์ด์ธํธ๋ HTTP ์์ฒญ(GET, POST, PUT, DELETE ๋ฑ)์ ๋ณด๋ด๋ ๊ฒ์ผ๋ก ์๋ฒ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ ์ด์ฉ ๊ฐ๋ฅ
public class Message {
private int httpStatusCode;
private String message;
/* ๊ธฐ๋ณธ ์
ํ
*/
}
@RestController
/* @Controller + @ResponseBody์ ์๋ฏธ
* ํด๋์ค ๋ ๋ฒจ์ ์์ฑํ๋ฉฐ ํด๋น ํด๋์ค ๋ด์ ๋ชจ๋ ํธ๋ค๋ฌ ๋ฉ์๋์ @ResponseBody ์ด๋
ธํ
์ด์
์ ๋ฌต์์ ์ผ๋ก ์ ์ฉํ๋ค๋ ์๋ฏธ (๋ฆฌํด๊ฐ์ด ๊ณง ์๋ต๊ฐ) */
@RestController
@RequestMapping("/response")
public class ResponseRestController {
/* 1. ๋ฌธ์์ด ์๋ต */
@GetMapping("/hello")
public String helloworld() {
return "Hello World๐ฅณ";
}
/* 2. ๊ธฐ๋ณธ ์๋ฃํ ์๋ต */
@GetMapping("/random")
public int getRandomNumber() {
return (int) (Math.random() * 10) + 1;
}
/* 3. Object ์๋ต */
@GetMapping("/message")
public Message getMessage() {
return new Message(200, "๋ฉ์ธ์ง ์๋ต ์ฑ๊ณต!๐คฉ");
}
/* 4. List ์๋ต */
@GetMapping("/list")
public List<String> getList() {
return List.of(new String[] {"ํ์ํฐ๐น", "ํธ๋ญ์ด๐ฏ", "์ฐ๋
๊ธฐ๐ฐ"});
}
/* 5. Map ์๋ต */
@GetMapping("/map")
public Map<Integer, String> getMap() {
List<Message> messageList = new ArrayList<>();
messageList.add(new Message(200, "์๋ต ์ฑ๊ณต!๐ฅ"));
messageList.add(new Message(404, "ํ์ด์ง๋ฅผ ์ฐพ์ง ๋ชป ํ์ด์๐ฉ"));
messageList.add(new Message(500, "๋์ ์๋ชป!๐ท"));
return messageList.stream().collect(Collectors.toMap(Message::getHttpStatusCode, Message::getMessage));
}
/* 6. ImageFile ์๋ต
* produces ์ค์ ์ response header์ content-type ์ค์
* ์ค์ ์ ๋ฐ๋ก ํ์ง ์์ผ๋ฉด text/html๋ก ์๋ตํ๊ธฐ ๋๋ฌธ์ ์ด๋ฏธ์ง๊ฐ ํ
์คํธ ํํ๋ก ์ ์ก ๋จ*/
@GetMapping(value="/image", produces=MediaType.IMAGE_PNG_VALUE)
public byte[] getImage() throws IOException {
return getClass().getResourceAsStream("/com/greedy/api/section01/response/sample.png").readAllBytes();
}
/* 7. ResponseEntity๋ฅผ ์ด์ฉํ ์๋ต (๋๋ถ๋ถ ์ด ๋ฐฉ์์ ์ฌ์ฉ) */
@GetMapping("/entity")
public ResponseEntity<Message> getEntity() {
return ResponseEntity.ok(new Message(123, "Baby Cheese"));
}
}
ResponseEntity
๐โโ๏ธ Response Entity๋,
๊ฒฐ๊ณผ ๋ฐ์ดํฐ์ HTTP ์ํ ์ฝ๋, ์๋ต ํค๋๋ฅผ ์ง์ ์ ์ดํ ์ ์๋ ํด๋์ค
public class UserDTO {
private int no;
private String id;
private String pwd;
private String name;
private Date enrollDate;
/* ๊ธฐ๋ณธ ์
ํ
*/
}
public class ResponseMessage {
private int httpStatus;
private String message;
private Map<String, Object> results;
/* ๊ธฐ๋ณธ ์
ํ
*/
}
@RestController
@RequestMapping("/entity")
public class ResponseEntityTestController {
private List<UserDTO> users;
public ResponseEntityTestController() {
users = new ArrayList<>();
users.add(new UserDTO(1, "user01", "pass01", "ํ์น์น", new Date()));
users.add(new UserDTO(2, "user02", "pass02", "ํ๊ผฌ์", new Date()));
users.add(new UserDTO(3, "user03", "pass03", "ํ๋ถ์", new Date()));
}
@GetMapping
HttpHeaders
setContentType()
MediaType
Charset.forName()
@GetMapping("/users")
public ResponseEntity<ResponseMessage> findAllUsers() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("users", users);
ResponseMessage responseMessage = new ResponseMessage(200, "์กฐํ ์ฑ๊ณต!๐ถ", responseMap);
return new ResponseEntity<>(responseMessage, headers, HttpStatus.OK); // new ResponseEntity<>(๋ฉ์ธ์ง, ์ปจํ
์ธ ํ์
, ์ํ์ฝ๋)
// users๋ผ๋ key๊ฐ์ addํ user๋ค์ Map์ ๋ฃ๊ณ , responseMessage์ ๊ทธ Map์ ํจ๊ป ๋ด์ ๋ณด๋
// ๊ฒฐ๊ตญ ์๋ต ๊ฐ์ responseMessage๊ฐ JSON ํํ๋ก ์ด !!
}
@GetMapping
stream()
filter()
toList()
ok()
headers()
body()
@GetMapping("/users/{userNo}")
public ResponseEntity<ResponseMessage> findUserByNo(@PathVariable int userNo) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));
UserDTO foundUser = users.stream().filter(user -> user.getNo() == userNo).toList().get(0); // no๊ฐ ์ผ์นํ๋ user๋ฅผ ์ฐพ๊ณ List๋ก ๋ฐํ
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("user", foundUser);
/* ๋น๋ ํจํด์ผ๋ก๋ ์์ฑ ๊ฐ๋ฅ (๋ฐฉ์์ ์ฐจ์ด์ผ๋ฟ, ๊ฒฐ๊ณผ๋ ์์ ๊ฐ์) */
return ResponseEntity
.ok()
.headers(headers)
.body(new ResponseMessage(200, "์กฐํ ์ฑ๊ณต!๐ถ", responseMap));
}
@PostMapping
@RequestBody
created(URI.create())
build()
// body ์ชฝ์ ์๋ตํ ๊ฒ์ด ๋ฐ๋ก ์์ ๋ ResponseEntity<?>
@PostMapping("/users")
public ResponseEntity<?> registUser(@RequestBody UserDTO newUser) {
// @ModelAttribute : url encoded ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๊ฐ ๋์ด์ด์ ์๋ฏธ(๊ธฐ๋ณธ๊ฐ, ์๋ต๊ฐ๋ฅ) => key=value&key=value
// @RequestBody : JSON ๋ฐฉ์์ ๋ฌธ์์ด๋ก ๋์ด์ด์ ์๋ฏธ(JSON ๋ฐฉ์์ผ๋ก ๋ฐ๊ธฐ ์ํด ๋ช
์ ํ์) => "key" : value, "key" : value
int lastUserNo = users.get(users.size()- 1).getNo();
newUser.setNo(lastUserNo + 1);
newUser.setEnrollDate(new Date());
users.add(newUser);
/* ์กฐํ ์์๋ 200๋ฒ ์ฝ๋๋ฅผ ์๋ตํ์ง๋ง ์ฝ์
์์๋ 201๋ฒ ์ฝ๋๋ฅผ ์๋ต
* 201 : ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ ๋์์ผ๋ฉฐ, ์์์ด ์์ฑ๋์์์ ๋ํ๋ด๋ ์ฑ๊ณต ์ํ ์๋ต ์ฝ๋
* ํด๋น ์์์ ๋ํ ์์ฒญ url์ location์ผ๋ก ์ค์ ํ์ฌ ์๋ต */
return ResponseEntity
.created(URI.create("/entity/users/" + users.get(users.size() - 1).getNo()))
.build();
// ์์์ newUser๊ฐ ์ถ๊ฐ ๋์์ผ๋ฏ๋ก ์ด users์ ์ฌ์ด์ฆ์์ -1์ ํ ๊ฒ์ no๊ฐ ์ถ๊ฐํด์ผํ user
}
@PutMapping
@PutMapping("/users/{userNo}")
public ResponseEntity<?> modifyUser(@PathVariable int userNo, @RequestBody UserDTO modifyInfo) {
UserDTO foundUser = users.stream().filter(user -> user.getNo() == userNo).toList().get(0);
foundUser.setId(modifyInfo.getId());
foundUser.setPwd(modifyInfo.getPwd());
foundUser.setName(modifyInfo.getName());
/* ์์ ์์ฒญ์ผ๋ก ์์์ด ๋ณ๊ฒฝ ๋๋ค๋ฉด 201๋ฒ ์ฝ๋๋ก ์๋ต
* ๋ง์ฝ ์์ ์์ ์์ฒญ์ ๊ฒฐ๊ณผ๊ฐ ๊ธฐ์กด์ ์์ ๋ด์ฉ๊ณผ ๋์ผํ์ฌ ๋ณ๊ฒฝ๋ ๋ด์ฉ์ด ์์ ๋๋ 204๋ก ์๋ตํ ์๋ ์์
* 204 : ์์ฒญ์ด ์ฑ๊ณตํ์ผ๋ ํด๋ผ์ด์ธํธ๊ฐ ํ์ฌ ํ์ด์ง์์ ๋ฒ์ด๋์ง ์์๋ ๋๋ค๋ ๊ฒ์ ๋ํ๋ด๋ ์ํ ์ฝ๋ */
return ResponseEntity
.created(URI.create("/entity/users/" + userNo))
.build();
}
@DeleteMapping
noContent()
@DeleteMapping("/users/{userNo}")
public ResponseEntity<?> removeUser(@PathVariable int userNo) {
UserDTO foundUser = users.stream().filter(user -> user.getNo() == userNo).toList().get(0);
users.remove(foundUser);
/* ์ญ์ ์์ฒญ์ ํ์ฌ ๋์ด์ ์ฐธ์กฐํ ์ ์์ ๊ฒฝ์ฐ 204๋ฒ์ผ๋ก ์๋ตํ๋ ๊ฒ์ด ๊ท์น */
return ResponseEntity
.noContent()
.build();
}
}
๐โโ๏ธ ๋ฐ์ดํฐ ๊ฒ์ฆ(validation)์ด๋,
ํด๋ผ์ด์ธํธ์ ๋ฐ์ดํฐ๋ ์กฐ์์ด ์ฝ๊ณ , ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ธ ๋ฐฉ์์ผ๋ก ๋ค์ด์ค์ง ์์ ๊ฐ๋ฅ์ฑ๋ ์๊ธฐ ๋๋ฌธ์ Client side ๋ฟ๋ง ์๋๋ผ Server side์์๋ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ํ ํ์์ฑ ์กด์ฌ !
๐ Spring boot starter validation ํ์ฉ
๐คโ ์ฌ์ฉ ๋ฐฉ๋ฒ
[1] ์์กด์ฑ ์ถ๊ฐ
[2] controller์ ํธ๋ค๋ฌ ๋ฉ์๋์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ ์ฉํ request ๊ฐ์ฒด ์์@Validated
์ด๋ ธํ ์ด์ ์์ฑ
[3] request๋ฅผ ํธ๋ค๋งํ ๊ฐ์ฒด ์ ์ ์ @Validation ์ด๋ ธํ ์ด์ ์ ํตํด ํ์ํ ์ ํจ์ฑ ๊ฒ์ฌ ์ ์ฉ
์์กด์ฑ ์ถ๊ฐ
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@NotNull
@NotBlank
@Size
@DateTimeFormat
@Past
public class UserDTO {
private int no;
// @NotNull(message = "์์ด๋๋ ํ์ ์
๋ ฅ ์ฌํญ์๋ค๐ฌ") // null์ ํ์ฉํ์ง ์์ผ๋ "", " "๋ฑ์ ํ์ฉ
@NotBlank(message = "์์ด๋๋ ๊ณต๋ฐฑ์ผ ์ ์์ด๋ค๐ฌ") // null, "", " " ๋ชจ๋ ํ์ฉ X
private String id;
private String pwd;
@Size(min=2, message="์ด๋ฆ์ ๋ ๊ธ์ ์ด์ ์
๋ ฅํด์ผํจ๋ค๐ฌ")
private String name;
@DateTimeFormat(pattern="yyyy-MM-dd")
@Past // @Past : ํ์ฌ๋ณด๋ค ๊ณผ๊ฑฐ, @Future : ํ์ฌ๋ณด๋ค ๋ฏธ๋
private Date enrollDate;
/* ๊ธฐ๋ณธ ์
ํ
*/
}
public class UserNotFoundException extends Exception {
public UserNotFoundException(String msg) {
super(msg);
}
}
@Validated
@RestController
@RequestMapping("/valid")
public class ValidTestController {
@GetMapping("/users/{userNo}")
public ResponseEntity<?> findUserByNo() throws UserNotFoundException {
boolean check = true;
if(check) {
throw new UserNotFoundException("ํ์ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ด์ฉ๐ฅบ");
}
return ResponseEntity.ok().build();
}
@PostMapping("/users")
public ResponseEntity<?> registUser(@Validated @RequestBody UserDTO user) {
/* null์ ๋ฐํํด๋ ์๊ด์์ผ๋, ๋ฏธ์์ฑ ๊ฐ์ผ๋ฏ๋ก ์ฝ๋ ์ถ๊ฐ */
return ResponseEntity
.created(URI.create("/valid/users/4"))
.build();
}
}
public class ErrorResponse {
private String code;
private String description;
private String detail;
/* ๊ธฐ๋ณธ ์
ํ
*/
}
@ControllerAdvice
: ์ปจํธ๋กค๋ฌ๋ฅผ ๋ณด์กฐํด์ฃผ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค๋ ๊ฒ์ ๋ช
์ํ๋ ์ด๋
ธํ
์ด์
์ผ๋ก ํด๋น ํด๋์ค ๋ด์ ์ ์ญ ์์ธ์ฒ๋ฆฌ ๋ฑ๋ก ๊ฐ๋ฅ
@ExceptionHandler
MethodArgumentNotValidException
getBindingResult()
hasErrors()
getFieldError()
getDefaultMessage()
getCode()
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserException(UserNotFoundException e) {
String code = "ERROR_CODE_00000";
String description = "ํ์ ์ ๋ณด ์กฐํ ์คํจ!";
String detail = e.getMessage();
return new ResponseEntity<>(new ErrorResponse(code, description, detail), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> mothodVaildException(MethodArgumentNotValidException e) {
String code = "";
String description = "";
String detail = "";
/* ๊ธฑ๊ธฐ ๋ค๋ฅธ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๊ฒฝ์ฐ, ๊ฐ ๋ฉ์ธ์ง๋ค์ด ์ถ๋ ฅ๋๋๋ก ํจ */
if(e.getBindingResult().hasErrors()) {
detail = e.getBindingResult().getFieldError().getDefaultMessage(); /* UesrDTO์์ ์ด๋
ธํ
์ด์
์ message์ ์
๋ ฅํ ๋ฉ์ธ์ง ๊ฐ์ ธ์ค๊ธฐ */
String bindResultCode = e.getBindingResult().getFieldError().getCode();
System.out.println("bindResultCode : " + bindResultCode);
switch(bindResultCode) {
case "NotNull" :
code = "ERROR_CODE_00001";
description = "ํ์ ๊ฐ์ด ๋๋ฝ๋์์ด์!";
break;
case "NotBlank" :
code = "ERROR_CODE_00002";
description = "ํ์ ๊ฐ์ด ๊ณต๋ฐฑ์ผ๋ก ์ฒ๋ฆฌ๋์์ด์!";
break;
case "Size" :
code = "ERROR_CODE_00003";
description = "์๋ง์ ํฌ๊ธฐ์ ๊ฐ์ด ์
๋ ฅ๋์ง ์์์ด์!";
break;
case "Past" :
code = "ERROR_CODE_00004";
description = "์๋ง์ ๊ธฐ๊ฐ์ ๊ฐ์ด ์
๋ ฅ๋์ง ์์์ด์!";
break;
}
}
/* ์ ํจ์ฑ ์กฐ๊ฑด์ ๋ถ์ผ์นํ๋ฉด code, description, detail๊ฐ ์ถ๋ ฅ๋จ */
return new ResponseEntity<>(new ErrorResponse(code, description, detail), HttpStatus.BAD_REQUEST);
}
}
์์กด์ฑ ์ถ๊ฐ
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-hateoas -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>