Hypermedia as the engine of application state (HATEOAS) is a constraint of the REST application architecture that distinguishes it from other network application architectures.
Hypermedia,
an extension of the term hypertext, is a nonlinear medium of information that includes graphics, audio, video, plain text and hyperlinks.
Hyperlink,
In computing, a hyperlink, or simply a link, is a digital reference to data that the user can follow or be guided to by clicking or tapping
Example
user-agent๋ ์ง์
์ URL์ ํตํด REST API์ HTTP ์์ฒญ์ ํฉ๋๋ค.
user-agent๊ฐ ํ ์ ์๋ ๋ชจ๋ ํ์ ์์ฒญ์ ๊ฐ ์์ฒญ์ ๋ํ ์๋ต ๋ด์์ ๋ฐ๊ฒฌ๋ฉ๋๋ค.
Requset
Copy GET /accounts/12345 HTTP/1.1
Host: bank.example.com
Response
Copy HTTP/1.1 200 OK
{
"account": {
"account_number": 12345,
"balance": {
"currency": "usd",
"value": 100.00
},
"links": {
"deposits": "/accounts/12345/deposits",
"withdrawals": "/accounts/12345/withdrawals",
"transfers": "/accounts/12345/transfers",
"close-requests": "/accounts/12345/close-requests"
}
}
}
CODE
HATEOAS ์ ์ฉ์
Copy @GetMapping
public ResponseEntity<Page<Product>> findAll(Principal principal,
Pageable pageable) {
Page<Product> products =
productDao.findProductsByUserEmail(principal.getName(), pageable);
return ResponseEntity.ok(products);
}
Copy HTTP/1.1 200 OK
Content-Type: application/hal+json
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 9362
{
"_embedded" : {
"productResponseList" : [ {
"productId" : "648dcfb047d916749c554cd4",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
}, {
"productId" : "648dcfb047d916749c554cd5",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
}
........
........
........
, {
"productId" : "648dcfb047d916749c554ce7",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
} ]
},
"page" : {
"size" : 20,
"totalElements" : 100,
"totalPages" : 5,
"number" : 1
}
}
HATEOAS ์ ์ฉํ
Copy @GetMapping
public ResponseEntity<PagedModel<ProductResponse>> findAll(Principal principal,
Pageable pageable) {
Page<Product> products =
productDao.findProductsByUserEmail(principal.getName(), pageable);
PagedModel<ProductResponse> productResponses = pagedResourcesAssembler
.toModel(products, productAssembler);
return ResponseEntity.ok(productResponses);
}
@RequiredArgsConstructor
@RestController
@RequestMapping(path = "/products", produces = MediaTypes.HAL_JSON_VALUE)
public class ProductController {
private final ProductService productService;
private final StoreDao storeDao;
private final ProductDao productDao;
private final PagedResourcesAssembler<Product> pagedResourcesAssembler;
private final ProductAssembler productAssembler;
@PostMapping
public ResponseEntity<CreatedProductResponse> addProduct(
@RequestBody ProductRequest productRequest,
Principal principal) {
StoreId storeId = storeDao.findStoreIdByUserEmail(principal.getName());
CreatedProductResponse productResponse = productService.addProduct(productRequest, storeId);
ProductId productId = productResponse.getProductId();
Link selfRel = BusinessLinks.getProductSelfRel(productId);
productResponse.add(selfRel, BusinessLinks.MY_STORE);
return ResponseEntity.created(selfRel.toUri()).body(productResponse);
}
@GetMapping
public ResponseEntity<PagedModel<ProductResponse>> findAll(Principal principal,
Pageable pageable) {
Page<Product> products =
productDao.findProductsByUserEmail(principal.getName(), pageable);
PagedModel<ProductResponse> productResponses = pagedResourcesAssembler
.toModel(products, productAssembler);
return ResponseEntity.ok(productResponses);
}
}
Copy HTTP/1.1 200 OK
Content-Type: application/hal+json
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 9362
{
"_embedded" : {
"productResponseList" : [ {
"productId" : "648dcfb047d916749c554cd4",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
"_links" : {
"self" : {
"href" : "http://localhost:8080/products/648dcfb047d916749c554cd4"
}
}
}, {
"productId" : "648dcfb047d916749c554cd5",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
"_links" : {
"self" : {
"href" : "http://localhost:8080/products/648dcfb047d916749c554cd5"
}
}
}
........
........
........
, {
"productId" : "648dcfb047d916749c554ce7",
"productName" : "testProductName",
"price" : 10000,
"description" : "testDescription",
"brand" : "testBrand",
"category" : "testCategory",
"createdTime" : "20230618002224",
"updateTime" : "20230618002224",
"_links" : {
"self" : {
"href" : "http://localhost:8080/products/648dcfb047d916749c554ce7"
}
}
} ]
},
"_links" : {
"first" : {
"href" : "http://localhost:8080/products?page=0&size=20"
},
"prev" : {
"href" : "http://localhost:8080/products?page=0&size=20"
},
"self" : {
"href" : "http://localhost:8080/products?page=1&size=20"
},
"next" : {
"href" : "http://localhost:8080/products?page=2&size=20"
},
"last" : {
"href" : "http://localhost:8080/products?page=4&size=20"
}
},
"page" : {
"size" : 20,
"totalElements" : 100,
"totalPages" : 5,
"number" : 1
}
}
์ฅ๋จ์
์ฅ์
ํด๋ผ์ด์ธํธ-์๋ฒ ๋
๋ฆฝ์ฑ
HATEOAS๋ ํด๋ผ์ด์ธํธ์ ์๋ฒ ๊ฐ์ ๊ฐ๋ ฅํ ๊ฒฐํฉ์ ํผํ๊ณ ํด๋ผ์ด์ธํธ์ ๋ํ API ๋ณ๊ฒฝ์ ์ฌํฅ์ ์ต์ํํฉ๋๋ค. ํด๋ผ์ด์ธํธ๋ ํ์ดํผ๋ฏธ๋์ด ๋งํฌ๋ฅผ ํตํด ๋ฆฌ์์ค์ ์ํธ์์ฉํ๊ณ , ์๋ฒ์์ ์ ๊ณตํ๋ ๋ฆฌ์์ค ํํ์ ๋ํด ์ฌ์ ์ง์์ด ํ์ํ์ง ์์ต๋๋ค. ์ด๋ก์จ ์๋ฒ ์ธก์ ๋ณ๊ฒฝ์๋ ํด๋ผ์ด์ธํธ๊ฐ ์ ์ฐํ๊ฒ ๋์ํ ์ ์์ต๋๋ค.
APIํ์ ๋ฐ ๋์ค์ปค๋ฒ๋ฆฌ
HATEOA๋ ํด๋ผ์ด์ธํธ์๊ฒ API์ ๊ตฌ์กฐ์ ์ฌ์ฉ ๊ฐ๋ฅํ ์์
์ ๋์ ์ผ๋ก ์ ๊ณตํฉ๋๋ค. ํด๋ผ์ด์ธํธ๋ ์์์ ๋ฆฌ์์ค๋ก๋ถํฐ ์ถ๋ฐํ์ฌ ํ์ดํผ๋ฏธ๋์ด ๋งํฌ๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉฐ ์๋ฒ๊ฐ ์ ๊ณตํ๋ ๋ฆฌ์์ค์ ์ํธ์์ฉํ ์ ์์ต๋๋ค. ์ด๋ API๋ฅผ ํ์ํ๊ณ ๋ฐ๊ฒฌํ๋ ๋ฐ ๋์์ ์ฃผ๋ฉฐ, ํด๋ผ์ด์ธํธ ์ฝ๋์์ ํ๋ ์ฝ๋ฉ๋ ์๋ํฌ์ธํธ๋ฅผ ์ ๊ฑฐํ๊ณ ์ ์ฐ์ฑ๊ณผ ํ์ฅ์์ ๋์ ์ ์์ต๋๋ค.
์
ํ ์ค๋ช
ํ API
HATEOAS๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ดํผ๋ฏธ๋์ด ๋งํฌ์ ํจ๊ป ๋ฆฌ์์ค ํํ์ด ์ ๊ณต๋ฉ๋๋ค. ์ด๋ฅผ ํตํด API๊ฐ ์๊ธฐ ์ค๋ช
์ (self-descriptive)์ด ๋์ด ํด๋ผ์ด์ธํธ๊ฐ ๋ฆฌ์์ค์ ์๋ฏธ์ ์ํธ์์ฉ ๋ฐฉ๋ฒ์ ์ดํด ํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์๊ฐ API๋ฌธ์๋ฅผ ์ฝ๊ณ ํด์ํ๋ ์๊ฐ๊ณผ ๋
ธ๋ ฅ์ ์ค์ผ ์ ์์ต๋๋ค.
๋จ์
๋ณต์ก์ฑ
HATEOA๋ฅผ ๊ตฌํํ๋ ค๋ฉด API ์ค๊ณ์ ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ๋ ๋ณต์กํ๊ฒ ๋ง๋ค์ด์ผ ํฉ๋๋ค. ํด๋ผ์ด์ธํธ๋ ํ์ดํผ๋ฏธ๋์ด ๋งํฌ๋ฅผ ์ดํดํ๊ณ ํด์ํ๋ ๋
ธ๋ฆฌ๋ฅผ ์ถ๊ฐ๋ก ๊ตฌํํด์ผ ํ๋ฉฐ, ์๋ฒ๋ ๋งํฌ๋ฅผ ๋์ ์ผ๋ก ์์ฑํ๊ณ ๊ด๋ฆฌํด์ผ ํ๋ค. ์ด๋ ๊ฐ๋ฐ ๋ฐ ์ ์ง๋ณด์ ๋น์ฉ์ ์ฆ๊ฐ์ํฌ ์ ์์ต๋๋ค.
๋คํธ์ํฌ ์ค๋ฒํค๋
HATEOA๋ฅผ ์ฌ์ฉํ๋ฉด ๋ง์ ํ์ดํผ๋ฏธ๋์ด ๋งํฌ๊ฐ ๋ฆฌ์์ค ํํ์ ํฌํจ๋ ์ ์์ต๋๋ค. ์ด๋ API ์๋ต์ ํฌ๊ธฐ๋ฅผ ์ฆ๊ฐ์ํฌ์ ์๊ณ , ๋คํธ์ํฌ ์ ์ก ์๊ฐ๊ณผ ๋์ญํญ์ ์๋นํ ์ ์์ต๋๋ค. ๋ฐ๋ผ์ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ์๋ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
ํด๋ผ์ด์ธํธ ๊ตฌํ์ ์ด๋ ค์
HATEOAS๋ฅผ ์ง์ํ์ง ์๋ ํด๋ผ์ด์ธํธ๋ ๋งํฌ์ ๋ฆฌ์์ค ์ํธ์์ฉ์ ์ ๋๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ํด๋ผ์ด์ธํธ ์ธก์ HATEOAS๋ฅผ ์ง์ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ๋ ์์ํฌ๋ฅผ ๋์
ํด์ผ ํ๋ฉฐ, ๊ธฐ์กด์ ํด๋ผ์ด์ธํธ๋ฅผ ์์ ํด์ผ ํ ์๋ ์์ต๋๋ค. ์ด๋ ์ผ๋ถ ๊ฐ๋ฐ์์๊ฒ ์ถ๊ฐ ์์
์ผ๋ก ๋ค๊ฐ์ฌ ์ ์์ต๋๋ค.
Reference