이전에 진행한 코드 패키지 구분을 해준다.
user 패키지 아래에 다음 것들을 추가해준다.
User
package com.example.restfulwebservice.user;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Date joinDate;
}
int id가 아닌 Integer id를 사용하는 이유는 원시값 사용을 하지 않기 위해서다.
원시값 포장과 래퍼
UserDaoService
package com.example.restfulwebservice.user;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class UserDaoService {
private static List<User> users = new ArrayList<>();
private static int userCount = 3;
static {
users.add(new User(1, "Kenneth", new Date()));
users.add(new User(2, "Alice", new Date()));
users.add(new User(3, "Elena", new Date()));
}
public List<User> findAll() {
return users;
}
public User save(User user) {
if (user.getId() == null) {
user.setId(++userCount);
}
users.add(user);
return user;
}
public User findOne(int id) {
for (User user : users) {
if (user.getId() == id) {
return user;
}
}
return null;
}
}
비즈니스 로직 관련된 부분은 Service 클래스에 주로 추가한다. 데이터베이스와 관련된 코드를 다루기 위해서 Dao 클래스를 이용한다. Service로 사용할 것이기 때문에 Service 어노테이션을 추가했다.
UserController
package com.example.restfulwebservice.user;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private UserDaoService service;
public UserController(UserDaoService service) {
this.service = service;
}
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return service.findAll();
}
// GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id);
}
}
생성자를 통한 방식으로 의존성을 주입하고 있다. Service는 @Service 컴포넌트를 붙여놔서 빈으로 등록되었기 때문에 가능하다.
retrieveUser 함수에서 인자로 int형을 받고 있는데 원래는 서버에서 String으로 받는데 int로 받겠다고 하면 전환해준다.
UserController
@PostMapping("/users")
public void createUser(@RequestBody User user) {
User savedUser = service.save(user);
}
post method, put method에서 form data가 아닌 json, xml같은 object 형태 데이터를 받으려면 @RequestBody를 선언해줘야 한다. 클라이언트에서 전달하는 데이터포맷을 일치하는 선언했던 필드에 매핑시켜 전달해준다.
지금까지는 GET이나 POST나 똑같이 응답코드 200이 왔다. 가능하면 응답코드가 구분되면 좋겠다.
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = service.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
이제 POSTMAN에서 POST를 하면 201 상태코드를 반환한다.
서버로부터 적당한 결과값에 따른 status code를 전달해주는 것이 좋은 코드이다.
생성된 아이디를 클라이언트가 알기 위해서는 서버에서 또 물어봐야하는데, POST method결과값으로 아이디를 주면 네트워크를 줄일 수 있다.
ServletUriComponentBuilder 참고글
좋은 REST API를 만드는데 매우 중요한듯하다!
존재하지 않는 데이터를 조회하는데 status code가 200 ok이다. 이는 문제가 될 수 있다.
// GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
return service.findOne(id);
}
이 함수를 고쳐서 결과 값이 null이면 다르게 작동하게 해보자. 기존 코드를 나누기 위해 return 문에서 Ctrl + Alt + v를 눌러보자.
UserController
// GET /users/1 .. /users/10 -> 서버에는 String으로 전달됨
@GetMapping("/users/{id}")
public User retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[$s] not found", id));
}
return user;
}
UserNotFoundException
package com.example.restfulwebservice.user;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
extends throwable이 아닌 RuntimeException을 상속받았다.
이제 없는 데이터를 조회하려하면 500번대 코드를 내놓는다. 하지만 문제는 서버에 어떤 코드에서 문제가 발생했는지 다 보여주는 것은 적절하지 않다.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
앞으로 이 예외가 발생하면 500번대가 아닌 404번 에러를 보여준다.
ExceptionResponse
package com.example.restfulwebservice.exception;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExceptionResponse {
private Date timestamp;
private String message;
private String details;
}
모든 컨트롤러에서 사용할 수 있는 일반화된 예외 클래스다.
ResponseEntityHandler라는 클래스를 상속받은 HandlerClass를 만들건데, 예외가 발생할 때 handler 예외 클래스가 발생하도록 해줄 것이다. 스프링에서 모든 비즈니스 로직에서 공통적으로 처리해줘야 할 것이 있다면 AOP를 사용해서 처리해줄 수 있다. 예외 처리 핸들러도 AOP 기능을 이용할 것이다.
CustomizedResponseEntityExceptionHandler
package com.example.restfulwebservice.exception;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestController
@ControllerAdvice
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
ControllerAdvice라는 어노테이션을 추가하면 모든 컨트롤러가 실행될 때, 이 빈이 실행된다. 에러가 발생하면 Handler가 실행되는 것이다. 즉 모든 예외를 여기서 처리하는 것이다.
없는 데이터를 조회하면 이렇게 보여진다.
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundExceptions(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
이 함수를 추가하면 UserNotFoundExceptoin에 대해서는 이 클래스가 처리한다.
UserDaoService
public User deleteById(int id) {
Iterator<User> iterator = users.iterator();
while (iterator.hasNext()) {
User user = iterator.next();
if (user.getId() == id) {
iterator.remove();
return user;
}
}
return null;
}
사용자 삭제를 위한 함수를 추가해준다. 여기에서는 iterator를 이용해봤다.
UserController
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
User user = service.deleteById(id);
if (user == null) {
throw new UserNotFoundException(String.format("ID[%s] not found", id));
}
}
삭제를 하고 나서 200번대를 받는다.