[스프링] REST API (2) - HATEOAS 구현


구현 커밋

HATEOAS
  • 애플리케이션의 상태는 하이퍼링크를 통해 전이되어야 함
  • html은 href를 통해서 링크로 상태 전이를 하므로 HATEOAS를 만족함
  • JSON에서는?
    • LINK 헤더를 사용하면 메시지에 링크를 포함할 수 있음
    • LINK 헤더는 이미 표준이 나와있는 거라서 누구나 해석이 가능
  • HATEOAS를 왜 해야하냐?
    • 서버와 클라이언트가 각각 독립적으로 진화한다.
    • 서버의 기능이 변경되어도 클라이언트를 업데이트할 필요가 없다
      • 즉, 신규 API 버전이 나와도 변경된 링크로 클라이언트에게 전달하고, 클라이언트가 서버 메시지를 해석한 링크로 상태 전이를 한다면 클라이언트는 수정될 필요가 없음
  • 애플리케이션 상태 전이의 late binding
    • 어디서 어디로 전이가 가능한지 미리 결정되지 않는다. 어떤 상태로 전이가 완료되고 나서야 그 다음 전이될 수 있는 상태가 결정된다.
    • 링크는 동적으로 변경될 수 있다.

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-hateoas'

구현해보면?

회원가입 API 예시

@PostMapping("/signup")
public ResponseEntity signup(@RequestBody SignUpRequestDto signUpRequestDto) {
    Account account = accountService.createNewAccount(signUpRequestDto)
    SignUpResultDto signUpResultDto = modelMapper.map(account, SignUpResultDto.class);
    EntityModel<SignUpResultDto> entityModel = EntityModel.of(signUpResultDto);
    entityModel.add(WebMvcLinkBuilder.linkTo(RestAccountController.class).slash("signup").withSelfRel());
    entityModel.add(WebMvcLinkBuilder.linkTo(RestAccountController.class).slash("login").withRel("login"));
    return ResponseEntity.created(uri).body(entityModel);
}

EntityModel 클래스를 이용해서 해당 요청으로부터 상태 전이가 가능한 링크를 담아 반환해준다.


회원가입 API 예시 테스트 코드

@DisplayName("회원가입 - 정상 입력")
@Test
void signupTest() throws Exception {
    /* given */
    SignUpRequestDto signUpRequestDto = SignUpRequestDto.builder()
            .nickname("blupine")
            .email("test@email.com")
            .password("asdfasdf")
            .build();

    /* when & then */
    mockMvc.perform(post("/api/signup")
            .contentType(MediaType.APPLICATION_JSON)
            .accept(MediaTypes.HAL_JSON)
            .content(objectMapper.writeValueAsString(signUpRequestDto)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("nickname").hasJsonPath())
            .andExpect(jsonPath("email").hasJsonPath())
            .andExpect(jsonPath("joinedAt").hasJsonPath())
            .andExpect(jsonPath("url").hasJsonPath())
            .andExpect(jsonPath("_links").hasJsonPath())
            .andExpect(jsonPath("_links.self").hasJsonPath())
            .andExpect(jsonPath("_links.login").hasJsonPath());
            
    /* then */
    Account accountAfter = accountRepository.findByNickname("blupine");
    Assertions.assertNotNull(accountAfter);
}

요청 결과

{
    "nickname": "testname1",
    "email": "Test@testemail.com",
    "joinedAt": null,
    "url": null,
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/signup"
        },
        "login": {
            "href": "http://localhost:8080/api/login"
        }
    }
}





© 2020.02. by blupine