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

12536 단어 SpringSpring
  • 이 장에서 배우는 내용
    스프링 MVC에서 REST 엔드 포인트 정의하기
    하이퍼링크 REST 리소스 활성화하기
    리퍼지터리 기반의 REST 엔드포인트 자동화

6.1 REST 컨트롤러 작성하기

앵귤러 클라이언트 코드는 HTTP 요청을 통해 REST API로 통신한다.

백엔드 스프링 코드에 초점을 두고, 앵귤러는 작동을 위한 간단한 코드만 구현할 것이다.

SPA (Single Page Application)

MPA(Multi-Page Application)와 반대되는 개념으로, 프리젠테이션 계층이 백엔드 처리와 독립적이다. 따라서 같은 백엔드 기능으로 여러 사용자 인터페이스(ex. 모바일앱, PC웹)을 개발할 수 있다.
또한 다른 애플리케이션과 통합할 수 있는 기회도 제공한다.

모든 애플리케이션이 이와 같은 유연성을 필요로 하는 것은 아니므로 웹페이지에 정보를 보여주는게 전부라면 MPA가 간단할 수 있다.

2장에서 @GetMapping, @PostMapping 을 사용해서 서버에서 데이터를 가져오거나 전송했는데,
REST API에도 여전히 위 애노테이션들은 여전히 사용된다. 더불어 Spring MVC는 다양한 타입의 HTTP 요청에 사용되는 애노테이션을 제공한다.

  1. @GetMapping : HTTP GET 요청 (리소스 데이터 읽기)
  2. @PostMapping : HTTP POST 요청 (리소스 생성하기)
  3. @PutMapping : HTTP PUT 요청 (리소스 변경하기 - 전체 )
  4. @PatchMapping : HTTP PATCH 요청 (리소스 변경하기 - 부분)
  5. @DeleteMapping : HTTP DELETE 요청 (리소스 삭제하기)
  6. @RequestMapping : 다목적 요청 처리이며, HTTP 메서드가 method속성에 지정된다.

6.1.1 서버에서 데이터 가져오기 (GET)

가장 최근에 생성된 타코를 가져오는 간단한 REST 엔드포인트를 작성한다.

최근에 생성된 타코를 보여주는 코드(RecentTacosComponent)는 아래와 같다.

ngOnInit에서 주입된 http모듈로 작성된 URL에 대해 HTTP GET 요청을 수행한다.
이 경우 recentTacos 모델 변수로 참조되는 타코들의 내역이 응답에 포함된다.

그리고 recents.component.html의 뷰에서는 브라우저에 나타나는 HTML로 모델 데이터를 보여준다.

recents.component.ts

import { Component, OnInit, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'recent-tacos',
  templateUrl: 'recents.component.html',
  styleUrls: ['./recents.component.css']
})

@Injectable()
export class RecentTacosComponent implements OnInit {
  recentTacos: any;

  constructor(private httpClient: HttpClient) { }

  ngOnInit() {
  // 최근 생성된 타코들을 서버에서 가져온다.
    this.httpClient.get('http://localhost:8080/design/recent') // <1>
        .subscribe(data => this.recentTacos = data);
  }
}

이제 앵귤러 컴포넌트가 수행하는 design/recent 의 GET 요청을 수행하는 엔드포인트 코드를 살펴보자.

2장에서는 동일한 컨트롤러명으로 MPA에 사용하는 코드였다면 이번의 코드는 @RestController 어노테이션으로 나타낸 REST 컨트롤러다.

DesignTacoController.java


package tacos.web.api;
// import 생략

@RestController
@RequestMapping(path="/design",                      // <1>
                produces="application/json")
@CrossOrigin(origins="*")       		     // <2>
public class DesignTacoController {
  private TacoRepository tacoRepo;
  
  @Autowired
  EntityLinks entityLinks;

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

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

@RestController

(스테레오타입 애노테이션으로서) 이 애노테이션이 지정된 클래스를 스프링 컴포넌트 검색으로 찾을 수 있다.

컨트롤러의 모든 HTTP 처리 메서드에서 HTTP 응답 몸체에 직접 쓰는 값을 반환함을 스프링에 알려준다.

@RestController 대신 모든 메서드에 @Controller + @ResponseBody 조합을 붙이거나, ResponseEntity 객체를 반환하는 방법도 존재한다.

/design/recent 경로의 GET 요청을 처리할 때 사용되는 메서드로 앵귤러 코드가 실행될 때 필요한 기능이다.

@RequestMapping > produces 속성 : produces="application/json"

요청의 Accept 헤더에 "application/json"이 포함된 요청만을 DesignTacoController의 메서드에 처리한다는 것을 나타낸다.

이 경우 응답 결과는 JSON 형식이 되지만 produces 속성의 값은 String 배열로 저장되므로, 다른 컨트롤러에서도 요청을 처리할 수 있도록 JSON 만이 아닌 다른 콘텐트 타입을 같이 지정할 수 있다.

만약 xml로 출력하려면 produces 속성에 "text/xml"를 추가한다.

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

@CrossOrigin(origins="*")

다른 도메인의 클라이언트에서 API를 사용할 수 있게 해준다.

현재 앵귤러 코드는 API와 별도의 도메인에서 실행중이므로 앵귤러 클라이언트에서 API를 사용하지 못하게 웹 브라우저가 막는다.

이런 제약은 서버 응답에 CORS 헤더를 포함시켜 극복할 수 있고, 스프링에서는 위 애노테이션을 지정해 적용 가능하다.

recentTacos()

최근 생성일자 순으로 정렬된 처음 12개의 결과를 갖는 첫 번째 페이지만 원한다는 것을 PageRequest 객체에 지정한다.

그리고 TacoRepository의 findAll() 메서드 인자로 PageRequest 객체가 전달되어 호출된 후 결과 페이지의 콘텐츠가 클라이언트에게 반환된다.

만약 타코ID로 특정 타코만 가져오는 엔트포인트를 제공하고 싶다면, 메서드 경로에 플레이스홀더{} 변수를 지정하고 해당 변수를 통해 ID를 인자를 받는 메서드를 추가한다.

지정된 타코 ID가 없을 수 있으므로 Optional을 사용했고, 값이 없으면 null을 반환하고 있는데 이는 좋은 방법이 아니다.

콘텐츠가 없는데도 정상 처리를 나타내는 HTTP 200(OK) 상태 코드를 클라이언트가 받기 때문이다.

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

따라서 아래와 같이 HTTP 404(NOT FOUND) 상태 코드를 반환하는 것이 더 좋다.

바뀐 코드에서 Taco 객체 대신 ResponseEntity가 반환되었는데, 반환할 객체와 상태코드를 함께 보낼 수 있다.

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

개발 시에 API 테스트를 위해서 curl이나 HTTPie를 사용해도 된다.

$ curl localhost:8080/design/recent

http :8080/design/recent

6.1.2 서버에 데이터 전송하기 (POST)

이제까지는 서버에서 데이터를 받아오는 것만 했다면, 이번에는 클라이언트에서 데이터를 받는 부분을 살펴본다.

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

타코 클라우드를 SPA로 변환하기 위해 앵귤러 컴포넌트와 엔드포인트를 생성해보자.

onSubmit() 메서드는 타코 디자인 폼의 제출을 처리한다.

이번엔 post방식으로, API로부터 데이터를 가져오는 대신 API로 데이터를 전송하는 것을 의미한다.

즉, model 변수에 저장된 데이터를 API 엔드포인트로 전송한다는 것이다.

design.component.ts

onSubmit() {
  this.httpClient.post(
      'http://localhost:8080/design',
      this.model, {
          headers: new HttpHeaders().set('Content-type', 'application/json'),
      }).subscribe(taco => this.cart.addToCart(taco));

  this.router.navigate(['/cart']);
}

따라서 DesignTacoController에 디자인 데이터를 요청하고 저장하는 메서드를 추가해야 한다.

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

consumes="application/json"

Content-type이 application/json인 요청만 처리한다.

@RequestBody

요청 몸체의 JSON 데이터가 Taco 객체로 변환되어 taco 매개변수와 바인딩 된다.

@ResponseStatus(HttpStatus.CREATED)

해당 요청이 성공적이면서 요청의 결과로 리소스가 생성되면 HTTP 201(CREATED)가 전달

HTTP 200(OK)보다 더 정확한 상태 설명을 전달할 수 있다.

6.1.3 서버의 데이터 변경하기 (PUT, PATCH)

위에선 새로운 Taco 객체를 생성했다면, 이번에는 (Taco 객체를) 변경해보겠다.

데이터 변경을 위한 HTTP 메서드로 put, patch가 존재한다.

PUT은 데이터 전체를 변경할 때, PATCH는 데이터의 일부만 변경할 때 사용된다.

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

PUT을 사용한다면 해당 주문 데이터 전체를 PUT 요청으로 제출해야 한다.
뭐 하나라도 데이터가 생략되면 값이 null로 변경된다.

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

그러므로 PATCH를 사용한다.

메서드 구현부가 길어졌는데 데이터 일부만 변경하기 위한 로직으로, 각 주문의 속성이 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);
  }

patchOrder() 메서드의 제약

특정 필드의 데이터를 변경하지 않음을 나타내는 것을 null로 정한다면 해당 필드를 null로 변경하고 싶을 때 클라이언트에서 이를 표현할 방법이 필요하다.

컬렉션에 저장된 항목을 삭제 혹은 추가할 방법이 없다.
따라서 클라이언트가 컬렉션의 항목을 삭제 혹은 추가하려면 변경될 컬렉션 테이터 전체를 전송해야 한다.

6.1.4 서버에서 데이터 삭제하기 (DELETE)

데이터를 삭제할 때에는 DELETE 요청을 처리하는 메서드에 @DeleteMapping을 지정한다.

예를 들어, 주문 데이터를 삭제하는 API의 컨트롤러 메서드는 다음과 같다.

이 메서드는 특정 주문 데이터를 삭제하는데, 해당 주문이 존재하지 않으면 Empty ResultDataAccessException이 발생된다.

@ResponseStatus(HttpStatus.NO_CONTENT)

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

응답의 상태코드가 204(NO CONTENT)가 된다.
(주문 데이터를 삭제하는 것이므로 클라이언트에게 데이터를 반환할 필요가 없기 때문)

6.2 하이퍼미디어 사용하기

지금까지는 클라이언트가 API 요청을 위해 URL스킴을 알아야 했다.

때문에 클라이언트 코드에서 하드코딩된 URL 패턴을 사용했는데, 만약 API의 URL 스킴이 변경되면 클라이언트 코드는 정상적으로 실행되지 않을 것이다.

HATEOAS

REST API를 구현하는 또 다른 방법으로 HATEOAS(Hypermedia As The Engine Of Application)이 있다.
여기엔 API로부터 반환되는 리소스에 해당 리소스와 관련된 하이퍼링크들이 포함되어있다.

클라이언트가 최소한의 API URL만 알면 다른 API URL들을 알아내 사용할 수 있다.

클라이언트가 최근 생성된 타코 리스트를 요청했다고 하자.

하이퍼링크가 없는 형태의 API 요청은 (JSON 형식으로) 타코 관련 정보만 클라이언트에서 수신될 것이다.

하이퍼링크가 없는 타코 리스트

{
	{
		"id": 4,
		"name":"Veg-Out",
		"createdAt":"2018-01-31T20:15:53.219+0000",
		"ingredients": [
			{"id": "FLTO", "name": "Flour Totila", "type": "WRAP"},
			...
		]
	},
	...
}

하이퍼링크를 포함한 타코 리스트

{
 "_embedded": {
   "tacoResourceList": [{
     "name": "Veg-Out",
     "createdAt": "2018-01-31T20:15:53.219+0000",
     "ingredients": [{
       "name": "Flour Tortilla", "type": "WRAP",
       "_links": {
         "self": { "href": "http://localhost:8080/ingredients/FLTO" }
       }
     },
     {
       "name": "Corn Tortilla", "type": "WRAP",
       "_links": {
         "self": { "href": "http://localhost:8080/ingredients/COTO" }
       }
     }],
     "_links": {
       "self": { "href": "http://localhost:8080/design/4" }
     }
   },
   { // 다른 taco
 ...
 ]}, // taco 끝
 "_links": {
   "recents": {
     "href": "http://localhost:8080/design/recent"
   }
 }
}

이런 형태의 HATEOAS를 HAL이라고 하며, JSON응답에 하이퍼링크를 포함시킬 떄 주로 사용된다.

"_link"

클라이언트가 관련 API를 수행할 수 있는 하이퍼링크 포함(self, recent)
self는 특정 타코 경로, recent는 현재 접근한 recent 경로를 표출한다.

따라서 특정 타코에 대해 HTTP 요청을 수행할 때 해당 타코 리소스의 URL을 지정하지 않아도 된다. 원하는 self링크를 요청하면 된다.

Spring HATEOAS를 사용하기 위해서는 아래의 의존성을 pom.xml에 추가하고 처리 메소드에서 반환시 도메인 객체가 아닌 EntityModel 객체(EntityModel, CollectionModel)를 반환하면 된다.

요약

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

좋은 웹페이지 즐겨찾기