브레이킹 체인지는 무엇인가요?

사용자들이 대응하지 않으면 API를 올바르게 사용하지 못하게 만드는 변화를 말합니다.

브레이킹 체인지는 다음이 변경될 경우에 발생할 수 있습니다.

  • 입력 / 출력 데이터
  • 파라미터
  • 리스폰스 상태나 에러
  • 목표
  • 흐름
  • 보안

출력 데이터의 브레이킹 체인지

속성의 이름을 바꾸거나 이동시키거나 속성의 타입을 변경하는 것은 출력 데이터로 인한 브레이킹 체인지를 유발합니다.

예를 들어 코드숨 공부방 예약 정보를 다음과 같이 응답하고 있다고 가정해 보겠습니다.

{
  "id": 1,
  "plan": {
    "content": "웹 API 디자인 책 읽기"
  },
  "date": "2022-10-27"
}

위의 응답에서 plan이라는 이름을 report라는 이름으로 변경하거나, plan의 타입이 문자열로 변경되거나, content의 위치를 가장 상단으로 옮기는 등의 변화가 있다면, 이 API를 사용하던 컨슈머는 대응하지 않는 이상 올바르게 동작하지 않을 것입니다.

{
  "id": 1,
  "content": "웹 API 디자인 책 읽기",
  "date": "2022-10-27"
}

출력 데이터에 적용하는 브레이킹 체인지에 따른 결과는 아래와 같습니다.

변경 결과
속성의 이름을 변경 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )
속성의 위치 이동 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )
필수 속성의 제거 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )
필수 속성에서 선택 사항으로 변경 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )
속성의 타입 변경 파싱 에러
속성의 포맷 변경 파싱 에러
속성의 특징 변경 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )
속성의 의미 변경 최악의 결과
enum 값 추가 구현에 따라 다름 ( UI데이터 사라짐, 데이터 오염 충돌 등등 )

입력 데이터와 파라미터에서의 브레이킹 체인지

출력 데이터와 마찬가지로 속성의 이름이나 타입, 위치를 변경하는 것은 브레이킹 체인지를 유발합니다.

만약 기존에 이메일 인증에 사용되는 난수를 ‘token’이라는 이름으로 주고 있었는데, 이를 ‘emailToken’이라는 이름으로 변경했을 경우, 이를 제때 수정하지 않은 컨슈머는 이메일 인증 시 에러 메시지를 받게 될 것입니다.

구현에 따라 달라지겠지만 ui 데이터나 데이터 오염 충돌 같은 현상들이 나타나게 됩니다.

입력 데이터에 적용하는 브레이킹 체인지에 따른 결과는 아래와 같습니다.

변경 결과 출력데이터와의 비교
속성의 이름을 변경 API 에러 동일
속성의 위치 이동 API 에러 동일
필수 속성의 제거 API 에러 동일
필수 속성에서 선택 사항으로 변경 API 에러 반대(필수 속성을 선택 사항으로 변경했을 때와 동일)
속성의 타입 변경 API 에러 동일
속성의 포맷 변경 API 에러 동일
속성의 특징 변경 API 에러 반대(특징을 확장한 때와 동일)
속성의 의미 변경 최악의 경우(주로 프로바이더에게 영향을 미침) 반대(컨슈머 영역에 영향을 미침)
enum 값 추가 API 에러 반대(값을 추가할 때와 같음)
필수 속성 추가 API 에러 에러발생 x

성공과 에러 피드백에서 브레이킹 체인지

에러를 응답하는 데이터의 형식이나 상태 코드가 변하는 등의 변화가 있을 경우 브레이킹 체인지가 발생할 수 있습니다.

예를 들어서 사용자가 필수 입력 항목을 빠트렸을 때 응답을 예시로 들어보겠습니다.

{
  "message": "invalid request",
  "items": [
    { "type": "MISSING_PARAMETER", "field": "name" },
    { "type": "MISSING_PARAMETER", "field": "address" }
  ]
}

여기서 만약 다음과 같이 items 대신에 errors라고 변경한다면, items로 처리하고 있던 컨슈머는 올바르게 동작하지 않을 것입니다.

{
  "message": "invalid request",
  "errors": [
    { "type": "MISSING_PARAMETER", "field": "name" },
    { "type": "MISSING_PARAMETER", "field": "address" }
  ]
}

items 뿐만 아니라 type이 변경되어도 브레이킹 체인지가 될 수 있습니다.

RFC 7231 에 따르면 같은 클래스의 상태 코드를 추가하거나 변경하는 것은 브레이킹 체인지가 되면 안 됩니다. 따라서 컨슈머는 상태 코드에 따라 다르게 처리하면 안 됩니다.

성공 / 에러 피드백에서 브레이킹 체인지를 유발하지 않는 유일한 방법은 HTTP 상태 코드를 제거하는 것뿐입니다.

목표와 흐름에서 브레이킹 체인지

위의 예제로 인해 목표에서 입력 출력 피드백 수정이 브레이킹 체인지를 유발하는 것을 보여줬습니다.

그 외에 매우 명백한 두 개의 브레이킹 체인지를 유발을 합니다.

  • 목표의 이름 변경

예를 들어 공부방의 api 표현을 reservations에서 reservate로 변경하게 된다면 기존에 사용중이던 컨슈머들은 404 Not Found를 응답받게 됩니다.

// 변경 
GET /reservations
204 No Content

// 변경 
GET /reservations
404 Not Found
  • 목표 제거

예를 들어 공부방의 예약 목록을 볼 수 있는 api를 제거하게 된다면 컨슈머가 해당 리소스를 사용할 때 405 Method Not Allowed 응답받게 됩니다.

// 제거 
GET /reservations
204 No Content

// 제거 
GET /reservations
405 Method Not Allowed
  • 목표 추가

그렇다면 목표를 새로 추가하는 것은 어떨까요? 이메일 인증 시 인증 리퀘스트 후에 이메일 토큰을 검증하는 ‘토큰 검증’ 목표가 새로 추가되었다고 생각해 봅시다. 그러면 관련 코드를 업데이트하지 않은 컨슈머들은 ‘토큰 검증’목표를 호출하지 않을 것이고, 이메일 인증이 정상적으로 이루어지지 않을 것입니다. 게다가 에러도 없기 때문에 컨슈머들은 문제가 발생한 줄도 모를 것입니다. 이를 사일런트 브레이킹 체인지라고 합니다.

보안에서 브레이킹 체인지

API를 변경은 보안에 영향을 주어 보안 취약점이 발생할 수 있습니다. 따라서 모든 API 변경 사항은 보안을 염두에 두어야 합니다.

예를 들어 응답에 새로운 데이터를 추가하기로 하였는데, 이 정보가 컨슈머에게 전달되면 안 되는 정보가 포함되지 않도록 주의해야 합니다.

{
  "reservations": [
    { "id": 1, "content": "웹 API 디자인 책 읽기", "date": "2022-10-27" }
  ]
}

컨슈머가 예약 정보에 누가 예약자명을 표기하기 위해 사용자의 이름을 추가했다고 가정해 봅시다.

{
  "reservations": [
    { "id": 1, "content": "웹 API 디자인 책 읽기", "date": "2022-10-27", "user": "홍길동" }
  ]
}

사용자의 이름은 민감한 정보일 수 있으므로, 이러한 변경사항이 보안에 취약점이 될 수 있습니다.

또는 보안적 변경사항이 컨슈머에게는 브레이킹 체인지가 될 수 있습니다. 예를 들어 사용자 정보를 조회할 수 있는 권한이 기본이었는데, 보안상 취약점을 보완하기 위해서 기본값을 조회할 수 없는 것으로 변경할 경우, 이미 사용자 정보를 조회하고 있는 컨슈머들에게는 브레이킹 체인지가 될 수 있습니다.

브레이킹 체인지를 유발하지 않고 API를 발전시키는 방법은?

버전을 지정하여 브레이킹 체인지를 관리하는 방법이 있습니다.

예를 들어, 코드숨 공부방 예약하기 API 두 번째 버전이 출시했습니다. 기존엔 좌석 지정 없이 예약을 할 수 있었던 반면에, 새롭게 특정 좌석을 예약하는 기능이 추가되었습니다. 컨슈머들은 특정한 좌석을 예약 기능을 사용하기 위해서는 이 새로운 버전의 API로 변경해야 합니다. 그러려면 컨슈머들은 그들의 코드를 일부 수정해야 합니다. 좌석 지정 같은 새로운 기능이 하위 호환성을 보장하지 않으면서 같은 역할을 하는 기능으로 변경되었습니다.

이런 상황에서 API 디자이너의 관점에서 버전 관리 작업은 이전 버전과 호환되지 않는 디자인을 만드는 데 있습니다. API의 두 가지 버전을 별도로 구분하기 위해 브레이킹 체인지를 도입하고 도메인 이름을 다르게 변경합니다.

API 버전 관리란 단순한 API 디자인을 넘어서는 주제이며, API 버전 관리는 디자인 외에도 세부 구현과 제품 관리에 영향을 미칩니다. API 버전 관리에 대해 살펴보기 전에 API 버전 관리와 구현상의 버전 관리가 명백히 다르다는 것을 알고 있어야 합니다.

공부방 초기 예약 API는 날짜를 지정하면 해당 날짜에 예약하는 것만 제공했습니다. 그렇지만 요구 사항이 추가되면서 새로운 기능들이 생겨났습니다.

| API 버전 | 구현의 버전 | | — | — | | API v1.0 | Node Reservation Engine v.1.0.0 | | API v1.1 | Node Reservation Engine v.1.1.0 (새로운 좌석 예약 기능 추가) | | API v2.0 | Java Reservation Engine v.2.0.0 (새로운 기능 추가와 과거 목표들을 현재 기준에 맞게 일관성 차원에서 수정 Node → Java로 언어변경) |

v1.0에서는 단순 예약만 가능했다면 v1.1에는 좌석을 예약하는 기능이 추가해서 출시했습니다. 하지만 동시에 유저가 좌석을 예약한다면 응답 시간이 오래 걸리고 현저히 느려집니다. 그래서 좌석 예약을 메시지 큐에 담아 비동기적으로 처리하여 API에 영향을 주지 않게 변경했습니다.

점차 서비스가 늘어날 것을 예상해서 Node를 사용하는 것보단 Java를 사용하는 것이 더 좋을 것 같다는 결정을 하게 되었습니다. 공부방 v2.0은 기존과 다른 언어로 사용하지만 v1.1의 좌석 예약을 그대로 사용하게끔 했습니다.

버전 v1.1은 세부 구현의 버전 v.2.0의 소프트웨어 컴포넌트 개발에 있어서 잘 알려진 버전 체계인 시멘틱 버저닝을 따르고 있습니다.

시멘틱 버저닝이란?

소프트웨어 컴포넌트 개발에 있어서 잘 알려진 버전 체계로 이 버전 체계에서는 버전 번호를 구성할 때 세 자리 숫자를 이용하여 메이저, 마이너, 패치의 형태로 만듭니다.

API에서의 시멘틱 버저닝이란?

브레이킹과 논 브레이킹 두 자리의 숫자로만 구성되어 있습니다. 이 숫자들을 가지고 하위 호환과 하위 호환이 되지 않는 API의 변경 사항을 추적할 수 있습니다.

API와 세부 구현의 버저닝은 서로 다르며, 컨슈머들은 오직 브레이킹 체인지에 대한 버전만 신경 씁니다. 그렇다면 컨슈머들은 어떻게 사용하고 싶은 특정 API 버전을 우리에게 알릴 수 있을까요?

컨슈머 관점에서 API 버전을 표현하는 여섯가지 방법이 있습니다.

  1. 경로
  2. 도메인 URL의 경로나 도메인을 변경하는 버저닝은 가장 일반적인 선택지로 어떤 버전의 API를 사용하는지 확인하기 위해 URL만 확인하면 되기 때문에 컨슈머들이 익숙하고 사용하기 편합니다.
  3. 쿼리 파라미터 기술적으로 추가된 파라미터와 기능적으로 추가된 파라미터와 혼재되기 때문에 API 디자이너 관점에서 깔끔하지 않아 추천하지 않는 방법입니다.
  4. 커스텀 헤드 Content-type 버저닝은 전문가 관점에서는 흥미로운 방법이지만 대부분 사람은 HTTP 헤더가 전혀 복잡하지 않아도 사용하기 꺼리는 게 현실입니다. 게다가 표준이 아닌 커스텀 헤더 때문에 더 악화되고 있습니다.
  5. 콘텐츠 네고시에이션 콘텐츠 네고시에이션 레벨에서 필요한 API 버전을 명시하는 것도 방법입니다. 이 방법을 사용하기 위해서는 커스텀 미디어 타입으로 Custom-type 헤더를 사용해 버전을 지정할 수 있습니다.
  6. 컨슈머의 설정 컨슈머가 설정할 필요가 없다는 점에서 완전히 컨슈머 친화적입니다. 하지만 작은 단점이 있습니다. 한 버전에서 다른 버전으로 전환하기 위해서 이 구성을 업데이트해야만 한다는 점인데, 이는 다른 버전의 API를 테스트할 때 성가시게 작용합니다.

이제 API 버저닝 세분화에 대해 알아보겠습니다. API의 버저닝을 할 때 API 전체의 버전을 관리하는 것이 관행이지만 반드시 그럴 필요는 없습니다. 유즈 케이스와 API의 타입에 따라 다른 선택지가 더욱더 효과적일 수 있습니다. REST API의 경우 API 수준 이외에도 목표/운영 수준, 그리고 데이터/메시지 수준에서도 얼마든지 버저닝이 가능합니다. 버저닝 세분화의 수준별로 어떠한 장단점이 있는지 알아보겠습니다.

세분화 정도 장점 단점 추천 여부
API 어떠한 버전의 동작이나 리소스가 함께 하는지 고민할 필요가 없다. 브레이킹 체인지를 유발한 변경이 무엇인지 단서를 제공할 수 없다. REST API의 기본
리소스 변경에 대한 힌트를 제공한다. 어떤 버전끼리 같이 동작하는지 추측이 불가능하다. 리소스가 완전히 독립적일 때만 사용
목표/동작 어떤 목표가 변경되었는지 표시가 가능하다. 어떤 버전끼리 같이 동작하는지 추측이 불가능하다. 동작이 완전히 독립적일 때만 사용
데이터/메시지 어떤 데이터/메시지가 변경되었는지 표시가 가능하다. 데이터/메시지가 어떤 버전끼리 같이 동작하는지 추측이 불가능하고 HTTP에서 리퀘스트/리스폰프 바디에서만 유효하다. API 레벨 세분화 과정에서 병용할 수 없다.

각 세분화 별로 장단점이 존재하지만 REST API에서는 가장 일반적으로 API 레벨의 버저닝 전략이 채택됩니다. 버저닝은 API 디자인 그 이상의 영역에도 영향을 끼치는 것을 주의해야 합니다. API 디자인의 변경이 브레이킹 체인지를 유발하지 않더라도, 변경 사항들은 신중하게 기록으로 남겨 컨슈머들에게 전달할 수 있어야 합니다.

확장 가능한 api 디자인하기

소프트웨어 디자인의 기본 원칙은 확장성입니다. 데이터, 상호작용, 흐름, 적절한 수준의 버저닝 세분화에 대한 신중한 디자인을 통해 가능한 API를 구성할 수 있습니다. 이를 통해 API의 진화를 촉진시킬 수 있으며, 더 중요한 것은 브레이킹 체인지에 대한 위험성을 줄일 수 있습니다. API가 커질수록, 발전의 횟수가 늘어날수록, 브레이킹 체인지의 위험도 커집니다. 위 문제를 해결하기 위해서 큰 API를 만드는 대신, 작은 API를 여러 개 만드는 겁니다.

확장 가능한 데이터 디자인

코드숨 공부방 예약하기로 예를 들어보겠습니다.

// "공부방 예약" 대한 리스폰스

1

위처럼 예약의 리스폰스로 예약 ID만 반환된다면 컨슈머는 혼란스러울 것입니다. 리스폰스의 Content-typeapplication/json이나 application/xml이 반환되는 것이 일반적인데, text/plain으로 순수한 문자열 리스폰스만 반환되기 때문입니다. 어색하지만 사용할 수는 있습니다. 하지만 많은 후속 호출들을 피하기 위해 한 번에 리소스에 대한 응답을 보내기로 결정해 응답 데이터를 변경했습니다.

// ID 대신 전체 리소스를 반환
{
  "id": 1,
  "reservationDate": "2022-10-29",
  "hasPlan": true,
  "hasRetrospective": false
}

하지만 이 변경은 브레이킹 체인지입니다. 만약 리스폰스가 처음부터 ID를 포함한 오브젝트였다면, 다른 속성 추가는 전혀 문제가 되지 않았을 것입니다. 이렇게 모든 고레벨의 데이터(REST API의 리소스들)은 반드시 오브젝트라는 봉투에 담겨 확장성을 보장하고 브레이킹 체인지의 위협을 줄여야 합니다.

그렇다면 봉투 안의 데이터 자체는 어떻게 해야 할까요? 불리언(Boolean)의 사용에 주의해야 하며, 자기 설명적인 데이터를 제공해야 합니다.

또다시 코드숨 공부방 예약으로 예시를 들어보겠습니다. 위의 마지막 응답 값을 다시 보면, hasPlanhasRetrospective라는 불리언 속성이 있습니다. 해당 값이 true 라면 계획 혹은 회고를 작성했다는 뜻이고, false 라면 아직 작성하지 않았다는 뜻입니다.

하지만 여기에 새로운 상태가 도입되어야 한다면 어떨까요? 또다시 불리언 속성을 추가해야 할 것입니다. 이러한 새로운 속성을 리스폰스에 추가해도 브레이킹 체인지가 발생하지는 않지만, 컨슈머들은 이러한 새로운 상태에 대해 코드를 업데이트 하지 않고서는 알 수가 없습니다.

이렇게 여러 개의 불리언 상태 속성을 추가하는 상황을 피하기 위해 status 라는 속성을 추가할 수 있습니다. 이렇게 만들면 새로운 상태가 추가될 일이 있어도 속성을 그때마다 추가할 필요가 없습니다.

{
  "id": 1,
  "reservationDate": "2022-10-29",
  "status": "RETROSPECTIVE_WAITING"
}

하지만 값을 enum에 추가하는 행위도 브레이킹 체인지를 유발합니다. 상태를 자기 설명적인 오브젝트로 만들어 코드와 사람이 이해할 수 있는 레이블로 구성되게 하는 것이 위험을 줄이는 방법입니다.

예약 리스폰스에 계획 작성일과 회고 작성일 속성이 추가되어야 한다면, 유사한 데이터끼리 묶어 하나의 목록으로 제공할 수 있습니다.

"planCreateDate": "2022-10-28",
"retrospectiveCreateDate": "2022-10-29"

{
  "id": 1,
  "reservationDate": "2022-10-29",
  "events": [
      {
        "planCreateDate": "2022-10-28",
        "status": "RESERVED"
      },
      {  
        "retrospectiveCreateDate": "2022-10-29",
        "status": "RETROSPECTIVE_WAITING"
      }
  ]
}

위 처럼 제공한다면 나중에 취소일자와 취소상태가 추가된다고 해도 추가를 쉽게 할 수 있으며 브레이킹 체인지의 위험도 줄일 수 있습니다.

따라서 속성의 타입은 확장성을 고려해 신중하게 선택하고, 브레이킹 체인지 유발 가능성을 줄이기 위해 항상 자기 설명적인 데이터를 제공하는 것도 고민해야 합니다. 만약 속성들이 유사하다면, 항상 이것들을 하나의 리스트로 제공하는 것을 고려하고 가능하다면 자기 설명적인 데이터를 이용해야 합니다.

확장 가능한 상호작용 디자인하기

반환하는 것은 일관성을 유지하고 에러를 피하도록 노력해야합니다.

{
  "erros": [
    { "source": "amount",
      "type": "MISSING_MANDATORY_ATTRIBUTE",
      "message": "필수값인 금액이 누락되었습니다." }
  ]
}

위의 예제처럼 에러들에 타입을 줄 수 있습니다. 이러한 타입은 포괄적이기 때문에, 우리는 이를 다른 필 수 속성에도 유사하게 쓸 수 있습니다. MISSING_AMOUNT라는 타입을 만들었다면, 우리는 그것을 재사용할 수는 없으며 대신 컨슈머가 코드를 업데이트하지 않고서는 해석할 수 없는 새로운 유형의 에러를 도입해야 할 겁니다. 일반적으로 type(타입) 값이 포괄적일수록 에러 피드백의 확장 가능성이 커집니다.

컨슈머가 송금 목록을 요청할 때 test=2라는 파라미터로 리퀘스트를 전송하면 에러 방지를 위해서 어떻게 해야 할까요? API는 엄격하게 “죄송합니다. test라는 파라미터를 이해할 수 없습니다”라고 에러 메시지를 반환할 수도 있습니다. 또는 API는 단순하게 이런 이해할 수 없는 파라미터는 취급하지 않고 리퀘스트를 처리해 반환할 수도 있습니다. 이 경우는 test를 무시해도 컨슈머 측에 의도치 않은 부작용이 발생하지 않는 경우에만 가능하다는 걸 기억해둡시다.

만약에 은행 API의 컨슈머가 PageSize=150 파라미터로 송금 목록 리퀘스트를 보냈는데, 실제로 최대 전송 가능한 페이지는 100페이지가 최고라면 어떻게 해야 할까요? 마찬가지로, API는 에러 메시지를 반환하는 대신에 100개의 요소만 반환할 수 있습니다. 하루치의 송금 목록의 경우에는 최대 개수가 50개로 줄어들어도 컨슈머들은 불평을 하지 않을 겁니다.

또한, 은행 API가 최대 송금 금액이 $10,000인데, amount(금액)을 15000으로 입력한다면 어떻게 해야 할까요? $15,000 대신에 $10,000달러를 송금해야 할까요? 당연히 안됩니다. API 디자이너로서, 우리는 에러 반환을 생략할 수도 있습니다. 그렇지만 모두 그래서는 안 됩니다.

이 부분은 세부 구현의 몫입니다.

API 디자이너로서, 여러분은 에러와 알 수 없거나 잘못된 파라미터에 대한 정책을 정의해야 합니다. 문제를 별도로 고려하지 않고 기본값을 우선으로 사용하여 브레이킹 체인지의 위험을 줄이시겠습니까? 아니면 더 안전하게 컨슈머의 정확성을 높이도록 엄격한 에러를 반환하시겠습니까? 변경이 발생한다면 컨슈머는 컨슈머는 업데이트를 해야 합니다. 여러분의 접근 방법은 API 와 그 API에 속한 각각의 목표들이 처한 컨텍스트에 따라 달라질 겁니다.

확장 가능한 흐름 디자인하기

흐름에 속한 각 목표를 어떻게 디자인하는가와 흐름 그 자체는 API의 확장성에 영향을 미칩니다. 확장성이란 브레이킹 체인지를 겪을 위험을 적게 하면서, 수정을 수행할 수 있도록 하는 것에만 국한되는 것이 아닙니다. 확장성은 API가 최초에 의도된 목적보다 더 넓은 의미의 유즈케이스에 대응할 수 있게 해주는 것이기도 합니다.

항상 작업 중인 현재의 특정한 유즈케이스나 흐름에 매몰되지 않고 더 넓게 봐야 합니다. 특히 UI 흐름 같은 것들은 프로세스와 상관이 없습니다. 또한, 각 디자인 시 단계별로 독립적으로 동작할 수 있게 해야 합니다. 보다 범용적으로 쓸 수 있는 입력과 출력을 택하면, 이를 달성하는 데 도움이 됩니다.