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์์ ๋ ๊ฐ๋จํ ์ฝ๋๋ก ๋์ฒด๋๋ค. ํ์ดํผ๋งํฌ๊ฐ ์ด๋ป๊ฒ ๋์ํ๋์ง ๊ตฌ์กฐ๋ฅผ ์ดํด๋ณธ๋ค๊ณ ์๊ฐํ์.
์ต๊ทค๋ฌ ํด๋ผ์ด์ธํธ ์ฝ๋๋ก 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 ์์ฒญ-์ฒ๋ฆฌ ์ด๋ ธํ ์ด์
๊ฐ์ฅ ์ต๊ทผ์ ์์ฑ๋ ํ์ฝ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฐ๋จํ 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 ์ ์ง์๊ธฐ๋ฅ
- (์คํ ๋ ์คํ์ ์ด๋ ธํ ์ด์ ์ผ๋ก์) ์ด ์ด๋ ธํ ์ด์ ์ด ์ง์ ๋ ํด๋์ค๋ฅผ ์คํ๋ง ์ปดํฌ๋ํธ ๊ฒ์์ผ๋ก ์ฐพ์ ์ ์๋ค.
- ์ปจํธ๋กค๋ฌ์ ๋ชจ๋ 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
์ด์ ๊น์ง๋ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๊ฒ๋ง ํ๋ค๋ฉด, ์ด๋ฒ์๋ ํด๋ผ์ด์ธํธ์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ ๋ถ๋ถ์ ์ดํด๋ณด๊ฒ ๋ค. request์ ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ฅผ ์์ฑํด๋ณด์.
ํ์ฝ ํด๋ผ์ฐ๋๋ฅผ SPA๋ก ๋ณํํ๊ธฐ ์ํด ์ต๊ทค๋ฌ ์ปดํฌ๋ํธ์ ์๋ํฌ์ธํธ๋ฅผ ์์ฑํด๋ณด์.
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);
}
์์์ ์๋ก์ด 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๋ก ๋ณ๊ฒฝํ๊ณ ์ถ์ ๋ ํด๋ผ์ด์ธํธ์์ ์ด๋ฅผ ํํํ ๋ฐฉ๋ฒ์ด ํ์ํ๋ค.
- ์ปฌ๋ ์ ์ ์ ์ฅ๋ ํญ๋ชฉ์ ์ญ์ ํน์ ์ถ๊ฐํ ๋ฐฉ๋ฒ์ด ์๋ค. ๋ฐ๋ผ์ ํด๋ผ์ด์ธํธ๊ฐ ์ปฌ๋ ์ ์ ํญ๋ชฉ์ ์ญ์ ํน์ ์ถ๊ฐํ๋ ค๋ฉด ๋ณ๊ฒฝ๋ ์ปฌ๋ ์ ํ ์ดํฐ ์ ์ฒด๋ฅผ ์ ์กํด์ผ ํ๋ค.
๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ ๋์๋ 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) {}
}
์ง๊ธ๊น์ง๋ ํด๋ผ์ด์ธํธ๊ฐ 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>
/design/recent์ ๋ํ GET ์์ฒญ์์ ๋ฐํ๋๋ ์ต๊ทผ ํ์ฝ ๋ฆฌ์คํธ์ ํ์ดํผ๋ฏธ๋์ด ๋งํฌ๋ฅผ ์ถ๊ฐํด๋ณด์.
spring HATEOAS์ ๋ฆฌ์์ค๋ฅผ ๋ํ๋ด๋ ๋ ๊ฐ์ง ๊ธฐ๋ณธํ์
์ด ์๋๋ฐ, Resource
์ Resources
์ด๋ค.
Resource๋ ๋จ์ผ ๋ฆฌ์์ค, Resources๋ ๋ฆฌ์์ค ์ปฌ๋ ์ ์ ๋ํ๋ด๊ณ ๋ ํ์ ๋ชจ๋ ์คํ๋ง MVC ์ปจํธ๋กค๋ฌ ๋ฉ์๋์์ ๋ฐํ๋ ๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ๋ JSON(or XML)์ ํฌํจ๋๋ค.
์ต๊ทผ ์์ฑ๋ ํ์ฝ ๋ฆฌ์คํธ์ ํ์ดํผ๋งํฌ๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด 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()์ ํ์ฉํด ์ปจํธ๋กค๋ฌ ํด๋์ค ๊ฒฝ๋ก, ๋ฉ์๋ ๊ฒฝ๋ก ๋์ค ํ๋๋ฅผ ํธ์ถํ ์ ์๊ฒ ํด์ค๋ค.
์ด๋ฒ์๋ ๋ฆฌ์คํธ์ ํฌํจ๋ ๊ฐ ํ์ฝ ๋ฆฌ์คํธ์ ๋ํ ๋งํฌ๋ฅผ ์ถ๊ฐํด๋ณด์.
๊ฐ ํ์ฝ ๋ฆฌ์คํธ ๋งํฌ๋ฅผ ์ผ์ผํ ์ถ๊ฐํ๊ธฐ ์ํด ๋ฐ๋ณต ๋ฃจํ๋ฅผ ๋๋ ค๋ ๋์ง๋ง, ๊ฐ 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()); // <---- ์์ ๋ ๋ถ๋ถ
}
}
์ด์ ์ต๊ทผ ์์ฑ๋ ํ์ฝ๋ฆฌ์คํธ์ ํ์ดํผ๋งํฌ๋ฅผ ์๋ฒฝํ ๊ตฌํํ๋ค.
์ต์์ ์์์ _embedded ํ์์ tacoResourceList๋ฅผ ์ดํด๋ณด์.
{
"_embedded": {
"tacoResourceList": [
...
Resources ๊ฐ์ฒด๊ฐ List๋ก๋ถํฐ ์์ฑ๋์ด ์๊ธด ์ด๋ฆ์ด๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ์ด ์ด๋ฆ์ ์ปจํธ๋กค๋ฌ ๋ช ์ ๋ฐ๋ผ๊ฐ์ง๋ง, ์ปจํธ๋กค๋ฌ๋ช ๊ณผ JSON ํ๋๋ช ๊ฐ์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๊ณ ์ถ๋ค๋ฉด @Relation ์ ์ฌ์ฉํ์.
@Relation(value="taco", collectionRelation="tacos")
public class TacoResource extends ResourceSupport {
...
// JSON ๊ฐ์ฒด๋ ๋ค์๊ณผ ๊ฐ์ด ๋ง๋ค์ด์ง๋ค.
{
"_embedded": {
"tacos": [
...
์์์ ์ด์ฌํ 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 ์๋ํฌ์ธํธ๋ ๋ ธ์ถ์ํค์ง ์๋๋ค.
์คํ๋ง ๋ฐ์ดํฐ REST๋ ์๋ํฌ์ธํธ๋ฅผ ๋ ธ์ถ์ํฌ ๋ ํด๋น ํด๋์ค ์ด๋ฆ์ ๋ณต์ํ์ ์ฌ์ฉํ๋ค.
order๋ orders, ingredient๋ ingredients๊ฐ ๋ณต์ํ์ด์ง๋ง taco์ ๋ณต์ํ์ tacoes์ด๋ค.
๊ด๊ณ์ด๋ฆ์ tacos๋ก ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด @RestResource๋ฅผ ์ฌ์ฉํ์.
@Data
@Entity
@RestResource(rel="tacos", path="tacos")
public class Taco {
๊ทธ๋ฆฌ๊ณ ์คํ๋ง ๋ฐ์ดํฐ REST๋ ๋ ธ์ถ๋ ๋ชจ๋ ์๋ํฌ์ธํธ์ ๋งํฌ๋ฅผ ๊ฐ๋ **ํ ๋ฆฌ์์ค(/api)**๋ ๋ ธ์ถ์์ผ์ค๋ค.
ํ ๋ฆฌ์์ค์ ๋ชจ๋ ๋งํฌ๋ ์ ํ์ ๋งค๊ฐ๋ณ์์ธ 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์ด ๋ ๊ฐ๋จํด์ง๊ธฐ๋ฅผ ์ํ๋ค.
์คํ๋ง ๋ฐ์ดํฐ REST๋ ์๋์ผ๋ก ์๋ํฌ์ธํธ ์์ฑ์ ํด์ฃผ์ง๋ง,
๊ธฐ๋ณธ์ ์ธ CRUD API์์ ๋ฒ์ด๋ ๊ฐ๋ฐ์ ๋๋ฆ์ ์๋ํฌ์ธํธ๋ฅผ ์์ฑํด์ผํ ๋๋ ์๋ค.
@RestController๋ฅผ ์ฌ์ฉํด REST๊ฐ ์๋์์ฑํ๋ ์๋ํฌ์ธํธ์ ์ฐ๋ฆฌ ๋๋ฆ์ ์๋ํฌ์ธํธ๋ฅผ ๋ณด์ถฉํ ์ ์๋ค.
์ปค์คํ ์๋ํฌ์ธํธ์ 2๊ฐ์ง ๊ณ ๋ คํ ์
- ์ฐ๋ฆฌ ์๋ํฌ์ธํธ ์ปจํธ๋กค๋ฌ๋ Spring Data REST์ ๊ธฐ๋ณธ๊ฒฝ๋ก๋ก ๋งคํ๋์ง ์๋๋ค. ๊ฐ๊ฒ ํ ์๋ ์๋๋ฐ, ๋์ค์ ๊ธฐ๋ณธ๊ฒฝ๋ก๊ฐ ์์ ๋๋ฉด ๋ ์ด๊ธ๋๊ฒ ๋๋ค. ๋ค์ ๋ง์ถ์ด์ผ ํ๋ค.
- ์ฐ๋ฆฌ ์ปจํธ๋กค๋ฌ์ ์ ์ํ ์๋ํฌ์ธํธ๋ ์คํ๋ง ๋ฐ์ดํฐ 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);
}
}
์ฌ๊ธฐ์๋ ๋๋ฒ์งธ ๋ฌธ์ ์ ๋ํ ํด๊ฒฐ์ฑ ์ ์ ์ํ๋ค.
์ฌ์ฉ์๊ฐ ๋ง๋ 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 ํ์ ์ ๋น์ ์ ์ ํ ์์์ ์ ์ฉํ๋ค.
์ง๊ธ๊ฐ์ ๊ฒฝ์ฐ์ ๊ฐ์ฅ ์ต๊ทผ์ ์์ฑ๋ ํ์ฝ์ ๋ํ ๋งํฌ๋ฅผ ๋ฐ๋๋ค.
- Spring MVC๋ก REST ์๋ํฌ์ธํธ๋ฅผ ์์ฑํ ์ ์๋ค.
- ์ปจํธ๋กค๋ฌ ํธ๋ค๋ฌ ๋ฉ์๋๋ @ResponseBody๋ฅผ ๋ถ์ด๊ฑฐ๋, ResponseEntity ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํด ๋ฆฌ์คํฐ์ค๋ฐ๋์ ๋ฐ์ดํฐ๋ฅผ ๋ด์ ์ ์๋ค.
- @RestController๋ Rest์ปจํธ๋กค๋ฌ์์ @ResponseBody๋ฅผ ์๋ตํ ์ ์๊ฒ ํด์ค๋ค.
- Spring HATEOAS๋ ์์์ ํ์ดํผ๋งํน์ ๊ฐ๋ฅํ๊ฒ ํด์ค๋ค.
- ์คํ๋ง ๋ฐ์ดํฐ ๋ฆฌํฌ์งํฐ๋ฆฌ๋ ์คํ๋ง ๋ฐ์ดํฐ REST๋ฅผ ์ฌ์ฉํด ์๋์ผ๋ก REST API๋ฅผ ๋ ธ์ถ์ํจ๋ค.