[스프링 인 액션] 6장 REST 서비스 생성하기
- 📕 Book/스프링 인 액션
- 2021. 7. 15.
6장 REST 서비스 생성하기
💻 실습 : https://github.com/cusbert/spring-in-action-5th
- Front 코드와 Backend 코드 분리
- Front-end
- tacocloud-ui directory 따로 생성
https://github.com/cusbert/spring-in-action-5th/tree/main/tacocloud-ui
- tacocloud-ui directory 따로 생성
- Back-end
- HATEOAS 실습 생략
https://github.com/cusbert/spring-in-action-5th/tree/main/ch6/taco-cloud - HATEOAS 포함한 교재에서 제공한 샘플 코드는 실행되도록 수정하여 따로 directory 생성
https://github.com/cusbert/spring-in-action-5th/tree/main/ch6-original
- HATEOAS 실습 생략
🎯 이 장에서 배우는 내용
- 스프링 MVC에서 REST 엔드포인트 정의하기
- 하이퍼링크 REST 리소스 활성화 하기
- 리퍼지터리 기반의 REST 엔드포인트 자동화
6.1 REST 컨트롤러 작성하기
- 스프링 MVC 의 HTTP 요청-처리 어노테이션
HTTP | 용도 | |
---|---|---|
@GetMapping |
HTTP GET 요청 | 리소스 데이터 읽기 |
@PostMapping |
HTTP POST 요청 | 리소스 생성 |
@PutMapping |
HTTP PUT 요청 | 리소스 변경 |
@PatchMapping |
HTTP PATCH 요청 | 리소스 변경 |
@DeleteMapping |
HTTP DELETE | 리소스 삭제 |
@RequestMapping |
다목적 요청 처리 |
@RestController
@RequestMapping("/foos")
class FooController {
@Autowired
private IFooService service;
@GetMapping
public List<Foo> findAll() {
return service.findAll();
}
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id) {
return RestPreconditions.checkFound(service.findById(id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Long create(@RequestBody Foo resource) {
Preconditions.checkNotNull(resource);
return service.create(resource);
}
@PutMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public void update(@PathVariable( "id" ) Long id, @RequestBody Foo resource) {
Preconditions.checkNotNull(resource);
RestPreconditions.checkNotNull(service.getById(resource.getId()));
service.update(resource);
}
@DeleteMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public void delete(@PathVariable("id") Long id) {
service.deleteById(id);
}
}
public class RestPreconditions {
public static <T> T checkFound(T resource) {
if (resource == null) {
throw new MyResourceNotFoundException();
}
return resource;
}
}
6.1.1 데이터 조회
데이터를 조회 할 때는 @GetMapping
를 사용한다.
@RestController
@RequestMapping(path = "/design", produces = "application/json")
@CrossOrigin(origins = "*")
public class DesignTacoController {
private TacoRepository tacoRepo;
public DesignTacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
// 페이지 목록
@GetMapping("/recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
// 아이템 1개 조회
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
Optional<Taco> optTaco = tacoRepo.findById(id);
if (optTaco.isPresent()) {
return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
}
@RestController
: Controller의 모든 HTTP 요청 처리 메서드에서 HTTP 응답을 Response Body에 직접 반환한다. 따라서 반환값이 뷰를 통해 HTML 로 변환되지 않고 직접 HTTP 응답으로 브라우저에 전달되어 내려간다.
6.1.2 데이터 전송
데이터를 입력할 때는 클라이언트에서 @PostMapping
으로 데이터를 전송한다.
@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Order postOrder(@RequestBody Order order) {
return repo.save(order);
}
@RequestBody
: Request Body에 Json 데이터가 Order 객체로 변환 되어 Order 매게 변수에 바인딩 된다.@PostMapping(consumes="application/json")
: application/json 만 처리@ResponseStatus(HttpStatus.CREATED)
: HTTP 201(CREATE) 전달, HTTP 200 OK 보다 명확하다.
6.1.3 데이터 변경
데이터를 변경하기 위한 HTTP 메소드로는 PUT 과 PATCH가 있다.
- PUT 은 데이터 전체를 교체한다.
- PATCH 는 데이터의 일부분을 변경하는 것이다.
@PutMapping(path="/{orderId}", consumes="application/json")
public Order putOrder(@RequestBody Order order) {
return repo.save(order);
}
PUT은 해당 URL 에 이 데이터를 쓰라는 의미이므로 이미 존재하는 해당 데이터 전체를 교체한다. 만약 해당 주문의 속성이 생략되면 이 속성의 값은 null 이 된다.
@PatchMapping(path="/{orderId}", consumes="application/json")
public Order patchOrder(@PathVariable("orderId") Long orderId,
@RequestBody Order patch) {
Order order = repo.findById(orderId).get();
if (patch.getDeliveryName() != null) {
order.setDeliveryName(patch.getDeliveryName());
}
if (patch.getDeliveryStreet() != null) {
order.setDeliveryStreet(patch.getDeliveryStreet());
}
if (patch.getDeliveryCity() != null) {
order.setDeliveryCity(patch.getDeliveryCity());
}
if (patch.getDeliveryState() != null) {
order.setDeliveryState(patch.getDeliveryState());
}
if (patch.getDeliveryZip() != null) {
order.setDeliveryZip(patch.getDeliveryState());
}
if (patch.getCcNumber() != null) {
order.setCcNumber(patch.getCcNumber());
}
if (patch.getCcExpiration() != null) {
order.setCcExpiration(patch.getCcExpiration());
}
if (patch.getCcCVV() != null) {
order.setCcCVV(patch.getCcCVV());
}
return repo.save(order);
}
따라서 데이터의 일부만 변경하고자 할 때는 PATCH를 사용해야 한다.
해당 주문 데이터를 전송된 Order 객체로 완전히 교체하는 대신 Order 객체의 각 필드 값이 null 이 아닌지 확인하고 기존 데이터를 변경해야한다. 이 방법을 사용하면 클라이언트에서 변경할 속성만 전송하면 된다. 그리고 서버에서는 클라이언트에서 지정하지 않은 속성의 기존 데이터를 그대로 보전할 수 있다.
6.1.4 데이터 삭제
데이터를 삭제할 때는 @DeleteMapping
을 사용한다.
@DeleteMapping("/{orderId}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 20
public void deleteOrder(@PathVariable("orderId") Long orderId) {
try {
repo.deleteById(orderId);
} catch (EmptyResultDataAccessException e) {}
}
@ResponseStatus(HttpStatus.NO_CONTENT)
: 삭제 성공시 HTTP 204 NO_CONTENT 전송한다. 대개의 경우 DELETE 요청 응답은 response body를 가지지 않는다.EmptyResultDataAccessException
: 정상적으로 삭제할 데이터가 없다면 EmptyResultDataAccessException를 발생 시킨다. 단 예외가 발생하여도 특별히 할 일은 없다.
📌 요약
- REST 엔드포인트는 스프링 MVC, 그리고 브라우저 지향의 컨트롤러와 동일한 프로그래밍 모델을 따르는 컨트롤러로 생성할 수 있다.
- 모델과 뷰를 거치지 않고 요청 응답 몸체에 직접 데이터를 쓰기 위해 컨트롤러의 핸들러 메소드에는
@ResponseBody
어노테이션을 지정할 수 있으며,ResponseEntity
객체를 반환 할 수 있다. @RestController
어노테이션을 컨트롤러에 지정하면 해당 컨트롤러의 각 핸들러 메서드에@ResponseBody
를 지정하지 않아도 되므로 컨트롤러를 단순화해 준다.- 스프링 HATEOAS는 스프링 MVC에서 반환되는 리소스의 하이퍼링크를 추가 할 수 있게 한다.
- 스프링 데이터 리퍼지터리는 스프링 데이터 REST 를 사용하는 REST API 로 자동 노출 될 수 있다.
참고
- 스프링 인 액션 5판
- https://www.baeldung.com/building-a-restful-web-service-with-spring-and-java-based-configuration
예제 코드 수정할 때 참고
반응형