Contents

[Mastering Spring 5.0] 6.2 HATEOAS

Mastering Spring 5.0 μŠ€ν„°λ””

μŠ€ν”„λ§ 5.0 λ§ˆμŠ€ν„° μŠ€ν„°λ””
μŠ€ν”„λ§ 5.0 λ§ˆμŠ€ν„° μŠ€ν„°λ”” ν•™μŠ΅ λ‚΄μš© μ •λ¦¬μž…λ‹ˆλ‹€.

REST μ„±μˆ™λ„ λͺ¨λΈ (Richardson Maturity Model)

Richardson Maturity Model μ—μ„œλŠ” Restful Web Service λ₯Ό λ‹€μŒμ˜ λ‹¨κ³„λ‘œ λ‚˜λˆ„μ–΄ μ„±μˆ™λ„λ₯Ό μ •μ˜ν•˜κ³  μžˆλ‹€.

  • Level 0 : 원격 ν”„λ‘œμ‹œμ € 호좜 (Remote Procedure Invocation) 에 κΈ°λ°˜ν•œ ν˜•νƒœλ‘œ resource ꡬ뢄 없이 μ„€κ³„λœ HTTP API (http://server/getPosts, http://server/deletePosts, http://server/doThis, http://server/doThat λ“±)

  • Level 1 : resourceλ₯Ό URI 톡해 λ‚˜νƒ€λ‚Έλ‹€. (λͺ…사 μ‚¬μš©) κ·ΈλŸ¬λ‚˜, HTTP METHOD(GET,POST,PUT,DELETE λ“±) μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. (http://server/accounts, http://server/accounts/10)

  • Level 2 : resourceλ₯Ό URI + HTTP Method λ₯Ό μ‚¬μš©ν•˜μ—¬ μ ‘κ·Όν•œλ‹€. (계정을 μˆ˜μ •ν•˜λ €λ©΄ PUT, 계정을 μƒμ„±ν•˜λ €λ©΄ POST λ©”μ„œλ“œλ₯Ό μˆ˜ν–‰ν•œλ‹€.)

  • Level 3 : HATEOAS. μš”μ²­ν•œ 정보 뿐만 μ•„λ‹ˆλΌ μš”μ²­ν•œ 정보에 κ΄€λ ¨ν•œ URI λ₯Ό ν¬ν•¨ν•¨μœΌλ‘œμ¨, μ„œλΉ„μŠ€ μ†ŒλΉ„μžκ°€ ν•  수 μžˆλŠ” λ‹€μŒ μ‘°μΉ˜μ— λŒ€ν•΄μ„œλ„ μ œκ³΅ν•œλ‹€.

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) λŠ” RESTful 아킀텍쳐λ₯Ό κ³ μœ ν•˜κ²Œ μœ μ§€ν•˜λŠ” REST μ‘μš© ν”„λ‘œκ·Έλž¨ μ•„ν‚€ν…μ³μ˜ μ œμ•½ 사항이닀. Hypermedia λΌλŠ” μš©μ–΄λŠ” 이미지, ν…μŠ€νŠΈ, λ™μ˜μƒ λ“± λ‹€λ₯Έ ν˜•μ‹μ˜ 미디어에 λŒ€ν•œ 링크가 ν¬ν•¨λœ 것을 μ˜λ―Έν•œλ‹€.
Hypermedia 의 μœ μ‚¬ν•œ κ°œλ…μ„ RESTful μ„œλΉ„μŠ€μ—λ„ μ μš©ν•˜μ—¬, μš”μ²­ν•œ λ¦¬μ†ŒμŠ€μ— λŒ€ν•œ 데이터 뿐만 μ•„λ‹ˆλΌ κ΄€λ ¨ λ¦¬μ†ŒμŠ€ λ˜λŠ” 의쑴 λ¦¬μ†ŒμŠ€μ˜ URI 링크 λ₯Ό 응닡에 ν¬ν•¨μ‹œμΌœ μ„œλΉ„μŠ€ μ†ŒλΉ„μžμ—κ²Œ μ œκ³΅ν•˜λŠ” ν˜•νƒœλΌκ³  λ³Ό 수 μžˆλ‹€.

κΈ°μ‘΄ RESTful API 의 단점

  • API 의 μ—”λ“œν¬μΈνŠΈκ°€ 정해지면 이λ₯Ό μ‰½κ²Œ λ³€κ²½ν•˜κΈ°κ°€ μ–΄λ ΅λ‹€. API κ°€ 변경됨에 따라 이λ₯Ό μ‚¬μš©ν•˜λŠ” λͺ¨λ“  ν΄λΌμ΄μ–ΈνŠΈ 듀이 ν•¨κ»˜ μˆ˜μ •λ˜μ–΄μ•Ό ν•œλ‹€.
  • API κ°€ μˆ˜μ •λ˜μ–΄μ•Ό ν•˜λŠ” 경우 API URL 에 버전λͺ…을 μΆ”κ°€ν•˜κ±°λ‚˜ λ‹€λ₯Έ API λ₯Ό μ§€μ†μ μœΌλ‘œ μΆ”κ°€ν•˜κ²Œ λœλ‹€. κ·Έλ ‡κ²Œ 되면 API URL 관리가 μ–΄λ €μ›Œμ§„λ‹€.
  • REST API 에 νŠΉμ • μž‘μ—…μ„ μˆ˜ν–‰ν•˜κΈ° μœ„ν•΄ 데이터λ₯Ό μˆ˜μ§‘ν•΄μ•Ό ν•œλ‹€κ±°λ‚˜, ν•΄λ‹Ή μž‘μ—…μ΄ κ°€λŠ₯ν•œμ§€ μ—¬λΆ€λ₯Ό νŒλ‹¨ν•˜λŠ” 둜직 λͺ¨λ‘ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ κ°€μ Έκ°€μ•Ό ν•œλ‹€.

HATEOAS μŠ€ν”„λ§λΆ€νŠΈμ— μ μš©ν•˜κΈ°

1. μ˜μ‘΄μ„± μΆ”κ°€ν•˜κΈ°

μŠ€ν”„λ§ λΆ€νŠΈμ—λŠ” spring-boot-starter-hateoas λΌλŠ” HATEOAS λ₯Ό μœ„ν•œ μŠ€νƒ€ν„°λ₯Ό μ œκ³΅ν•œλ‹€. λ”°λΌμ„œ κ΄€λ ¨ 쒅속성을 pom.xml λ˜λŠ” build.gradle 에 μΆ”κ°€ν•œλ‹€.

pom.xml

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

build.gradle

1
implementation ('org.springframework.boot:spring-boot-starter-hateoas')

μ•„λž˜λŠ” spring-boot-starter-hateoas 의 μ€‘μš”ν•œ μ˜μ‘΄μ„± 쀑 ν•˜λ‚˜λŠ” HATEOAS κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” spring-hateoas 이닀.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.1.1.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.hateoas</groupId>
      <artifactId>spring-hateoas</artifactId>
      <version>0.25.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.plugin</groupId>
      <artifactId>spring-plugin-core</artifactId>
      <version>1.2.0.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

2. λ¦¬μ†ŒμŠ€ 링크λ₯Ό λ°˜ν™˜ν•˜λŠ” 컨트둀러 ꡬ성

Response 값에 {name} 에 κ΄€λ ¨λœ λͺ¨λ“  응닡을 κ²€μƒ‰ν•˜κΈ° μœ„ν•œ 링크λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ μ„€μ •ν•œλ‹€. κΈ°μ‘΄ ResponseEntity λŒ€μ‹  Resource<Todo> 객체λ₯Ό λ¦¬ν„΄ν•˜λ„λ‘ μ†ŒμŠ€λ₯Ό μˆ˜μ •ν•œλ‹€.

Resource class둜 도메인 객체λ₯Ό wrapping ν•΄μ£Όκ³  linkλ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@GetMapping("/users/{name}/todos/{id}")
public Resource<Todo> retrieveTodo(@PathVariable String name, @PathVariable int id) {
    Todo todo = todoService.retrieveTodo(id);
    if( todo == null ) {
        throw new TodoNotFoundException("Todo Not Found.");
    }
    // Todo 객체에 λŒ€ν•œ λ¦¬μ†ŒμŠ€ 객체λ₯Ό μƒμ„±ν•œλ‹€.
    Resource<Todo> todoResource = new Resource<Todo>(todo);     
    
    // ν˜„μž¬ 컨트둀러의 Name κ΄€λ ¨ν•œ λͺ¨λ“  할일 λͺ©λ‘μ„ μ‘°νšŒν•˜λŠ” 링크λ₯Ό parent ν•­λͺ©μœΌλ‘œ μΆ”κ°€ν•œλ‹€.
    ControllerLinkBuilder linkBuilder = linkTo(methodOn(this.getClass()).retrieveTodos(name)); 
    todoResource.add(linkBuilder.withRel("parent"));
    return todoResource;
}

3. 응닡에 HATEOAS 링크 정보 ν™•μΈν•˜κΈ°

curl λͺ…λ Ήμ–΄ ν˜Ήμ€ POSTMAN 으둜 μš”μ²­ν•œλ‹€.

1
curl http://localhost:8080/users/Jack/todos/1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "id": 1,
  "user": "Jack",
  "desc": "Learn Spring MVC",
  "targetDate": "2018-12-25T07:59:23.073+0000",
  "done": false,
  "_links": {
    "parent": {
      "href": "http://localhost:8080/users/Jack/todos"
    }
  }
}

ν•΄λ‹Ή URL 을 μš”μ²­ν•˜λ©΄ _links ν‚€ 값에 λͺ¨λ“  할일을 μ‘°νšŒν•  수 μžˆλŠ” 링크가 ν¬ν•¨λœλ‹€.

μ°Έκ³