Application/Spring Framework

[스프링 인 액션 정리] 6장, 7장. REST 서비스 생성 & 사용하기

반응형

6. REST 서비스 생성하기

  • 최근에는 모바일 장치, 태블릿, 스마트 워치, 음성 기반 장치들이 흔히 사용된다.
  • 또한 웹 브라우저 기반의 애플리케이션조차도 서버 위주로 실행되기보다는 프로세서가 있는 클라이언트에서 자바스크립트 애플리케이션으로 많이 실행된다.
  • 이처럼 클라이언트 측에서 다양한 선택을 할 수 있으므로, 많은 애플리케이션이 클라이언트에 더 다가갈 수 있는 사용자 인터페이스 설계를 적용하고 있음.
  • 또한 모든 종류의 클라이언트가 백엔드 기능과 상호작용할 수 있게 서버는 클라이언트가 필요로 하는 API를 제공.


REST 컨트롤러 작성

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

    private final TacoRepository tacoRepository;

    public DesignTacoController(TacoRepository tacoRepository) {
        this.tacoRepository = tacoRepository;
    }

    @GetMapping("/recent")
    public Iterable<Taco> recentTacos() {
        PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());
        return tacoRepository.findAll(page).getContent();
    }

}


@RestController

  • @Controller, @Service와 같이 스테레오타입 애노테이션으로, 이 애노테이션이 지정된 클래스를 스프링의 컴포넌트 검색으로 찾을 수 있다.
  • @Controller(뷰로 보여줄 값을 반환)와 다르게 컨트롤러의 모든 HTTP 요청 처리 메소드에서 HTTP body에 직접 쓰는 값을 반환한다는 것을 스프링에게 알려줌.


@CrossOrigin

  • 클라이언트는 API와 별도의 도메인(호스트와 포트 중 하나라도 다른)에서 실행 중이므로 클라이언트에서 API를 사용하지 못하도록 브라우저가 막는다.
  • 이러 제약은 서버 응답에 CORS 헤더를 포함시켜 극복할 수 있음.
  • @CrossOrigin은 다른 도메인의 클라이언트에서 해당 REST API를 사용할 수 있게 해주는 어노테이션.
@RestController
@RequestMapping(path = "/design", produces = "application/json")
@CrossOrigin(origins = "*")
public class DesignTacoController {

    private final TacoRepository tacoRepository;

    public DesignTacoController(TacoRepository tacoRepository) {
        this.tacoRepository = tacoRepository;
    }

    ...

    @GetMapping("/{id}") // GET: http://localhost:8080/design/4
    public ResponseEntity<Taco> findTacoById(@PathVariable("id") Long id) {
        Optional<Taco> optionalTaco = tacoRepository.findById(id);
        if (optionalTaco.isPresent()) {
            return new ResponseEntity<>(optionalTaco.get(), HttpStatus.OK);
        }
        return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }

    @PostMapping(consumes = "application/json") // Content-Type: application/json와 일치하는 요청만 처리.
    @ResponseStatus(HttpStatus.CREATED)
    public Taco postTaco(@RequestBody Taco taco) {
        return tacoRepository.save(taco);
    }

}


PUT vs PATCH

  • PUT
    • 데이터를 변경하는 데 사용되기는 하지만, 실제로는 GET과 반대의 의미를 갖는다.
    • 즉, GET 요청은 서버로 부터 클라이언트로 데이터를 전송하는 반면, PUT 요청은 클라이언트로부터 서버로 데이터를 전송한다.
    • 이런 관점에서 PUT은 데이터 전체를 교체하는 것.
  • PATCH
    • 데이터의 일부분을 변경하는 것.
@RestController
@RequestMapping(path = "/orders", produces = "application/json")
@CrossOrigin(origins = "*")
public class OrderApiController {

    ...

    @PutMapping(path = "/{orderId}", consumes = "application/json")
    public Order putOrder(@RequestBody Order order) {
        return repo.save(order);
    }

    @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());
        }
        ...
        return repo.save(order);
    }

}


PATCH 를 하는 방법은 여러 가지가 있다.

  • patchOrder() 메소드에 적용하는 방법은 두가지 제약을 갖는다.
    • 만일 특정 필드의 데이터를 변경하지 않는다는 것을 나타내기 위해 null 값이 사용된다면 해당 필드를 null로 변경하고 싶을 때 클라이언트에서 이를 나타낼 수 있는 방법이 필요하다.
    • 컬렉션에 저장된 항목을 삭제 혹은 추가할 방법이 없다. 따라서 클라이언트가 컬렉션의 항목을 삭제 혹은 추가하려면 변경될 컬렉션 데이터 저체를 전송해야 한다.
  • PATCH 요청을 처리하는 방법이나 수신 데이터의 형식에 관해 반드시 지켜야 할 규칙은 없다. 따라서 클라이언트는 실제 도메인 데이터를 전송하는 대신 PATCH에 적용할 변경사항 명세를 전송할 수 있다.
  • 이때 도메인 데이터 대신 PATCH 명세를 처리하도록 요청 처리 메소드가 작성되어야 한다.


하이퍼 미디어 사용

HATEOAS

  • 기본적인 API에서는 해당 API를 사용하는 클라이언트가 API의 URL Scheme을 알아야 한다.
  • API 클라이언트 코드에서는 하드코딩된 URL 패턴을 사용하고 문자욜로 처리한다. 그러나 API의 URL 스킴이 변경되면 어떻게 될까?
  • REST API를 구현하는 다른 방법은 HATEOAS(HyperMedia As the Engine Of Application State)가 있다.
  • API로 부터 반환되는 리소스에 해당 리소스와 관련된 하이퍼링크들이 포함된다. 따라서 클라이언트가 최소한의 API URL만 알면 반환되는 리소스와 관련하여 처리 가능한 다른 API URL들을 알아내어 사용할 수
    있다.
{
  "content": "Hello, World!",
  "_links": {
    "self": {
      "href": "http://localhost:8080/greeting?name=World"
      // JSON 응답에 하이퍼링크를 포함
    }
  }
}
  • 이런 형태의 HATEOAS를 HAL이라고 한다. 이것은 JSON응답에 하이퍼링크를 포함시킬 때 주로 사용되는 형식.
  • 각 요소는 _links라는 속성을 포함하는데, 이 속성은 클라이언트가 관련 API를 수행할 수 있는 하이퍼링크를 포함한다.


Spring Data REST

  • 스프링 데이터에는 애플리케이션의 API를 정의하는 데 도움을 줄 수 있는 기능도 있다.
  • 스프링 데이터 REST는 스프링 데이터의 또 다른 모듈이며, 스프링 데이터가 생성하는 Repository의 REST API를 자동 생성한다.
  • 따라서 스프링 데이터 REST를 우리 빌드에 추가하면 우리가 정의한 각 Repository 인터페이스를 사용하는 API를 얻을 수 있다.


정리

  • REST 엔드포인트는 스프링 MVC, 그리고 브라우저 지향의 컨트롤러와 동일한 프로그래밍 모델을 따르는 컨트롤러로 생성할 수 있다.
  • 모델과 뷰를 거치지 않고 요청 응답 몸체에 직접 데이터를 쓰기 위해 컨트롤러의 핸들러 메소드에는 @ResponseBody 어노테이션을 지정할 수 있으며, ResponseEntity 객체를 반환할 수 있다.
  • @RestController 어노테이션을 컨트롤러에 지정하면 해당 컨트롤러의 각 핸들러 메소드에 @ResponseBody를 지정하지 않아도 되므로 컨트롤러를 단순화 해준다.
  • 스프링 HATEOAS는 스프링 MVC에서 반환되는 리소스의 하이퍼링크를 추가할 수 있게 한다.
  • 스프링 데이터 Repository는 스프링 데이터 REST를 사용하는 REST API로 자동 노출될 수 있다.


7. REST 서비스 사용하기

  • 스프링 애플리케이션에서 API를 제공하면서 다른 애플리케이션의 API를 요청.
  • 마이크로서비스에서는 REST API를 많이 사용.
  • RestTemplate
    • 스프링 프레임워크에서 제공하는 간단하고 동기화된 REST 클라이언트.
  • Traverson
    • 스프링 HATEOAS에서 제공하는 하이퍼링크를 인식하는 동기화 REST 클라이언트로 같은 이름의 자바스크립트 라이브러리로부터 비롯.
  • WebClient
    • 스프링 5에서 소개된 반응형 비동기 REST 클라이언트


RestTemplate으로 REST 엔드포인트 사용.

  • 클라이언트 입장에서 REST 리소스와 상호작용하려면 해야 할 일이 많아서 코드가 장황해진다.
  • 저수준의 HTTP 라이브러리로 작업하면서 클라이언트는 클라이언트 인스턴스와 요청 객체를 생성하고, 해당 요청을 실행하고, 응답을 분석해 관련 되메인 객체와 연관시켜 처리해야 함.
  • 또한 발생될 수 있는 예외도 처리해야 함.
  • 이러한 장황한 코드를 피하기 위해 스프링은 RestTemplate을 제공.
  • RestTemplate은 REST 리소스를 사용하는 데 번잡한 일을 처리해 준다.


GET

@Slf4j
@Service
public class TacoCloudClient {

    private final RestTemplate restTemplate;

    public TacoCloudClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    /**
     * getForObject()의 두번째 매개변수는 응답이 바인딩 되는 타입이다.
     * 여기서는 JSON 형식인 응답 데이터가 객체로 역직렬화되시ㅓ 반환된다.
     */
    public Ingredient getIngredientById(String ingredientId) {
        return restTemplate.getForObject("http://localhost:8080/ingredients/{id}", Ingredient.class, ingredientId);
    }

    /**
     * Map을 사용해서 URL 변수들을 지정할 수 있다.
     */
    public Ingredient getIngredientById2(String ingredientId) {
        Map<String, String> urlVariables = new HashMap<>();
        urlVariables.put("id", ingredientId);
        return restTemplate.getForObject("http://localhost:8080/ingredients/{id}", Ingredient.class, urlVariables);
    }

    /**
     * URI 매개변수를 사용할 때는 URI 객체를 구성하여 getForObject()를 호출해야한다.
     */
    public Ingredient getIngredientById3(String ingredientId) {
        Map<String, String> urlVar = new HashMap<>();
        urlVar.put("id", ingredientId);
        URI uri = UriComponentsBuilder
                .fromHttpUrl("http://localhost:8080/ingredients/{id}")
                .build(urlVar);
        return restTemplate.getForObject(uri, Ingredient.class);
    }

    /**
     * 응답의 Date헤더를 확인하고 싶은 경우
     */
    public Ingredient getIngredientById4(String ingredientId) {
        ResponseEntity<Ingredient> responseEntity = restTemplate.getForEntity("http://localhost:8080/ingredients/{id}", Ingredient.class, ingredientId);
        log.info("Fetched Time: " + responseEntity.getHeaders().getDate());
        return responseEntity.getBody();
    }

}


PUT, POST, DELETE

@Slf4j
@Service
public class TacoCloudClient {

    private final RestTemplate restTemplate;

    public TacoCloudClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void updateIngredient(Ingredient ingredient) {
        restTemplate.put("http://localhost:8080/ingredients/{id}", ingredient, ingredient.getId());
    }

    public void deleteIngredient(Ingredient ingredient) {
        restTemplate.delete("http://localhost:8080/ingredients/{id}", ingredient.getId());
    }

    public Ingredient createIngredient(Ingredient ingredient) {
        return restTemplate.postForObject("http://localhost:8080/ingredients", ingredient, Ingredient.class);
    }

}
반응형