Difference Between Swagger and HATEOAS

1. 개요

REST API를 설계하는 데 자주 사용되는 두 가지 인기 있는 접근 방식은 Swagger와 HATEOAS입니다. 두 방식 모두 API를 더 사용자 친화적이고 이해하기 쉽게 만들고자 하지만, 각각의 독특한 패러다임을 따릅니다.

이 튜토리얼에서는 Swagger와 HATEOAS의 차이점과 몇 가지 일반적인 사용 사례를 살펴보겠습니다.

2. Swagger란 무엇인가?

Swagger는 REST API를 구축하고 문서화하며 소비하기 위한 오픈 소스 도구 세트입니다. 개발자는 이러한 도구를 사용하여 OpenAPI Specification (OAS)을 기반으로 JSON 또는 YAML 파일을 통해 API의 구조를 설명할 수 있습니다.

Swagger의 주요 기능을 살펴보겠습니다.

2.1. 코드 생성

Swagger를 사용하면 인터랙티브한 API 문서, 코드 및 클라이언트 라이브러리를 자동으로 생성할 수 있습니다. Swagger는 또한 다양한 프로그래밍 언어로 서버 스텁과 클라이언트 SDK를 생성하여 개발 속도를 높입니다.

이것은 API 우선 접근 방식으로, 요구 사항과 애플리케이션을 유지 관리하는 사람들 간에 계약을 정의합니다.

개발자는 Swagger 사양 파일을 제공함으로써 SwaggerHub와 같은 도구를 사용하여 다양한 프로그래밍 언어의 보일러플레이트 코드를 생성할 수 있습니다. 예를 들어, 간단한 User 엔드포인트에 대한 YAML 템플릿을 살펴보겠습니다:

openapi: 3.0.1
info:
  title: User API
  version: "1.0.0"
  description: API for managing users.

paths:
  /users:
    get:
      summary: Get all users
      security:
        - bearerAuth: []  # Specifies security for this endpoint
      responses:
        '200':
          description: A list of users.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '401':
          description: Unauthorized - Authentication required
        '500':
          description: Server error

    post:
      summary: Create a new user
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewUser'
      responses:
        '201':
          description: User created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Invalid input
        '401':
          description: Unauthorized - Authentication required
        '500':
          description: Server error

  /users/{id}:
    get:
      summary: Get user by ID
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            example: 1
      responses:
        '200':
          description: User found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          description: Unauthorized - Authentication required
        '404':
          description: User not found
        '500':
          description: Server error

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT  # JWT specifies the type of token expected

  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: johndoe@example.com
        createdAt:
          type: string
          format: date-time
          example: "2023-01-01T12:00:00Z"

    NewUser:
      type: object
      properties:
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: johndoe@example.com
      required:
        - name
        - email

이 YAML 파일에 대한 개요를 살펴보겠습니다:

  • 일반 정보 (info): API 제목, 버전 및 간단한 설명이 포함됩니다.
  • 경로:
  • GET /users: 모든 사용자를 검색하며, 200 응답으로 User 객체 배열을 반환합니다.
  • POST /users: 새 사용자를 생성합니다. NewUser 스키마를 가진 요청 본문을 기대하며, 생성된 사용자 객체와 함께 201 응답을 반환합니다.
  • GET /users/{id}: 특정 ID로 사용자를 검색합니다. User를 찾지 못한 경우 404 응답을 포함합니다.
  • 구성 요소:
  • User 스키마: 사용자 객체의 구조를 정의하며, id, name, email, 및 createdAt 필드를 포함합니다.
  • NewUser 스키마: 새 사용자 생성을 위해 요청 본문에서 사용되며, nameemail 필드를 요구합니다.
  • SecuritySchemes: 이 섹션은 API가 보안을 처리하는 방식을 정의합니다. 이 경우 Bearer 토큰을 사용하는 bearerAuth 스킴을 지정합니다.

API에 대한 거의 모든 것을 정의하고 가장 일반적인 언어를 위해 자동 생성할 수 있어 이 과정의 속도를 높일 수 있습니다.

2.2. API 문서화

또한 프로젝트 코드에 직접 Open API 문서화 태그를 적용할 수 있습니다. 자동 생성 방식이나 수동 태깅 방식 중, 사용자 엔드포인트가 Java Spring REST 애플리케이션에서 어떻게 보일 수 있는지 살펴보겠습니다:

@RestController
@RequestMapping("/api/users")
public class UserController {
    // 필드 및 생성자
    @Operation(summary = "Get all users", description = "Retrieve a list of all users")
    @ApiResponses(value = {
      @ApiResponse(responseCode = "200", description = "List of users", 
        content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
      @ApiResponse(responseCode = "500", description = "Internal server error") })
    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        return ResponseEntity.ok()
          .body(userRepository.getAllUsers());
    }

    @Operation(summary = "Create a new user", description = "Add a new user to the system")
    @ApiResponses(value = {
      @ApiResponse(responseCode = "201", description = "User created", 
        content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
      @ApiResponse(responseCode = "400", description = "Invalid input") })
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<User> createUser(
      @RequestBody(description = "User data", required = true, 
        content = @Content(schema = @Schema(implementation = NewUser.class))) NewUser user) {
        return new ResponseEntity<>(userRepository.createUser(user), HttpStatus.CREATED);
    }

    @Operation(summary = "Get user by ID", description = "Retrieve a user by their unique ID")
    @ApiResponses(value = {
      @ApiResponse(responseCode = "200", description = "User found", 
        content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
      @ApiResponse(responseCode = "404", description = "User not found") })
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Integer id) {
        return ResponseEntity.ok()
          .body(userRepository.getUserById(id));
    }
}

가장 중요한 주석 몇 가지를 살펴보겠습니다:

  • @Operation: 각 API 작업에 대한 요약 및 설명을 추가하여 엔드포인트가 무엇을 하는지와 그 목적을 설명하는 데 도움이 됩니다.
  • @ApiResponse: HTTP 상태 코드에 대한 개별 응답을 정의하며, 설명과 예상되는 콘텐츠 유형 및 스키마를 포함합니다.
  • @Content: 응답 또는 요청 본문의 콘텐츠 유형(예: application/json)을 지정하고 데이터 직렬화를 위한 스키마를 제공합니다.
  • @Schema: 요청 및 응답 본문의 데이터 모델을 설명하며, 클래스(예: User)를 Swagger에 표시된 JSON 구조와 연결합니다.

2.3. 인터랙티브 콘솔

Swagger UI 콘솔은 OpenAPI 사양에서 동적으로 문서를 생성하는 인터랙티브한 웹 기반 인터페이스입니다. 이를 통해 개발자와 API 소비자는 시각적으로 엔드포인트를 탐색하고 테스트할 수 있습니다. 이 콘솔은 API 엔드포인트, 요청 매개변수, 응답, 오류 코드를 사용자 친화적인 레이아웃으로 구성하여 표시합니다.

각 엔드포인트는 사용자에게 매개변수 값, 헤더 및 요청 본문을 입력할 수 있는 필드를 제공하여 사용자가 콘솔에서 직접 실시간 요청을 할 수 있도록 합니다. 이 기능은 개발자가 API 동작을 이해하고, 통합을 검증하며, 별도의 도구 없이도 문제를 해결하도록 도와줍니다. 이는 API 개발 및 테스트를 위해 필수적인 리소스입니다. 예를 들어, 애완동물 가게의 Swagger UI 예제를 볼 수 있습니다.

2.4. API 우선 접근 방식의 이점

문서화에 대해 고유한 API 계약이나 템플릿을 사용해야 하는 이유는 무엇일까요?

템플릿은 API의 모든 엔드포인트가 일관된 구조를 따르도록 보장합니다. 이러한 일관성은 내부 개발 팀과 외부 소비자 모두에게 API를 이해하고 사용하는 데 단순화를 제공합니다. 예를 들어 개발자, QA 엔지니어 및 외부 이해관계자는 API의 기능과 구조에 대해 명확하고 공유된 이해를 가집니다.

더욱이, 클라이언트는 문서 내에서 API를 직접 실험할 수 있어 API를 보다 쉽게 채택하고 통합할 수 있으며, 추가적인 광범위한 지원이 필요하지 않습니다. 우리는 API의 구조와 응답이 사양을 충족하는지 확인하기 위해 자동화된 테스트를 설정할 수 있습니다.

3. HATEOAS란 무엇인가?

HATEOAS (Hypermedia as the Engine of Application State)는 REST 애플리케이션 아키텍처의 제약 조건입니다. HATEOAS는 더 넓은 REST 패러다임의 일부이며, 클라이언트가 서버가 동적으로 제공하는 하이퍼미디어를 통해 REST API와 상호작용해야 한다고 강조합니다. HATEOAS에서 서버는 자신의 응답에 링크를 포함시켜 클라이언트가 다음 작업을 안내합니다.

3.1. HATEOAS 예제

이제 Spring HATEOAS 애플리케이션 예제를 살펴보겠습니다. 먼저, 특정 표현 모델의 일부로 User를 정의해야 합니다:

public class User extends RepresentationModel<User> {
    private Integer id;
    private String name;
    private String email;
    private LocalDateTime createdAt;

    // Constructors, Getters, and Setters
}

이제 사용자 엔드포인트에 대해 이를 구현하는 방법의 예를 살펴보겠습니다:

@RestController
@RequestMapping("/api/hateoas/users")
public class UserHateoasController {
    // 필드 및 생성자

    @GetMapping
    public CollectionModel<User> getAllUsers() {
        List<User> users = userService.getAllUsers();

        users.forEach(user -> {
            user.add(linkTo(methodOn(UserController.class).getUserById(user.getId())).withSelfRel());
        });

        return CollectionModel.of(users, linkTo(methodOn(UserController.class).getAllUsers())
          .withSelfRel());
    }

    @GetMapping("/{id}")
    public EntityModel<User> getUserById(@PathVariable Integer id) {
        User user = userService.getUserById(id);
        user.add(linkTo(methodOn(UserController.class).getUserById(id)).withSelfRel());
        user.add(linkTo(methodOn(UserController.class).getAllUsers()).withRel("all-users"));
        return EntityModel.of(user);
    }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<EntityModel<User>> createUser(@RequestBody NewUser user) {
        User createdUser = userService.createUser(user);
        createdUser.add(
          linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).withSelfRel());
        return ResponseEntity.created(
          linkTo(methodOn(UserController.class).getUserById(createdUser.getId())).toUri())
            .body(EntityModel.of(createdUser));
    }
}

모든 사용자 검색을 위한 getAllUsers 엔드포인트의 샘플 응답을 살펴보겠습니다. 여기서는 링크를 통해 User의 동작 및 관련 리소스를 동적으로 발견할 수 있습니다:

[
    {
        "id": 1,
        "name": "John Doe",
        "email": "johndoe@example.com",
        "createdAt": "2023-01-01T12:00:00",
        "_links": {
            "self": {
                "href": "http://localhost:8080/users/1"
            }
        }
    },
    {
        "id": 2,
        "name": "Jane Smith",
        "email": "janesmith@example.com",
        "createdAt": "2023-02-01T12:00:00",
        "_links": {
            "self": {
                "href": "http://localhost:8080/users/2"
            }
        }
    }
]

3.2. 테스트

보다 자세히 이해하기 위해 컨트롤러에 대한 일부 통합 테스트를 살펴보겠습니다.

모든 사용자를 요청할 때 시작하겠습니다:

@Test
void whenAllUsersRequested_thenReturnAllUsersWithLinks() throws Exception {
    User user1 = new User(1, "John Doe", "john.doe@example.com", LocalDateTime.now());
    User user2 = new User(2, "Jane Smith", "jane.smith@example.com", LocalDateTime.now());

    when(userService.getAllUsers()).thenReturn(List.of(user1, user2));

    mockMvc.perform(get("/api/hateoas/users").accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$._embedded.userList[0].id").value(1))
      .andExpect(jsonPath("$._embedded.userList[0].name").value("John Doe"))
      .andExpect(jsonPath("$._embedded.userList[0]._links.self.href").exists())
      .andExpect(jsonPath("$._embedded.userList[1].id").value(2))
      .andExpect(jsonPath("$._embedded.userList[1].name").value("Jane Smith"))
      .andExpect(jsonPath("$._links.self.href").exists());
}

이 경우, 우리가 조회하는 각 Userid에 대한 상대 경로를 가져야 합니다.

freestar

이제 idUser를 가져오는 엔드포인트를 살펴보겠습니다:

@Test
void whenUserByIdRequested_thenReturnUserByIdWithLinks() throws Exception {
    User user = new User(1, "John Doe", "john.doe@example.com", LocalDateTime.now());

    when(userService.getUserById(1)).thenReturn(user);

    mockMvc.perform(get("/api/hateoas/users/1").accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.id").value(1))
      .andExpect(jsonPath("$.name").value("John Doe"))
      .andExpect(jsonPath("$.email").value("john.doe@example.com"))
      .andExpect(jsonPath("$._links.self.href").exists())
      .andExpect(jsonPath("$._links.all-users.href").exists());
}

이제 모든 사용자가 응답에 id 참조로 존재할 것으로 예상됩니다.

마지막으로, 새 사용자를 생성한 후, 우리는 새 참조가 응답에 포함될 것으로 예상합니다:

@Test
void whenUserCreationRequested_thenReturnUserByIdWithLinks() throws Exception {
    User user = new User(1, "John Doe", "john.doe@example.com", LocalDateTime.now());
    when(userService.createUser(any(NewUser.class))).thenReturn(user);

    mockMvc.perform(post("/api/hateoas/users").contentType(MediaType.APPLICATION_JSON)
        .content("{\"name\":\"John Doe\",\"email\":\"john.doe@example.com\"}"))
      .andExpect(status().isCreated())
      .andExpect(jsonPath("$.id").value(1))
      .andExpect(jsonPath("$.name").value("John Doe"))
      .andExpect(jsonPath("$._links.self.href").exists());
}

3.3. 핵심 요점

앞서 살펴본 대로, HATEOAS API는 응답에 링크를 포함하여 클라이언트의 행동을 안내합니다. 이는 클라이언트가 엔드포인트 경로를 하드 코딩할 필요성을 줄이고, API와의 보다 유연한 상호작용을 가능하게 합니다.

마찬가지로, 서버가 제공하는 링크를 따라 클라이언트가 다양한 상태 또는 작업을 동적으로 탐색할 수 있는 방법을 제공하여, 보다 적응적인 워크플로를 가능하게 합니다. 따라서 HATEOAS는 API가 탐색 가능하도록 만드는 궁극적인 단계로 생각할 수 있으며, 클라이언트가 그 행동을 이해할 수 있도록 합니다.

4. Swagger와 HATEOAS 간의 주요 차이점

Swagger와 HATEOAS의 차이를 설명해 보겠습니다:

| 측면 | Swagger | HATEOAS |
| —————————- | ———————————————————————————————————————————————————————————— | —————————————————————————————————————————————————————————————————————————— |
| API 문서화 | Swagger는 소비자가 사용 가능한 엔드포인트, 요청 매개변수 및 응답을 사전에 이해할 수 있도록 UI를 갖춘 자세하고 인간이 읽을 수 있는 API 문서를 제공합니다. | HATEOAS는 서버에서 응답 내에 반환된 하이퍼미디어 링크에 의존하므로 문서화가 더 암묵적입니다. 따라서 소비자는 생성을 통해 이러한 링크를 통해 동적으로 작업을 발견합니다. |
| 클라이언트 측 구현 | 클라이언트는 일반적으로 Swagger 사양을 기반으로 생성되거나 작성됩니다. API의 구조는 사전에 알려져 있으며, 클라이언트는 미리 정의된 경로에 따라 요청을 할 수 있습니다. | HATEOAS 클라이언트는 응답 내의 하이퍼미디어 링크를 통해 사용 가능한 작업을 발견하여 API와 동적으로 상호작용합니다. 클라이언트는 전체 API 구조를 사전에 알 필요가 없습니다. |
| 유연성 | Swagger는 미리 정의된 엔드포인트와 일관된 API 구조를 기대하고 있어 보다 경직되어 있습니다. 이는 문서 또는 사양을 업데이트하지 않고 API를 발전시키기 어렵게 만듭니다. | HATEOAS는 API가 발전할 수 있도록 더 큰 유연성을 제공하여 하이퍼미디어 기반 응답을 변경하더라도 기존 클라이언트에 영향을 주지 않습니다. |
| 소비자 용이성 | 자동 생성된 문서 또는 API 사양에서 직접 클라이언트 코드를 생성하는 도구에 의존하는 소비자에게는 쉽습니다. | 소비자에게는 더 복잡합니다. 이들은 응답을 해석하고 하이퍼미디어 링크를 따라 개별적으로 작업을 발견해야 합니다. |
| API 발전 | API 구조의 모든 변경은 Swagger 사양을 업데이트하고, 클라이언트 코드를 재생성하며, 사용자가 사용하도록 배포해야 합니다. | HATEOAS는 클라이언트가 하이퍼미디어를 통해 API를 탐색하므로 API가 발전할 때 업데이트가 덜 필요합니다. |
| 버전 관리 | Swagger는 일반적으로 명시적인 버전 관리와 함께 여러 버전의 API를 별도로 유지 관리해야 합니다. | HATEOAS는 클라이언트가 제공된 링크를 따라 동적으로 발전하므로 엄격한 버전 관리 없이 진화할 수 있습니다. |

HATEOAS는 응답에 포함된 하이퍼미디어 링크를 통해 클라이언트를 API 상호작용으로 동적으로 안내하는 데 중점을 두며, Swagger (또는 OpenAPI)는 API의 구조, 엔드포인트 및 작업을 설명하는 정적이고 인간이 읽을 수 있으며 기계가 읽을 수 있는 API 문서를 제공합니다.

5. 결론

이 기사에서는 Swagger와 HATEOAS에 대해 배우고, 주요 차이점을 강조하는 몇 가지 애플리케이션 예제를 살펴보았습니다. YAML 템플릿에서 소스 코드를 생성하는 방법이나 Swagger 주석을 사용하여 엔드포인트를 장식하는 방법을 보았습니다. HATEOAS에 관해서는 엔드포인트와 관련된 모든 리소스를 탐색하는 데 유용한 링크를 추가하여 모델 정의를 개선하는 방법을 살펴보았습니다.

항상 그렇듯이 코드는 GitHub에서 확인할 수 있습니다.

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다