1. 개요
RAML 예제 기사에서 RESTful API 모델링 언어 를 소개하고 Foo 라는 단일 엔터티를 기반으로 간단한 API 정의를 만들었습니다 . 이제 모두 동일하거나 유사한 GET, POST, PUT 및 DELETE 작업을 포함하는 여러 엔터티 유형 리소스가 있는 실제 API를 상상해 보십시오. API 문서가 얼마나 빨리 지루하고 반복적인지 알 수 있습니다.
이 기사에서는 RAML에서 자원 유형 및 특성 기능을 사용하여 공통 섹션을 추출하고 매개변수화 하여 자원 및 메소드 정의의 중복 을 제거하여 복사 및 붙여넣기 오류를 제거하고 API 정의를 더 간결하게 만드는 방법을 보여줍니다.
2. 우리의 API
리소스 유형 및 특성 의 이점을 보여주기 위해 Bar 라는 두 번째 엔터티 유형에 대한 리소스를 추가하여 원래 API를 확장할 것입니다 . 수정된 API를 구성할 리소스는 다음과 같습니다.
- GET /api/v1/foos
- POST /api/v1/foos
- GET /api/v1/foos/{fooId}
- PUT /api/v1/foos/{fooId}
- 삭제 /api/v1/foos/{fooId}
- GET /api/v1/foos/name/{이름}
- GET /api/v1/foos?name={이름}&ownerName={ownerName}
- GET /api/v1/bars
- POST /api/v1/bars
- GET /api/v1/bars/{barId}
- PUT /api/v1/bars/{barId}
- 삭제 /api/v1/bars/{barId}
- GET /api/v1/bars/fooId/{fooId}
3. 패턴 인식
API의 리소스 List을 읽으면 몇 가지 패턴이 나타나기 시작합니다. 예를 들어 단일 엔터티를 생성, 읽기, 업데이트 및 삭제하는 데 사용되는 URI 및 메서드에 대한 패턴이 있고 엔터티 컬렉션을 검색하는 데 사용되는 URI 및 메서드에 대한 패턴이 있습니다. 컬렉션 및 컬렉션 항목 패턴은 RAML 정의에서 리소스 유형 을 추출하는 데 사용되는 보다 일반적인 패턴 중 하나입니다 .
API의 몇 가지 섹션을 살펴보겠습니다.
[참고: 아래 코드 스니펫에서 세 개의 점(…)만 포함된 줄은 간결성을 위해 일부 줄이 생략되었음을 나타냅니다.]
/foos:
get:
description: |
List all foos matching query criteria, if provided;
otherwise list all foos
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Foo[]
post:
description: Create a new foo
body:
application/json:
type: Foo
responses:
201:
body:
application/json:
type: Foo
...
/bars:
get:
description: |
List all bars matching query criteria, if provided;
otherwise list all bars
queryParameters:
name?: string
ownerName?: string
responses:
200:
body:
application/json:
type: Bar[]
post:
description: Create a new bar
body:
application/json:
type: Bar
responses:
201:
body:
application/json:
type: Bar
사용된 HTTP 메서드를 포함하여 /foos 및 /bars 리소스 의 RAML 정의를 비교할 때 각각의 다양한 속성 간에 여러 중복성을 볼 수 있으며 패턴이 나타나기 시작하는 것을 다시 볼 수 있습니다.
리소스 또는 메서드 정의에 패턴이 있는 곳마다 RAML 리소스 유형 또는 특성 을 사용할 기회가 있습니다 .
4. 리소스 유형
API에 있는 패턴을 구현하기 위해 자원 유형 은 이중 꺾쇠 괄호(<< 및 >>)로 묶인 예약 및 사용자 정의 매개변수를 사용합니다.
4.1. 예약된 매개변수
두 개의 예약된 매개변수는 자원 유형 정의에서 사용될 수 있습니다.
- <<resourcePath>> 는 전체 URI( baseURI 다음 )를 나타내며,
- <<resourcePathName>> 은 중괄호 { }를 무시하고 맨 오른쪽 슬래시(/) 다음의 URI 부분을 나타냅니다.
리소스 정의 내에서 처리될 때 해당 값은 정의 중인 리소스를 기반으로 계산됩니다.
예를 들어 자원 /foos 가 주어지면 <<resourcePath>> 는 "/foos"로 평가되고 <<resourcePathName>> 은 "foos"로 평가됩니다.
/foos/{fooId} 리소스가 주어지면 <<resourcePath>> 는 "/foos/{fooId}"로 평가되고 << resourcePathName>> 은 "foos"로 평가됩니다.
4.2. 사용자 정의 매개변수
자원 유형 정의 에는 사용자 정의 매개변수도 포함될 수 있습니다. 값이 정의되는 리소스에 따라 동적으로 결정되는 예약된 매개변수와 달리 사용자 정의 매개변수는 이를 포함하는 리소스 유형 이 사용될 때마다 값을 할당해야 하며 해당 값은 변경되지 않습니다.
사용자 정의 매개변수는 리소스 유형 정의 의 시작 부분에서 선언할 수 있지만 그렇게 하는 것이 필수는 아니며 일반적인 관행도 아닙니다. 일반적으로 독자는 해당 이름과 사용되는 컨텍스트를 통해 의도된 용도를 파악할 수 있기 때문입니다.
4.3. 매개변수 기능
자원 정의에서 매개변수가 처리될 때 매개변수의 확장된 값을 변환하기 위해 매개변수가 사용될 때마다 소수의 유용한 텍스트 함수를 사용할 수 있습니다.
매개변수 변환에 사용할 수 있는 기능은 다음과 같습니다.
- ! 단일화하다
- ! 복수화하다
- ! 대문자
- ! 소문자
- ! 대문자
- ! 소문자
- ! 대문자
- ! 소문자
- ! 대문자
- ! 소문자
함수는 다음 구성을 사용하여 매개변수에 적용됩니다.
<< 매개변수 이름 | ! 함수 이름 >>
원하는 변환을 달성하기 위해 둘 이상의 함수를 사용해야 하는 경우 각 함수 이름을 파이프 기호("|")로 구분하고 사용되는 각 함수 앞에 느낌표(!)를 붙입니다.
예를 들어 자원 /foos 가 주어지면 << resourcePathName >>은 "foos"로 평가됩니다.
- << 리소스 경로 이름 | ! 단수화 >> ==> "foo"
- << 리소스 경로 이름 | ! 대문자 >> ==> "FOOS"
- << 리소스 경로 이름 | ! 단일화하다 | ! 대문자 >> ==> "FOO"
리소스 /bars/{barId} 가 주어지면 << resourcePathName >>은 "bars"로 평가됩니다.
- << 리소스 경로 이름 | ! 대문자 >> ==> "BARS"
- << 리소스 경로 이름 | ! 대문자 >> ==> "바"
5. 컬렉션에 대한 리소스 유형 추출
리소스 유형 을 사용하여 공통 속성을 캡처 하여 위에 표시된 /foos 및 /bars 리소스 정의 를 리팩터링해 보겠습니다 . 예약된 매개변수 <<resourcePathName>> 과 사용자 정의 매개변수 <<typeName>> 을 사용하여 사용된 데이터 유형을 나타냅니다.
5.1. 정의
다음은 항목 모음을 나타내는 리소스 유형 정의입니다.
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName>>
get:
description: Get all <<resourcePathName>>, optionally filtered
responses:
200:
body:
application/json:
type: <<typeName>>[]
post:
description: Create a new <<resourcePathName|!singularize>>
responses:
201:
body:
application/json:
type: <<typeName>>
API에서 데이터 유형은 기본 리소스 이름의 단일 버전인 대문자이기 때문에 사용자 정의 << typeName >> 매개변수 를 도입하는 대신 << resourcePathName >> 매개변수에 함수를 적용할 수 있습니다. , API의 이 부분에 대해 동일한 결과를 얻으려면:
resourceTypes:
collection:
...
get:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>[]
post:
...
type: <<resourcePathName|!singularize|!uppercamelcase>>
5.2. 애플리케이션
<< typeName >> 매개변수 를 통합하는 위의 정의를 사용하여 리소스 /foos 및 / bar 에 "컬렉션" 리소스 유형 을 적용하는 방법은 다음과 같습니다.
/foos:
type: { collection: { "typeName": "Foo" } }
get:
queryParameters:
name?: string
ownerName?: string
...
/bars:
type: { collection: { "typeName": "Bar" } }
리소스 유형 정의가 제공 해야 하는 모든 기능을 계속 활용하면서 두 리소스(이 경우에는 queryParameters 섹션 ) 간의 차이점을 통합할 수 있습니다.
6. 컬렉션의 단일 항목에 대한 리소스 유형 추출
이제 컬렉션의 단일 항목인 /foos/{fooId} 및 /bars/{barId} 리소스를 처리하는 API 부분에 초점을 맞추겠습니다. 다음은 /foos/{fooId} 에 대한 코드입니다 .
/foos:
...
/{fooId}:
get:
description: Get a Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a Foo
body:
application/json:
type: Foo
responses:
200:
body:
application/json:
type: Foo
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a Foo
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
/ bars/ { barId} 리소스 정의에는 GET, PUT 및 DELETE 메서드도 있으며 문자열 "foo" 및 "bar"(및 각각의 복수형 및/또는 대문자 형식).
6.1. 정의
방금 식별한 패턴을 추출하여 컬렉션의 단일 항목에 대한 리소스 유형 을 정의하는 방법은 다음과 같습니다.
resourceTypes:
...
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
put:
description: Update a <<typeName>>
body:
application/json:
type: <<typeName>>
responses:
200:
body:
application/json:
type: <<typeName>>
404:
body:
application/json:
type: Error
example: !include examples/Error.json
delete:
description: Delete a <<typeName>>
responses:
204:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
6.2. 애플리케이션
다음은 "항목" 리소스 유형 을 적용하는 방법입니다 .
/foos:
...
/{fooId}:
type: { item: { "typeName": "Foo" } }
...
/bars:
...
/{barId}:
type: { item: { "typeName": "Bar" } }
7. 특성
리소스 유형 이 리소스 정의에서 패턴을 추출하는 데 사용되는 반면 특성 은 리소스 전체에서 공통적인 메서드 정의에서 패턴을 추출하는 데 사용됩니다.
7.1. 매개변수
<< resourcePath >> 및 << resourcePathName >> 과 함께 하나의 추가 예약 매개변수를 특성 정의에 사용할 수 있습니다. << methodName >>은 HTTP 메소드(GET, POST, PUT, DELETE 등)로 평가됩니다. 특성 이 정의됩니다. 사용자 정의 매개변수는 특성 정의 내에 나타날 수도 있으며 적용되는 경우 적용되는 리소스의 값을 가져옵니다.
7.2. 정의
"항목" 리소스 유형 은 여전히 중복으로 가득 차 있습니다. 특성이 이러한 특성 을 제거하는 데 어떻게 도움이 되는지 살펴보겠습니다 . 요청 본문을 포함하는 모든 메서드에 대한 특성 을 추출하는 것으로 시작하겠습니다 .
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
이제 정상적인 응답에 본문이 포함된 메서드의 특성 을 추출해 보겠습니다 .
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
마지막으로 404 오류 응답을 반환할 수 있는 모든 메서드 의 특성 은 다음과 같습니다.
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
7.3. 애플리케이션
그런 다음 이 특성 을 리소스 유형 에 적용합니다 .
resourceTypes:
collection:
usage: Use this resourceType to represent any collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: Create a new <<resourcePathName|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: | [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
리소스 내에 정의된 메서드에 특성 을 적용할 수도 있습니다 . 이는 자원-방법 조합이 하나 이상의 특성 과 일치하지만 정의된 자원 유형 과 일치하지 않는 "일회성" 시나리오에 특히 유용합니다 .
/foos:
...
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
8. 결론
이 사용방법(예제)에서는 RAML API 정의에서 중복을 크게 줄이거나 경우에 따라 제거하는 방법을 보여 주었습니다.
먼저 리소스의 중복 섹션을 식별하고 해당 패턴을 인식하고 리소스 유형 을 추출 했습니다. 그런 다음 특성 을 추출하기 위해 리소스 전체에서 공통적으로 사용되는 방법에 대해 동일한 작업을 수행했습니다 . 그런 다음 리소스 유형 과 정의된 리소스 유형 중 하나와 정확히 일치하지 않는 "일회성" 리소스-방법 조합에 특성 을 적용하여 추가 중복을 제거할 수 있었습니다 .
그 결과 단 두 개의 엔터티에 대한 리소스가 포함된 간단한 API가 177개에서 100줄이 조금 넘는 코드로 줄었습니다. RAML 리소스 유형 및 특성 에 대해 자세히 알아 보려면 RAML.org 1.0 사양 을 방문하십시오 .
이 예제의 전체 구현은 github 프로젝트 에서 찾을 수 있습니다 .
다음은 전체 최종 RAML API입니다.
#%RAML 1.0
title: Baeldung Foo REST Services API
version: v1
protocols: [ HTTPS ]
baseUri: http://rest-api.baeldung.com/api/{version}
mediaType: application/json
securedBy: basicAuth
securitySchemes:
basicAuth:
description: |
Each request must contain the headers necessary for
basic authentication
type: Basic Authentication
describedBy:
headers:
Authorization:
description: |
Used to send the Base64 encoded "username:password"
credentials
type: string
responses:
401:
description: |
Unauthorized. Either the provided username and password
combination is invalid, or the user is not allowed to
access the content provided by the requested URL.
types:
Foo: !include types/Foo.raml
Bar: !include types/Bar.raml
Error: !include types/Error.raml
resourceTypes:
collection:
usage: Use this resourceType to represent a collection of items
description: A collection of <<resourcePathName|!uppercamelcase>>
get:
description: |
Get all <<resourcePathName|!uppercamelcase>>,
optionally filtered
is: [ hasResponseCollection: { typeName: <<typeName>> } ]
post:
description: |
Create a new <<resourcePathName|!uppercamelcase|!singularize>>
is: [ hasRequestItem: { typeName: <<typeName>> } ]
item:
usage: Use this resourceType to represent any single item
description: A single <<typeName>>
get:
description: Get a <<typeName>>
is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
put:
description: Update a <<typeName>>
is: [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
delete:
description: Delete a <<typeName>>
is: [ hasNotFound ]
responses:
204:
traits:
hasRequestItem:
body:
application/json:
type: <<typeName>>
hasResponseItem:
responses:
200:
body:
application/json:
type: <<typeName>>
hasResponseCollection:
responses:
200:
body:
application/json:
type: <<typeName>>[]
hasNotFound:
responses:
404:
body:
application/json:
type: Error
example: !include examples/Error.json
/foos:
type: { collection: { typeName: Foo } }
get:
queryParameters:
name?: string
ownerName?: string
/{fooId}:
type: { item: { typeName: Foo } }
/name/{name}:
get:
description: List all foos with a certain name
is: [ hasResponseCollection: { typeName: Foo } ]
/bars:
type: { collection: { typeName: Bar } }
/{barId}:
type: { item: { typeName: Bar } }
/fooId/{fooId}:
get:
description: Get all bars for the matching fooId
is: [ hasResponseCollection: { typeName: Bar } ]