ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 인 액션] Chapter 6 - REST 서비스 생성하기 :: REST 컨트롤러 작성하기
    개발서적읽기/Spring in Action 제 5판 2020. 8. 11. 00:09



    이번 장에서는 스프링을 사용해서 타코 클라우드 애플리케이션에 REST API를 제공할 것이다.


     우선, 몇 가지 새로운 스프링 MVC 컨트롤러의 작성부터 시작한다.




    ■서버에서 데이터 가져오기


    http://localhost:8080/design/recent


    위 경로로, 가장 최근에 생성된 타코의 내역을 보여주는 REST API를 생성해보자.


    hateoas 형식으로 REST API return type을 사용할 것이기 때문에


    아래와 같이 관련 dependency를 추가하자.

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

    그리고 DesignTacoController 클래스를 정의하자.


    DesignTacoController 클래스는 원래 html view를 return 하는 목적으로


    사용되었던 클래스다.


    하지만 이번 장에서는 REST API를 위한 컨트롤러로 탈바꿈시킬 것이다.

    package tacos.web;
    /*
    * @USER JungHyun
    * @DATE 2020-08-10
    * @DESCRIPTION
    */

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Sort;
    import org.springframework.hateoas.EntityLinks;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import tacos.Taco;
    import tacos.data.TacoRepository;

    @RestController
    @RequestMapping(path = "/design", produces = "application/json")
    @CrossOrigin(origins = "*")
    public class DesignTacoController {
    private TacoRepository tacoRepo;

    @Autowired
    EntityLinks entityLinks;

    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();
    }
    }

    @RestController 애노테이션은 다음 두 가지를 지원한다.


    우선 @Controller나 @Service와 같이 스트레오타입 애노테이션으로, 


    이 애노테이션이 지정된 클래스는 스프링의 컴포넌트 검색의 대상이 된다.


    그리고 컨트롤러의 모든 HTTP 요청 처리 메서드에서 HTTP 응답 몸체에 


    직접 쓰는 값을 반환한다는 것을 스프링에게 알려준다. 


    따라서 반환값이 뷰를 통해 html로 변환되지 않고 직접 HTTP 응답으로


    브라우저에 전달되어 나타난다.


    또는 일반적인 스프링 MVC 컨트롤러처럼 DesignTacoController 클래스에


    @Controller를 사용할 수도 있다. 그러나 이 때는 이 클래스의 모든 요청


    처리 메서드에 @ResponseBody 애노테이션을 지정해야만


    @RestController와 같은 결과를 얻을 수 있다.


    DesignTacoController에는 /design 경로의 요청을 처리하도록 


    @RequestMapping 어노테이션이 지정되었다.


    그리고 recentTacos() 메서드에는 


    /recent 경로의 GET 요청을 처리하는 @GetMapping이 지정되었다.


    따라서 recentTacos() 메서드에서는 /design/recent 경로의 GET 요청을 처리해야 한다.


    produces


    @RequestMapping 애노테이션에는 produces 속성도 설정되어 있다.


    이것은 DesignTacoController의 메서드는 요청의 Accept 헤더에 


    "application/json"이 포함된 요청만을 처리한다는 것을 나타낸다.


    이 경우 응답 결과는 JSON 형식이 되지만, produces 속성의 값은 String 배열로 저장된다.


    따라서 다른 컨트롤러에서도 요청을 처리할 수 있도록 


    JSON이 아닌 다른 콘텐트 타입을 같이 지정할 수 있다.


    예를 들어, XML로 출력하고자 할 때는 


    다음과 같이 "text/html"을 produces 속성에 추가하면 된다.

    @RequestMapping(path = "/design", produces = {"application/json", "text/xml"})

    @CrossOrigin


    DesignTacoController 클래스에는 @CrossOrigin 애노테이션이 지정되어 있다.


    REST API를 호출하는 클라이언트는 REST API와 별도의 도메인


    (호스트와 포트 모두, 혹은 둘 중 하나만이라도 다른)을 가지고 있다.


    웹 브라우저는 현재의 도메인과 다른 도메인을 호출하는 것을 막으므로


    이 제한을 극복할 필요가 있다.


    이러한 제약은 서버 응답에 CORS 헤더를 포함시켜 해결할 수 있으며,


    스프링에서는 @CrossOrigin 애노테이션을 지정하여 쉽게 CORS를 적용할 수 있다.


    즉, @CrossOrigin은 다른 도메인의 클라이언트에서 


    해당 REST API를 사용할 수 있게 해주는 스프링 애노테이션이다.


    recentTacos()


    recentTacos() 메서드의 로직은 간단하다.


    우선 최근 생성 일자 순으로 정렬된 처음 12개의 결과를 갖는 


    첫 번째 페이지만 원한다는 것을 PageRequest 객체에 지정한다.


    즉, 가장 최근에 생성된 12개의 타코 디자인을 원한다는 의미다.


    그다음에 TacoRepository의 findAll()의 인자로 PageRequest 객체가 전달되어


    호출된 후 결과 페이지의 콘텐츠가 클라이언트에게 반환된다.


    이 반환된 결과는 사용자에게 보여줄 모델 데이터로 사용된다.


    타코 ID로 특정 타코만 가져오는 엔드포인트를 제공하고 싶다면 어떻게 하면 될까?


    이때는 메서드의 경로에 플레이스홀더 변수를 지정하고 해당 변수를 통해 ID를


    인자로 받는 메서드를 DesignTacoController에 추가하면 된다.


    그러면 이 메서드에서 해당 ID를 사용해서 리퍼지터리의 특정 객체를 찾을 수 있다.

    @GetMapping("/{id}")
    public Taco tacoById(@PathVariable("id") Long id) {
    Optional<Taco> optTaco = tacoRepo.findById(id);
    if (optTaco.isPresent()) {
    return optTaco.get();
    }
    return null;
    }

    {id}는 플레이스홀더 라고 부른다.


    @PathVariable에 의해 {id} 플레이스홀더와 대응되는 id 매개변수에


    해당 요청의 실제 값이 지정된다.


    id와 일치하는 타코가 없다면 null을 반환한다.


    하지만 이는 좋은 방법이 아니다.


    null인데도 클라이언트는 200 코드를 받기 때문이다.


    따라서 아래와 같이 고치자.

    @GetMapping("/{id}")
    public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
    Optional<Taco> optTaco = tacoRepo.findById(id);
    if (optTaco.isPresent()) {
    return ResponseEntity.ok(optTaco.get());
    }
    return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }

    API를 테스트할 때는 curl이나 HTTPie를 사용해도 된다.


    - curl localhost:8080/design/recent


    - http :8080/design/recent


    지금까지는 정보 반환만 하는 엔드포인트 API를 정의하였다.


    그러나 API가 클라이언트로부터 데이터를 받아야 한다면 어떻게 해야 할까?


    요청의 입력 데이터를 처리하는 컨트롤ㄹ러 메서드를 작성해보자.




    ■서버에 데이터 전송하기


    타코 디자인 데이터를 요청하고 저장하는 메서드를 DesignTacoController에 추가하자.

    @PostMapping(consumes = "application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public Taco postTaco(@RequestBody Taco taco) {
    return tacoRepo.save(taco);
    }

    consumes 속성은 content-type이 application/json과 


    일치하는 요청만 처리한다는 것을 의미한다.


    그리고 taco 매개변수에는 @RequestBody가 지정되었다.


    이것은 요청 몸체의 JSON 데이터가 Taco 객체로 변환되어 


    taco 매개변수와 바인딩된다는 것을 나타낸다.


    @RequestBody 애노테이션은 중요하다.


    이것이 지정되지 않으면 매개변수(쿼리 매개변수나 폼 매개변수)가


    곧바로 Taco 객체와 바인딩되는 것으로 스프링 MVC가 간주하기 때문이다.


    여기서는 새로운 Taco 객체를 생성하기 위해 @PostMapping을 사용했지만


    변경할 때도 사용할 수 있다.


    그렇지만 일반적으로 POST 요청은 데이터 생성에 사용되고


    변경 시에는 PUT이나 PATCH 요청이 사용된다.


    지금부터는 @PutMapping과 @PatchMapping을 사용해서 


    데이터를 변경하는 방법을 알아보자.




    ■서버의 데이터 변경하기


    데이터를 변경하기 위한 HTTP 메서드로는 PUT과 PATCH가 있다.


    PUT은 데이터를 변경하는 데 사용되기는 하지만, 실제로는 GET과 반대의 의미를 갖는다.


    - GET 요청은 서버로부터 클라이언트로 데이터를 전송


    - PUT 요청은 클라이언트로부터 서버로 데이터를 전송


    이러한 관점에서 PUT은 데이터 전체를 교체하는 것이며


    반면에 HTTP PATCH의 목적은 데이터의 일부분을 변경하는 것이다.


    예를 들어, 특정 주문 데이터의 주소를 변경하고 싶다고 하자.


    REST API를 통해서 이렇게 할 수 있는 한 가지 방법은 


    다음과 같이 PUT 요청을 처리하는 것이다.

    @PutMapping("/{orderId}")
    public Order putOrder(@RequestBody Order order) {
    return repo.save(order);
    }

    그러나 이 경우는 클라이언트에서 해당 주문 데이터 전체를 PUT 요청으로 제출해야 한다.


    PUT은 해당 URL에 이 데이터를 쓰라는 의미이므로 


    이미 존재하는 해당 데이터 전체를 교체한다.


    그리고 만일 해당 주문의 속성이 생략되면 이 속성의 값은 null로 변경된다.


    따라서 주문에 관련된 주소만 변경할지라도 해당 주문에 포함된 여러 개의


    타코 데이터들이 같이 제출되어야 한다. 


    그렇지 않으면 타코 데이터들이 삭제되기 때문이다.


    그렇다면 데이터의 일부만 변경하고자 할 때는 어떻게 요청을 처리해야 할까?


    바로 이때 HTTP PATCH 요청과 스프링의 @PatchMapping을 사용한다.

    @PatchMapping(value = "/{orderId}", consumes = "application/json")
    public Order patchOrder(@PathVariable("orderId") Long orderId, @RequestBody Order patch) {
    Order order = orderRepo.findById(orderId);
    if (patch.getDeliveryName() != null) {
    order.setDeliveryName(patch.getDeliveryName());
    }
    //그리고 다른 필드들도 null 체크 후 set
    return orderRepo.save(order);
    }

    주석으로 처리하긴 했지만, patchOrder()는 putOrder()보다 실행 코드가 더 많다.


    @PatchMapping과 @PutMapping을 비롯해서 스프링 MVC의 애노테이션들은


    어떤 종류의 요청을 메서드에서 처리하는지만 나타내며


    해당 요청이 어떻게 처리되는지는 나타내지 않는다.


    따라서 PATCH가 부분 변경의 의미를 내포하고 있더라도 


    실제로 변경을 수행하는 메서드 코드는 수동으로 작성해야 한다.


    putOrder()의 경우는 HTTP PUT의 의미대로 한 주문 전체 데이터를 받고 저장한다.


    그러나 HTTP PATCH의 의미를 따르는 patchingMapping()에서는 


    데이터의 일부만 변경하기 위한 로직이 필요하다.


    즉 해당 주문 데이터를 전송된 Order 객체로 완전히 교체하는 대신


    Order 객체의 각 필드 값이 null이 아닌지 확인하고 기존 주문 데이터에 변경해야 한다.


    이 방법을 사용하면 클라이언트에서 변경할 속성만 전송하면 된다.


    그리고 서버에서는 클라이언트에서 지정하지 않은 속성의 기존 데이터를 그대로


    보존할 수 있다.


    PATCH를 구현하는 방법은 다양하다.


    patchOrder()에 PATCH를 적용하는 방법은 다음 주 가지 제약이 있다.


    - 만일 특정 필드의 데이터를 변경하지 않는다는 것을 나타내기 위해 


    null 값이 사용된다면, 해당 필드를 null로 변경하고 싶을 때 


    클라이언트에서 이를 나타낼 수 있는 방법이 필요하다.


    - 컬렉션에 저장된 항목을 삭제, 추가할 방법이 없다.


    따라서 클라이언트가 컬렉션의 항목을 삭제, 추가하려면

    변경될 컬렉션 데이터 전체를 전송해야 한다.


    PATCH 요청을 처리하는 방법이나 수신 데이터의 형식에 관해


    반드시 지켜야 할 규칙은 없다. 따라서 클라이언트는 실제 도메인 데이터를


    전송하는 대신 PATCH에 적용할 변경사항 명세를 전송할 수 있다.


    물론 이때는 도메인 데이터 대신 PATCH 명세를 처리하도록


    요청 처리 메서드가 구현되어 있어야 한다.


    지금까지 @GetMapping과 @PostMapping을 사용해서 


    데이터를 가져오거나 쓰는 방법을 알아보았다.


    그리고 @PutMapping과 @PatchMapping을 사용해서 


    데이터를 변경하는 방법도 알아보았다.


    이제 삭제하는 요청을 처리해보자.




    ■서버에서 데이터 삭제하기


    데이터를 그냥 삭제할 때는 클라이언트에서 HTTP DELETE 요청으로 삭제를 요청하면 된다.


    이때는 DELETE 요청을 처리하는 메서드에 스프링 MVC의 @DeleteMapping을 지정한다.

    @DeleteMapping("/{orderId}")
    @ResponseStatus(code = HttpStatus.NO_CONTENT)
    public void deleteOrder(@PathVariable Long orderId) {
    try {
    orderRepo.deleteById(orderId);
    } catch (EmptyResultDataAccessException e) {
    }
    }

    orderId에 해당하는 주문이 존재하면 삭제하되, 없으면 Exception이 발생한다.


    여기서는 Exception을 catch한 후 아무 것도 하지 않는다.


    설사 존재하지 않는 주문 데이터를 삭제하려다가 예외가 생겨도 


    정상적으로 존재하는 주문이 삭제된 것처럼 특별히 할 것이 없기 때문이다.


    물론 이렇게 하는 대신에 null로 지정된 ResponseEntity와 


    'NOT FOUND' HTTP 상태 코드를 deleteOrder()에서 반환하게 할 수도 있다.


    이 메서드에서는 @ResponseStatus가 지정되어 있다.


    이것은 응답의 HTTP 상태 코드가 204(NO CONTENT)가 되도록 하기 위해서다.


    이 메서드는 주문 데이터를 삭제하는 것이므로 


    클라이언트에게 데이터를 반환할 필요가 없다.


    따라서 대개 DELETE 요청의 응답은 몸체 데이터를 갖지 않으며


    반환 데이터가 없다는 것을 클라이언트가 알 수 있게 HTTP 상태 코드를 사용한다.


    이제는 기본적인 타코 클라우드 API가 구현되었으므로


    클라이언트 코드에서 이 API를 사용하여, 식자재를 보여주거나 주문을 받거나


    최근 생성된 타코를 보여줄 수 있다.


    그러나 클라이언트에서 이 API를 더 쉽게 사용할 수 있다.


    지금부터는 타코 클라우드 API에 하이퍼미디어를 추가하는 방법을 알아본다.



    댓글

Designed by Tistory.