📕 Book/스프링 인 액션

[스프링 인 액션] 6장 REST 서비스 생성하기

최리 2021. 7. 15. 23:00

6장 REST 서비스 생성하기

💻 실습 : https://github.com/cusbert/spring-in-action-5th

🎯 이 장에서 배우는 내용

  • 스프링 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);
    }

}

Rest API controller 기본 템플릿

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 로 자동 노출 될 수 있다.

참고

예제 코드 수정할 때 참고

반응형