chap 6 - JAVA-JIKIMI/SPRING-IN-ACTION-5 GitHub Wiki

2๋ถ€์˜ 6์žฅ-9์žฅ์€ ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ฃผ๋Š” ์ฃผ์ œ๋ฅผ ๋‹ค๋ฃฌ๋‹ค.

6์žฅ์—์„œ๋Š” ์Šคํ”„๋ง์—์„œ REST API๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๋ฉด์„œ 2์žฅ์—์„œ ์‹œ์ž‘ํ–ˆ๋˜ ์Šคํ”„๋ง MVC๋ฅผ ๋” ํญ๋„“๊ฒŒ ๋‹ค๋ฃฐ ๊ฒƒ์ด๋‹ค.


์ด ์žฅ์—์„œ ๋ฐฐ์šฐ๋Š” ๋‚ด์šฉ

  • ์Šคํ”„๋ง MVC์—์„œ REST ์—”ํŠธํฌ์ธํŠธ ์ •์˜ํ•˜๊ธฐ
  • ํ•˜์ดํผ๋งํฌ REST ๋ฆฌ์†Œ์Šค ํ™œ์„ฑํ™”ํ•˜๊ธฐ
  • ๋ฆฌํผ์ง€ํ„ฐ๋ฆฌ ๊ธฐ๋ฐ˜์˜ REST ์—”๋“œํฌ์ธํŠธ ์ž๋™ํ™”

์ด๋ฒˆ ์žฅ์—์„œ๋Š”...

์Šคํ”„๋ง์„ ์‚ฌ์šฉํ•ด ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ ์•ฑ์— REST API๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

  • rest controller๋ฅผ ์ž‘์„ฑ
  • ํ•˜์ดํผ๋ฏธ๋””์–ด๋ฅผ ์‚ฌ์šฉํ•ด API์—์„œ url ๋ชฉ๋ก์„ ๋ฐ›์•„์˜จ๋‹ค
  • ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ํ™œ์„ฑํ™”

II์˜ ๋‚ด์šฉ์€ III์—์„œ ๋” ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋กœ ๋Œ€์ฒด๋œ๋‹ค. ํ•˜์ดํผ๋งํฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณธ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ž.

I. REST ์ปจํŠธ๋กค๋Ÿฌ ์ž‘์„ฑํ•˜๊ธฐ

์•ต๊ทค๋Ÿฌ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋กœ HTTP ์š”์ฒญ์„ ํ†ตํ•ด REST API๋กœ ํ†ต์‹ 

์ด๋ฒˆ ์žฅ์—์„œ๋Š” ์•ต๊ทค๋Ÿฌ ๊ธฐ๋ฐ˜์˜ UI์™€ ํ†ต์‹ ํ•˜๋Š” REST API๋ฅผ ์ƒ์„ฑํ•˜์ž. (์ถ”ํ›„, 2์žฅ์—์„œ ์ƒ์„ฑํ–ˆ๋˜ ์„œ๋ฒ„ ์ƒ์„ฑ ํŽ˜์ด์ง€๋ฅผ ์•ต๊ทค๋Ÿฌ ํ”„๋ ˆ์ž„์›Œํฌ, SPA๋กœ ๋Œ€์ฒดํ•˜๊ฒŒ ๋œ๋‹ค.)

์ด ์ฑ…์˜ ๋ชฉ์ ์€ ์•ต๊ทค๋Ÿฌ๋ฅผ ๋ฐฐ์šฐ๋Š”๊ฒŒ ์•„๋‹ˆ๋ฏ€๋กœ ๋ฐฑ์—”๋“œ ์Šคํ”„๋ง ์ฝ”๋“œ์— ์ดˆ์ ์„ ๋‘๊ณ , ์•ต๊ทค๋Ÿฌ๋Š” ์ž‘๋™์„ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋งŒ ๊ตฌํ˜„ํ•  ๊ฒƒ์ด๋‹ค.

SPA (Single Page Application)

  • MPA(Multi-Page Application)์™€ ๋ฐ˜๋Œ€๋˜๋Š” ๊ฐœ๋…
  • ํ”„๋ฆฌ์  ํ…Œ์ด์…˜ ๊ณ„์ธต์ด ๋ฐฑ์—”๋“œ ์ฒ˜๋ฆฌ์™€ ๋…๋ฆฝ์ 
  • ๋”ฐ๋ผ์„œ, ๊ฐ™์€ ๋ฐฑ์—”๋“œ ๊ธฐ๋Šฅ์œผ๋กœ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค(ex. ๋ชจ๋ฐ”์ผ์•ฑ, PC์›น)์„ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋˜ํ•œ ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐํšŒ๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ด์™€ ๊ฐ™์€ ์œ ์—ฐ์„ฑ์„ ํ•„์š”๋กœ ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋ฏ€๋กœ ์›นํŽ˜์ด์ง€์— ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๋Š”๊ฒŒ ์ „๋ถ€๋ผ๋ฉด MPA๊ฐ€ ๊ฐ„๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2์žฅ์—์„œ @GetMapping, @PostMapping ์„ ์‚ฌ์šฉํ•ด์„œ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ์ „์†กํ–ˆ๋Š”๋ฐ,

REST API์—๋„ ์—ฌ์ „ํžˆ ์œ„ ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ Spring MVC๋Š” ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ HTTP ์š”์ฒญ์— ์‚ฌ์šฉ๋˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ณตํ•œ๋‹ค.

์Šคํ”„๋ง MVC์˜ HTTP ์š”์ฒญ-์ฒ˜๋ฆฌ ์–ด๋…ธํ…Œ์ด์…˜

1. ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (GET)

๊ฐ€์žฅ ์ตœ๊ทผ์— ์ƒ์„ฑ๋œ ํƒ€์ฝ”๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฐ„๋‹จํ•œ REST ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ๋‹ค.

recents.component.ts

์ตœ๊ทผ์— ์ƒ์„ฑ๋œ ํƒ€์ฝ”๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์ฝ”๋“œ(RecentTacosComponent)๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

  • ngOnInit์—์„œ ์ฃผ์ž…๋œ http๋ชจ๋“ˆ๋กœ ์ž‘์„ฑ๋œ URL์— ๋Œ€ํ•ด HTTP GET ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.
  • ์ด ๊ฒฝ์šฐ recentTacos ๋ชจ๋ธ ๋ณ€์ˆ˜๋กœ ์ฐธ์กฐ๋˜๋Š” ํƒ€์ฝ”๋“ค์˜ ๋‚ด์—ญ์ด ์‘๋‹ต์— ํฌํ•จ๋œ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  recents.component.html์˜ ๋ทฐ์—์„œ๋Š” ๋ธŒ๋ผ์šฐ์ €์— ๋‚˜ํƒ€๋‚˜๋Š” HTML๋กœ ๋ชจ๋ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
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);
  }
}

DesignTacoController.java

์ด์ œ ์•ต๊ทค๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” design/recent ์˜ GET ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž.

  • 2์žฅ์—์„œ๋Š” ๋™์ผํ•œ ์ปจํŠธ๋กค๋Ÿฌ๋ช…์œผ๋กœ MPA์— ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ์˜€๋‹ค๋ฉด, ์ด๋ฒˆ์˜ ์ฝ”๋“œ๋Š” @RestController ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋‚˜ํƒ€๋‚ธ REST ์ปจํŠธ๋กค๋Ÿฌ๋‹ค.
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 ์˜ ์ง€์›๊ธฐ๋Šฅ

  1. (์Šคํ…Œ๋ ˆ์˜คํƒ€์ž… ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ์„œ) ์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ์ง€์ •๋œ ํด๋ž˜์Šค๋ฅผ ์Šคํ”„๋ง ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ƒ‰์œผ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.
  2. ์ปจํŠธ๋กค๋Ÿฌ์˜ ๋ชจ๋“  HTTP ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์—์„œ HTTP ์‘๋‹ต ๋ชธ์ฒด์— ์ง์ ‘ ์“ฐ๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•จ์„ ์Šคํ”„๋ง์— ์•Œ๋ ค์ค€๋‹ค.
  • @RestController ๋Œ€์‹  ๋ชจ๋“  ๋ฉ”์„œ๋“œ์— @Controller + @ResponseBody ์กฐํ•ฉ์„ ๋ถ™์ด๊ฑฐ๋‚˜, ResponseEntity ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์กด์žฌํ•œ๋‹ค.

<3>

/design/recent ๊ฒฝ๋กœ์˜ GET ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์„œ๋“œ๋กœ ์•ต๊ทค๋Ÿฌ ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์ด๋‹ค.

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="*")

๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ ํด๋ผ์ด์–ธํŠธ์—์„œ API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

ํ˜„์žฌ ์•ต๊ทค๋Ÿฌ ์ฝ”๋“œ๋Š” API์™€ ๋ณ„๋„์˜ ๋„๋ฉ”์ธ์—์„œ ์‹คํ–‰์ค‘์ด๋ฏ€๋กœ ์•ต๊ทค๋Ÿฌ ํด๋ผ์ด์–ธํŠธ์—์„œ API๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ์›น ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ง‰๋Š”๋‹ค. ์ด๋Ÿฐ ์ œ์•ฝ์€ ์„œ๋ฒ„ ์‘๋‹ต์— CORS ํ—ค๋”๋ฅผ ํฌํ•จ์‹œ์ผœ ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ๊ณ , ์Šคํ”„๋ง์—์„œ๋Š” ์œ„ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ง€์ •ํ•ด ์ ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.

recentTacos()

์ตœ๊ทผ ์ƒ์„ฑ์ผ์ž ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ์ฒ˜์Œ 12๊ฐœ์˜ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ–๋Š” ์ฒซ ๋ฒˆ์งธ ํŽ˜์ด์ง€๋งŒ ์›ํ•œ๋‹ค๋Š” ๊ฒƒ์„ PageRequest ๊ฐ์ฒด์— ์ง€์ •ํ•œ๋‹ค.
๊ทธ๋ฆฌ๊ณ  TacoRepository์˜ findAll() ๋ฉ”์„œ๋“œ ์ธ์ž๋กœ PageRequest ๊ฐ์ฒด๊ฐ€ ์ „๋‹ฌ๋˜์–ด ํ˜ธ์ถœ๋œ ํ›„ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€์˜ ์ฝ˜ํ…์ธ ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜๋œ๋‹ค.

๋งŒ์•ฝ ํƒ€์ฝ”ID๋กœ ํŠน์ • ํƒ€์ฝ”๋งŒ ๊ฐ€์ ธ์˜ค๋Š” ์—”ํŠธํฌ์ธํŠธ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด

๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ์— ํ”Œ๋ ˆ์ด์Šคํ™€๋”{} ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•˜๊ณ  ํ•ด๋‹น ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ID๋ฅผ ์ธ์ž๋ฅผ ๋ฐ›๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

์ง€์ •๋œ ํƒ€์ฝ” ID๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Optional์„ ์‚ฌ์šฉํ–ˆ๊ณ , ๊ฐ’์ด ์—†์œผ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์ด๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹ˆ๋‹ค. ์ฝ˜ํ…์ธ ๊ฐ€ ์—†๋Š”๋ฐ๋„ ์ •์ƒ ์ฒ˜๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” HTTP 200(OK) ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

// before
@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๊ฐ€ ๋ฐ˜ํ™˜๋˜์—ˆ๋Š”๋ฐ, ๋ฐ˜ํ™˜ํ•  ๊ฐ์ฒด์™€ ์ƒํƒœ์ฝ”๋“œ๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

// after
@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๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ฐœ๋ฐœ ์‹œ์— API ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ curl์ด๋‚˜ HTTPie๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.

$ curl localhost:8080/design/recent
http :8080/design/recent

2. ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ ์ „์†กํ•˜๊ธฐ (POST)

์ด์ œ๊นŒ์ง€๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ฒƒ๋งŒ ํ–ˆ๋‹ค๋ฉด, ์ด๋ฒˆ์—๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๋Š” ๋ถ€๋ถ„์„ ์‚ดํŽด๋ณด๊ฒ ๋‹ค. request์˜ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ๋ฅผ SPA๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ ์œ„ํ•ด ์•ต๊ทค๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์™€ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ƒ์„ฑํ•ด๋ณด์ž.

design.component.ts

onSubmit() ๋ฉ”์„œ๋“œ๋Š” ํƒ€์ฝ” ๋””์ž์ธ ํผ์˜ ์ œ์ถœ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

์ด๋ฒˆ์—” post๋ฐฉ์‹์œผ๋กœ, API๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  API๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

์ฆ‰, model ๋ณ€์ˆ˜์— ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ API ์—”๋“œํฌ์ธํŠธ๋กœ ์ „์†กํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

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์— ๋””์ž์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๊ณ  ์ €์žฅํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

consumes="application/json"

Content-type์ด application/json์ธ ์š”์ฒญ๋งŒ ์ฒ˜๋ฆฌํ•œ๋‹ค.

@RequestBody

์š”์ฒญ ๋ชธ์ฒด์˜ JSON ๋ฐ์ดํ„ฐ๊ฐ€ Taco ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜๋˜์–ด taco ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ๋ฐ”์ธ๋”ฉ ๋œ๋‹ค.

@ResponseStatus(HttpStatus.CREATED)

ํ•ด๋‹น ์š”์ฒญ์ด ์„ฑ๊ณต์ ์ด๋ฉด์„œ ์š”์ฒญ์˜ ๊ฒฐ๊ณผ๋กœ ๋ฆฌ์†Œ์Šค๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด HTTP 201(CREATED)๊ฐ€ ์ „๋‹ฌ

HTTP 200(OK)๋ณด๋‹ค ๋” ์ •ํ™•ํ•œ ์ƒํƒœ ์„ค๋ช…์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

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

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() ๋ฉ”์„œ๋“œ์˜ ์ œ์•ฝ

  1. ํŠน์ • ํ•„๋“œ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒƒ์„ null๋กœ ์ •ํ•œ๋‹ค๋ฉด ํ•ด๋‹น ํ•„๋“œ๋ฅผ null๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ๋•Œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ด๋ฅผ ํ‘œํ˜„ํ•  ๋ฐฉ๋ฒ•์ด ํ•„์š”ํ•˜๋‹ค.
  2. ์ปฌ๋ ‰์…˜์— ์ €์žฅ๋œ ํ•ญ๋ชฉ์„ ์‚ญ์ œ ํ˜น์€ ์ถ”๊ฐ€ํ•  ๋ฐฉ๋ฒ•์ด ์—†๋‹ค. ๋”ฐ๋ผ์„œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ปฌ๋ ‰์…˜์˜ ํ•ญ๋ชฉ์„ ์‚ญ์ œ ํ˜น์€ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด ๋ณ€๊ฒฝ๋  ์ปฌ๋ ‰์…˜ ํ…Œ์ดํ„ฐ ์ „์ฒด๋ฅผ ์ „์†กํ•ด์•ผ ํ•œ๋‹ค.

4. ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œํ•˜๊ธฐ (DELETE)

๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ์—๋Š” DELETE ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ์— @DeleteMapping์„ ์ง€์ •ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” API์˜ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

์ด ๋ฉ”์„œ๋“œ๋Š” ํŠน์ • ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š”๋ฐ, ํ•ด๋‹น ์ฃผ๋ฌธ์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด Empty ResultDataAccessException์ด ๋ฐœ์ƒ๋œ๋‹ค.

@ResponseStatus(HttpStatus.NO_CONTENT)

์‘๋‹ต์˜ ์ƒํƒœ์ฝ”๋“œ๊ฐ€ 204(NO CONTENT)๊ฐ€ ๋œ๋‹ค. (์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•  ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ)

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

II. ํ•˜์ดํผ๋ฏธ๋””์–ด ์‚ฌ์šฉํ•˜๊ธฐ

์ง€๊ธˆ๊นŒ์ง€๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ 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)๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋œ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด

  • ์Šคํ”„๋ง HATEOAS๋ฅผ ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์˜ classpath์— ์ถ”๊ฐ€
  • ์Šคํ”„๋ง HATEOAS๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ์ž๋™-๊ตฌ์„ฑ ์ œ๊ณต
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

1. ํ•˜์ดํผ๋งํฌ ์ถ”๊ฐ€ํ•˜๊ธฐ

/design/recent์— ๋Œ€ํ•œ GET ์š”์ฒญ์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” ์ตœ๊ทผ ํƒ€์ฝ” ๋ฆฌ์ŠคํŠธ์— ํ•˜์ดํผ๋ฏธ๋””์–ด ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์ž.

spring HATEOAS์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋‘ ๊ฐ€์ง€ ๊ธฐ๋ณธํƒ€์ž…์ด ์žˆ๋Š”๋ฐ, Resource์™€ Resources์ด๋‹ค.

Resource๋Š” ๋‹จ์ผ ๋ฆฌ์†Œ์Šค, Resources๋Š” ๋ฆฌ์†Œ์Šค ์ปฌ๋ ‰์…˜์„ ๋‚˜ํƒ€๋‚ด๊ณ  ๋‘ ํƒ€์ž… ๋ชจ๋‘ ์Šคํ”„๋ง MVC ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ์—์„œ ๋ฐ˜ํ™˜๋  ๋•Œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฐ›๋Š” JSON(or XML)์— ํฌํ•จ๋œ๋‹ค.

recent๋งํฌ ์ถ”๊ฐ€

์ตœ๊ทผ ์ƒ์„ฑ๋œ ํƒ€์ฝ” ๋ฆฌ์ŠคํŠธ์— ํ•˜์ดํผ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด List ๋Œ€์‹  Resources ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •ํ•œ๋‹ค.

@GetMapping("/recent")
public Resources<Resource<Taco>> recentTacos() {
    PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());

    List<Taco> tacos = tacoRepo.findAll(pageRequest).getContent();
		Resources<Resource<Taco>> recentResources = Resources.wrap(tacos); // <1>

    recentResources.add(new Link("http://localhost:8080/design/recent", "recents")); //<2>
    return recentResources;
}

<1> : ์ง์ ‘ ํƒ€์ฝ” ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ  Resources.wrap()๋ฅผ ์‚ฌ์šฉํ•ด Resouces<..>๋กœ ํƒ€์ฝ”๋ฅผ ๋ž˜ํ•‘

<2> : ์ด ์ฝ”๋“œ๋กœ ์ธํ•ด JSON์— ๋‹ค์Œ ๋งํฌ๊ฐ€ ํฌํ•จ๋œ๋‹ค.

"_links": {
  "recents": {
    "href": "http://localhost:8080/design/recent"
  }
}

ํ˜„์žฌ <2>์— ๋งํฌ๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ ๋˜์–ด ์žˆ๋‹ค.

**๋งํฌ ๋นŒ๋” ControllerLinkBuilder**๋ฅผ ์‚ฌ์šฉํ•˜์ž.

  • URL์„ ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ์•Š๊ณ  ํ˜ธ์ŠคํŠธ ์ด๋ฆ„์„ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์คŒ
  • ์ปจํŠธ๋กค๋Ÿฌ์˜ ๊ธฐ๋ณธ URL์— ๊ด€๋ จ๋œ ๋งํฌ์˜ ๋นŒ๋“œ๋ฅผ ๋„์™€์ฃผ๋Š” API ์ œ๊ณต
recentResources.add(new Link("http://localhost:8080/design/recent", "recents")); //<2>

// ์œ„์˜ ์ฝ”๋“œ์™€ ์•„๋ž˜์˜ ์ฝ”๋“œ๋Š” ๋™์ผํ•˜๋‹ค. ์ ์  ํ•˜๋“œ์ฝ”๋”ฉ์ด ์‚ฌ๋ผ์ง„๋‹ค.

// 1.
recentModels.add(
    ControllerLinkBuilderr.linkTo(DesignTacoController.class)
                     .slash("recent")
                     .withRel("recents")
    );

// 2. methodOn()๋ฅผ ์‚ฌ์šฉํ•ด ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ๋‹ค. 
recentResources.add(
        linkTo(methodOn(RecentTacosController.class).recentTacos())
        .withRel("recents"));

์ปจํŠธ๋กค๋Ÿฌ์˜ ๊ธฐ๋ณธ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ด Link ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ

slash()๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด ์Šฌ๋ž˜์‹œ(/)์™€ ์ธ์ž๋กœ ์ „๋‹ฌ๋œ ๊ฐ’์„ URL์— ์ถ”๊ฐ€

์ด ๋งํฌ์˜ ๊ด€๊ณ„์ด๋ฆ„์„ ์ง€์ •(recents)

methodOn()์„ ํ™œ์šฉํ•ด ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค ๊ฒฝ๋กœ, ๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ ๋‘˜์ค‘ ํ•˜๋‚˜๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

2. ๋ฆฌ์†Œ์Šค ์–ด์…ˆ๋ธ”๋Ÿฌ ์ƒ์„ฑํ•˜๊ธฐ

์ด๋ฒˆ์—๋Š” ๋ฆฌ์ŠคํŠธ์— ํฌํ•จ๋œ ๊ฐ ํƒ€์ฝ” ๋ฆฌ์ŠคํŠธ์— ๋Œ€ํ•œ ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์ž.

๊ฐ ํƒ€์ฝ” ๋ฆฌ์ŠคํŠธ ๋งํฌ๋ฅผ ์ผ์ผํžˆ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ˜๋ณต ๋ฃจํ”„๋ฅผ ๋Œ๋ ค๋„ ๋˜์ง€๋งŒ, ๊ฐ API์ฝ”๋“œ๋งˆ๋‹ค ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋ฒˆ๊ฑฐ๋กญ๋‹ค.

๋”ฐ๋ผ์„œ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•ด, Taco ๊ฐ์ฒด๋ฅผ TacoResource๊ฐ์ฒด(Taco + ๋งํฌ)๋กœ ๋ณ€ํ™˜ํ•˜์ž.

package tacos.web.api;
// import ์ƒ๋žต
@Relation(value="taco", collectionRelation="tacos")
public class TacoResource extends ResourceSupport {

  private static final IngredientResourceAssembler 
            ingredientAssembler = new IngredientResourceAssembler();
  
  @Getter
  private final String name;

  @Getter
  private final Date createdAt;

  @Getter
  private final List<IngredientResource> ingredients;
  
  public TacoResource(Taco taco) {
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients = 
        ingredientAssembler.toResources(taco.getIngredients()); <--๋ฃจํ”„๋ฌธ์„ ํ”ผํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ์ฝ”๋“œ
  }
  
}

TacoResource ๋Š” ResourceSupport ์˜ ์„œ๋ธŒ ํด๋ž˜์Šค๋กœ์„œ Link ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ์™€ ์ด๊ฒƒ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์†๋ฐ›๋Š”๋‹ค.

  • ๋ถˆํ•„์š”ํ•œ id์ •๋ณด๋ฅผ ๊ฐ์ถœ ์ˆ˜ ์žˆ๊ณ  ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค. (self๋งํฌ๊ฐ€ ๋ฆฌ์†Œ์Šค ์‹๋ณ„์ž ์—ญํ• )

ํƒ€์ฝ” ๋ฆฌ์†Œ์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฆฌ์†Œ์Šค ์–ด์…ˆ๋ธ”๋Ÿฌ

package tacos.web.api;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import tacos.Taco;

public class TacoResourceAssembler
       extends ResourceAssemblerSupport<Taco, TacoResource> {

  public TacoResourceAssembler() {
    super(DesignTacoController.class, TacoResource.class);
  }
  
  @Override
  protected TacoResource instantiateResource(Taco taco) {
    return new TacoResource(taco);
  }

  @Override
  public TacoResource toResource(Taco taco) {
    return createResourceWithId(taco.getId(), taco);
  }

}

๊ธฐ๋ณธ ์ƒ์„ฑ์ž : ๋ถ€๋ชจ์ธ ResourceAssemblerSupport ์˜ ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ํ˜ธ์ถœ. ์ปจํŠธ๋กค๋Ÿฌ์˜ URL ๊ฒฝ๋กœ๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด ํด๋ž˜์Šค๋ช…์„ ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์•˜๋‹ค.

instantiateResource() : ์ธ์ž๋กœ ๋ฐ›์€ Taco ๊ฐ์ฒด๋กœ TacoResources ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

toResource() : Taco ๊ฐ์ฒด๋กœ TacoResources ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑ + self ๋งํฌ๊ฐ€ URL์— ์ž๋™์ง€์ •

instantiateResource์™€ toResource์˜ ์ฐจ์ด์ ์€ ์ „์ž๋Š” Resources ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑํ•˜๊ณ , ํ›„์ž๋Š” ๊ฑฐ๊ธฐ์— ๋”ํ•ด ๋งํฌ๋„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค๋Š” ์ ์ด๋‹ค.

๋‹ค์‹œ recentTacos() ๋ฉ”์„œ๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ž.

@GetMapping("/recenth")
  public Resources<TacoResource> recentTacosH() {
// ๋ฆฌํ„ดํƒ€์ž…์ด Resources<Resource<Taco>>์—์„œ ๋ณ€๊ฒฝ
    PageRequest page = PageRequest.of(
            0, 12, Sort.by("createdAt").descending());
    List<Taco> tacos = tacoRepo.findAll(page).getContent();

    List<TacoResource> tacoResources = 
        new TacoResourceAssembler().toResources(tacos);
    Resources<TacoResource> recentResources = 
        new Resources<TacoResource>(tacoResources);
    recentResources.add(
        linkTo(methodOn(DesignTacoController.class).recentTacos())
        .withRel("recents"));
    return recentResources;
  }

์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๋ฉด self, recent ๋งํฌ๋Š” ์žˆ์ง€๋งŒ, ๊ฐ ํƒ€์ฝ”์˜ ์‹์ž์žฌ ๋งํฌ๊ฐ€ ์—†๋‹ค. ์‹์ž์žฌ์˜ ๋ฆฌ์†Œ์Šค ์–ด์…ˆ๋ธ”๋Ÿฌ ํด๋ž˜์Šค(์™€ IngredientResource) ๋„์ƒ์„ฑํ•˜์ž.

TacoResourceAssembler์™€ ์ด๋ฆ„๋งŒ ๋‹ค๋ฅผ๋ฟ ์ฝ”๋“œ์˜ ์—ญํ• ,๊ธฐ๋Šฅ์€ ๋™์ผํ•˜๋‹ค.

IngredientResourceAssembler

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Ingredient;

class IngredientResourceAssembler extends 
          ResourceAssemblerSupport<Ingredient, IngredientResource> {

  public IngredientResourceAssembler() {
    super(IngredientController.class, IngredientResource.class);
  }

  @Override
  public IngredientResource toResource(Ingredient ingredient) {
    return createResourceWithId(ingredient.getId(), ingredient);
  }
  
  @Override
  protected IngredientResource instantiateResource(
                                            Ingredient ingredient) {
    return new IngredientResource(ingredient);
  }

}

IngredientResource

package tacos.web.api;
import org.springframework.hateoas.ResourceSupport;
import lombok.Getter;
import tacos.Ingredient;
import tacos.Ingredient.Type;

public class IngredientResource extends ResourceSupport {

  @Getter
  private String name;

  @Getter
  private Type type;
  
  public IngredientResource(Ingredient ingredient) {
    this.name = ingredient.getName();
    this.type = ingredient.getType();
  }

}

IngredientResource ๊ฐ์ฒด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋„๋ก TacoResource ํด๋ž˜์Šค ์ˆ˜์ •

package tacos.web.api;
import java.util.Date;
import java.util.List;

import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.core.Relation;

import lombok.Getter;
import tacos.Taco;

public class TacoResource extends ResourceSupport {

  private static final IngredientResourceAssembler 
            ingredientAssembler = new IngredientResourceAssembler();
  
  @Getter
  private final String name;

  @Getter
  private final Date createdAt;

  @Getter
  private final List<IngredientResource> ingredients;
  
  public TacoResource(Taco taco) {
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients = 
        ingredientAssembler.toResources(taco.getIngredients()); // <---- ์ˆ˜์ •๋œ ๋ถ€๋ถ„
  }
  
}

์ด์ œ ์ตœ๊ทผ ์ƒ์„ฑ๋œ ํƒ€์ฝ”๋ฆฌ์ŠคํŠธ์˜ ํ•˜์ดํผ๋งํฌ๋ฅผ ์™„๋ฒฝํžˆ ๊ตฌํ˜„ํ–ˆ๋‹ค.

3. embedded ๊ด€๊ณ„ ์ด๋ฆ„ ์ง“๊ธฐ

์ตœ์ƒ์œ„ ์š”์†Œ์˜ _embedded ํ•˜์œ„์˜ tacoResourceList๋ฅผ ์‚ดํŽด๋ณด์ž.

{
    "_embedded": {
        "tacoResourceList": [
...

Resources ๊ฐ์ฒด๊ฐ€ List๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑ๋˜์–ด ์ƒ๊ธด ์ด๋ฆ„์ด๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์ด ์ด๋ฆ„์€ ์ปจํŠธ๋กค๋Ÿฌ ๋ช…์„ ๋”ฐ๋ผ๊ฐ€์ง€๋งŒ, ์ปจํŠธ๋กค๋Ÿฌ๋ช…๊ณผ JSON ํ•„๋“œ๋ช… ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ์‹ถ๋‹ค๋ฉด @Relation ์„ ์‚ฌ์šฉํ•˜์ž.

@Relation(value="taco", collectionRelation="tacos")
public class TacoResource extends ResourceSupport {
...

// JSON ๊ฐ์ฒด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋งŒ๋“ค์–ด์ง„๋‹ค.
{
    "_embedded": {
        "tacos": [
...

III. ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ํ™œ์„ฑํ™”ํ•˜๊ธฐ

์œ„์—์„œ ์—ด์‹ฌํžˆ RESP API๋ฅผ ๋…ธ์ถœ์‹œํ‚ค๋ ค๊ณ  ์ฝ”๋“œ ์ž‘์„ฑ์„ ํ–ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•Œ์•„์„œ REST ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค.

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

์œ„ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ง€๊ธˆ๊นŒ์ง€ ์ƒ์„ฑํ–ˆ๋˜ @RestController ์–ด๋…ธํ…Œ์ด์…˜์ด ์ง€์ •๋œ ๋ชจ๋“  ํด๋ž˜์Šค(์˜ ์–ด๋…ธํ…Œ์ด์…˜)๋“ค์„ ์ด ์‹œ์ ์—์„œ ์ œ๊ฑฐํ•˜์ž.

๊ทธ๋Ÿผ ์œ„์—์„œ ๋ฟŒ๋ ค์คฌ๋˜ JSON๊ฐ์ฒด๊ฐ€ ๋™์ผํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚œ๋‹ค.

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๊ฐ€ ์ž๋™ ์ƒ์„ฑํ•œ API์˜ ๊ธฐ๋ณธ ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•ด์ฃผ์ž.

ํ•ด๋‹น API์˜ ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์šฐ๋ฆฌ๊ฐ€ ์•„๊นŒ ์ž‘์„ฑํ•œ ๋‹ค๋ฅธ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์ถฉ๋Œํ•˜๋Š”๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•จ์ด๋‹ค.

spring:
  data:
    rest:
  		base-path: /api

์ด์ œ ๊ธฐ๋ณธ ๊ฒฝ๋กœ์— /api๊ฐ€ ๋ถ™์–ด ์‹์ž์žฌ์˜ ์—”๋“œํฌ์ธํŠธ๋Š” /api/ingredients๊ฐ€ ๋œ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ, ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๊ฐ€ /api/taco ์—”๋“œํฌ์ธํŠธ๋Š” ๋…ธ์ถœ์‹œํ‚ค์ง€ ์•Š๋Š”๋‹ค.

1. ๋ฆฌ์†Œ์Šค ๊ฒฝ๋กœ์™€ ๊ด€๊ณ„ ์ด๋ฆ„ ์กฐ์ •ํ•˜๊ธฐ

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๋Š” ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋…ธ์ถœ์‹œํ‚ฌ ๋•Œ ํ•ด๋‹น ํด๋ž˜์Šค ์ด๋ฆ„์˜ ๋ณต์ˆ˜ํ˜•์„ ์‚ฌ์šฉํ•œ๋‹ค.

order๋Š” orders, ingredient๋Š” ingredients๊ฐ€ ๋ณต์ˆ˜ํ˜•์ด์ง€๋งŒ taco์˜ ๋ณต์ˆ˜ํ˜•์€ tacoes์ด๋‹ค.

๊ด€๊ณ„์ด๋ฆ„์„ tacos๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด @RestResource๋ฅผ ์‚ฌ์šฉํ•˜์ž.

@Data
@Entity
@RestResource(rel="tacos", path="tacos")
public class Taco {

๊ทธ๋ฆฌ๊ณ  ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๋Š” ๋…ธ์ถœ๋œ ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์˜ ๋งํฌ๋ฅผ ๊ฐ–๋Š” **ํ™ˆ ๋ฆฌ์†Œ์Šค(/api)**๋„ ๋…ธ์ถœ์‹œ์ผœ์ค€๋‹ค.

2. ํŽ˜์ด์ง•๊ณผ ์ •๋ ฌ

ํ™ˆ ๋ฆฌ์†Œ์Šค์˜ ๋ชจ๋“  ๋งํฌ๋Š” ์„ ํƒ์  ๋งค๊ฐœ๋ณ€์ˆ˜์ธ page, size, sort ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ํ•œ ํŽ˜์ด์ง€๋‹น 20๊ฐœ์˜ ํ•ญ๋ชฉ์ด ๋ฐ˜ํ™˜๋˜๋ฉฐ page ์™€ size ๋ฅผ ์‚ฌ์šฉํ•ด ์›ํ•˜๋Š” ํŽ˜์ด์ง€์™€ ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

curl localhost:8080/api/tacos?size=5          // ์ฒซ๋ฒˆ์งธ ํŽ˜์ด์ง€(ํ•œ ํŽ˜์ด์ง€์— 5๊ฐœ item)
curl "localhost:8080/api/tacos?size=5&page=1" // ๋‘๋ฒˆ์งธ ํŽ˜์ด์ง€(ํ•œ ํŽ˜์ด์ง€์— 5๊ฐœ item)

curl ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•  ๋–„ ์•ฐํผ์ƒŒ๋“œ(&)๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ๊ฒน๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ์ค˜์•ผ ํ•œ๋‹ค.

HATEOAS๋Š” first, last, next, previous ํŽ˜์ด์ง€์˜ ๋งํฌ๋ฅผ ์š”์ฒญ ์‘๋‹ต์— ์ œ๊ณตํ•œ๋‹ค.

"_links": {
	"first": {
		"href": "http://localhost:8080/api/tacos?page=0&size=2"
	},
	"self": {
		"href": "http://localhost:8080/api/tacos",
	},
	"next": {
		"href": "http://localhost:8080/api/tacos?page=1&size=2"
	},
	"last": {
		"href": "http://localhost:8080/api/tacos?page=2&size=2"
	},
	"profile": {
		"href": "http://localhost:8080/api/profile/tacos"
	}
	"recents": {
		"href": "http://localhost:8080/api/tacos/recent"
	}
}
// ํ˜„์žฌ ํŽ˜์ด์ง€๊ฐ€ 0์ด๋ผ previous๊ฐ€ ์—†๋‹ค.

sort ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•˜๋ฉด ์—”ํ‹ฐํ‹ฐ์˜ ์†์„ฑ์„ ๊ธฐ์ค€์œผ๋กœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ •๋ ฌ ํ•  ์ˆ˜ ์žˆ๋‹ค.

curl "localhost:8080/api/tacos?sort=createdAt,desc&page=0&size=12"  // ์ตœ๊ทผ ์ƒ์„ฑ๋œ ํƒ€์ฝ” 12๊ฐœ

์—ฌ๊ธฐ์—” UI์ฝ”๋“œ๊ฐ€ ํ•˜๋“œ์ฝ”๋”ฉ๋๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋งํฌ ๋ฆฌ์ŠคํŠธ์—์„œ URL์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ข‹๊ณ , URL์ด ๋” ๊ฐ„๋‹จํ•ด์ง€๊ธฐ๋ฅผ ์›ํ•œ๋‹ค.

3. ์ปค์Šคํ…€ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ํ•˜๊ธฐ

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๋Š” ์ž๋™์œผ๋กœ ์—”๋“œํฌ์ธํŠธ ์ƒ์„ฑ์„ ํ•ด์ฃผ์ง€๋งŒ,

๊ธฐ๋ณธ์ ์ธ CRUD API์—์„œ ๋ฒ—์–ด๋‚˜ ๊ฐœ๋ฐœ์ž ๋‚˜๋ฆ„์˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•  ๋•Œ๋„ ์žˆ๋‹ค.

@RestController๋ฅผ ์‚ฌ์šฉํ•ด REST๊ฐ€ ์ž๋™์ƒ์„ฑํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ์— ์šฐ๋ฆฌ ๋‚˜๋ฆ„์˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋ณด์ถฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์ปค์Šคํ…€ ์—”๋“œํฌ์ธํŠธ์˜ 2๊ฐ€์ง€ ๊ณ ๋ คํ•  ์ 

  1. ์šฐ๋ฆฌ ์—”๋“œํฌ์ธํŠธ ์ปจํŠธ๋กค๋Ÿฌ๋Š” Spring Data REST์˜ ๊ธฐ๋ณธ๊ฒฝ๋กœ๋กœ ๋งคํ•‘๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ฐ™๊ฒŒ ํ•  ์ˆ˜๋„ ์žˆ๋Š”๋ฐ, ๋‚˜์ค‘์— ๊ธฐ๋ณธ๊ฒฝ๋กœ๊ฐ€ ์ˆ˜์ •๋˜๋ฉด ๋˜ ์–ด๊ธ‹๋‚˜๊ฒŒ ๋œ๋‹ค. ๋‹ค์‹œ ๋งž์ถ”์–ด์•ผ ํ•œ๋‹ค.
  2. ์šฐ๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ์— ์ •์˜ํ•œ ์—”๋“œํฌ์ธํŠธ๋Š” ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST ์—”๋“œํฌ์ธํŠธ์—์„œ ๋ฐ˜ํ™˜๋˜๋Š” ๋ฆฌ์†Œ์Šค์˜ ํ•˜์ดํผ๋งํฌ์— ์ž๋™์œผ๋กœ ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค. (Spring Data REST๊ฐ€ ์ž๋™์œผ๋กœ ๋งŒ๋“  hyperlink์— ์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ๋“  api๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค.)

.

์šฐ์„  ์ฒซ๋ฒˆ์งธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ํด๋ž˜์Šค์— ๋ถ™์ด๋Š” @RepositoryRestController ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์ด ์–ด๋…ธํ…Œ์ด์…˜์€ ์„ค์ •๋œ Controller์˜ ์‹คํ–‰ ๊ฒฝ๋กœ๋ฅผ application.yml์—์„œ ์„ค์ •ํ•œ data.rest.base-path๋ฅผ base path(๊ธฐ๋ณธ๊ฒฝ๋กœ)๋กœ ์„ค์ •ํ•˜์—ฌ ์ ‘์  ์ฃผ์†Œ๋ฅผ ๋งŒ๋“ ๋‹ค.

์‹ค์ œ ๋™์ž‘์„ ๋ณด๊ธฐ ์œ„ํ•ด์„œ ๋ฉ”์†Œ๋“œ๊ฐ€ ํ•˜๋‚˜๋งŒ ์žˆ๋Š” ์ƒˆ๋กœ์šด ํด๋ž˜์Šค๋ฅผ ํ•˜๋‚˜ ์ƒ์„ฑํ•˜๊ณ  ์•„๋ž˜์ฒ˜๋Ÿผ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•œ ํ›„ Rest ํด๋ผ์ด์–ธํŠธ์—์„œ localhost:8080/api/tacos๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด๋ฉด ์ •์ƒ์ ์œผ๋กœ /api/tacos/recent๊ฐ€ ๋‚˜์˜จ๋‹ค.

@RepositoryRestController๋Š” @RestController๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์•„, ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ์š”์ฒญ ์‘๋‹ต์˜ ๋ชธ์ฒด์— ์ž๋™์œผ๋กœ ์ˆ˜๋กํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ @ResponseBody๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜ ResponseEntity๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค.

์•„๋ž˜ ์†Œ์Šค๋Š” ๋‹จ์ˆœํžˆ 6.2์— ์žˆ๋Š” ์†Œ์Šค์—์„œ ํด๋ž˜์Šค์˜ ๋ถ™์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์˜€๊ณ  ๋ฆฌํ„ด ํƒ€์ž…๋„ ResponseEntity๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ ์ฒซ๋ฒˆ์งธ ๋ฌธ์ œ๋ฅผ ์ด ์ฑ…์—์„œ ์ œ์‹œํ•˜๋Š” ํ•ด๊ฒฐ์ฑ…์ด๋‹ค.

package tacos.web.api;
// import ์ƒ๋žต
@RepositoryRestController
public class RecentTacosController {

  private TacoRepository tacoRepo;

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

  @GetMapping(path="/tacos/recent", produces="application/hal+json")
  public ResponseEntity<Resources<TacoResource>> recentTacos() {
    PageRequest page = PageRequest.of(
                          0, 12, Sort.by("createdAt").descending());
    List<Taco> tacos = tacoRepo.findAll(page).getContent();

    List<TacoResource> tacoResources = 
        new TacoResourceAssembler().toResources(tacos);
    Resources<TacoResource> recentResources = 
            new Resources<TacoResource>(tacoResources);
    
    recentResources.add(
        linkTo(methodOn(RecentTacosController.class).recentTacos())
            .withRel("recents"));
    return new ResponseEntity<>(recentResources, HttpStatus.OK);
  }

}

4. ์ปค์Šคํ…€ ํ•˜์ดํผ๋งํฌ๋ฅผ ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ ์—”๋“œํฌ์ธํŠธ์— ์ถ”๊ฐ€ํ•˜๊ธฐ

์—ฌ๊ธฐ์„œ๋Š” ๋‘๋ฒˆ์งธ ๋ฌธ์ œ์— ๋Œ€ํ•œ ํ•ด๊ฒฐ์ฑ…์„ ์ œ์‹œํ•œ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ๋“  rest ์ ‘์ ์„ spring์ด ์ž๋™์ƒ์„ฑํ•œ hyperlink๋ฅผ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด RepresentationModelProcessor ์ด๋ผ๋Š” bean๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋งํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

@Configuration
public class SpringDataRestConfiguration {

  @Bean
  public ResourceProcessor<PagedResources<Resource<Taco>>>
    tacoProcessor(EntityLinks links) {

    return new ResourceProcessor<PagedResources<Resource<Taco>>>() {
      @Override
      public PagedResources<Resource<Taco>> process(
                          PagedResources<Resource<Taco>> resource) {
        resource.add(
            links.linkFor(Taco.class)
                 .slash("recent")
                 .withRel("recents"));
        return resource;
      }
    };
  }
  
}

์ด ๋นˆ์€ ์ต๋ช… ์ด๋„ˆํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์Šคํ”„๋ง HATEOAS๋Š” ResourcesProcessor ํƒ€์ž…์˜ ๋นˆ์„ ์ ์ ˆํ•œ ์ž์›์— ์ ์šฉํ•œ๋‹ค.

์ง€๊ธˆ๊ฐ™์€ ๊ฒฝ์šฐ์—” ๊ฐ€์žฅ ์ตœ๊ทผ์— ์ƒ์„ฑ๋œ ํƒ€์ฝ”์— ๋Œ€ํ•œ ๋งํฌ๋ฅผ ๋ฐ›๋Š”๋‹ค.

Summary

  • Spring MVC๋กœ REST ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ปจํŠธ๋กค๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ๋Š” @ResponseBody๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜, ResponseEntity ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•ด ๋ฆฌ์Šคํฐ์Šค๋ฐ”๋””์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค.
  • @RestController๋Š” Rest์ปจํŠธ๋กค๋Ÿฌ์—์„œ @ResponseBody๋ฅผ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.
  • Spring HATEOAS๋Š” ์ž์›์˜ ํ•˜์ดํผ๋งํ‚น์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ค€๋‹ค.
  • ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๋Š” ์Šคํ”„๋ง ๋ฐ์ดํ„ฐ REST๋ฅผ ์‚ฌ์šฉํ•ด ์ž๋™์œผ๋กœ REST API๋ฅผ ๋…ธ์ถœ์‹œํ‚จ๋‹ค.
โš ๏ธ **GitHub.com Fallback** โš ๏ธ