호환성

이번 페이지에서는 버전 관리 섹션에 나오는 이전 버전과 호환되는 변경사항과 이전 버전과 호환되지 않는 변경사항에 대해서 자세하게 설명합니다.

이전 버전과 호환되지 않는 변경사항으로 간주할 수 있는 기준이 항상 절대적으로 정해져 있는 것은 아닙니다. 따라서 여기에서 언급하는 지침은 가능한 변경사항을 모두 포괄하는 종합 목록보다는 식별하기 위한 정보로 취급해야 합니다.

여기에서 나열하는 규칙은 클라이언트 호환성하고만 관련이 있습니다. 따라서 API 작성자는 구현 세부정보의 변경사항을 포함해 배포와 관련된 자신의 요구 사항을 잘 알고 있어야 합니다.

일반적인 목적은 새로운 부 버전 또는 패치로 업데이트하는 서비스로 인해 클라이언트가 단절되지 않도록 하는 데 있습니다. 주의해야 할 호환성 단절은 다음과 같습니다.

  • 소스 호환성: 1.0에서 작성된 코드는 1.1에서 컴파일되지 않음
  • 바이너리 호환성: 1.0에서 컴파일된 코드는 1.1 클라이언트 라이브러리에서 연결하거나 실행되지 않음 (상황에 따라 여러 가지 여러 가지 유형이 있기 때문에 정확한 세부정보는 클라이언트 플랫폼에 따라 달라집니다)
  • 통신 호환성: 1.0에서 개발된 애플리케이션은 1.1 서버와 통신하지 못함
  • 시맨틱스 호환성: 모든 것이 실행되지만 의도하지 않았거나 갑작스러운 결과가 나타남

다시 말해서 이전 클라이언트는 주 버전 번호만 동일하다면 최신 서버에서도 작동할 수 있어야 하며, 새로운 부 버전으로 업데이트를 원하는 경우에도(새로운 기능 이용 등) 쉽게 업데이트할 수 있어야 합니다.

참고: v1.1 및 v1.0과 같은 버전 번호를 참조하는 경우 구체적인 내용이 없는 논리 버전 번호를 참조하는 것입니다. 변경사항을 더 쉽게 설명하기 위한 것입니다.

프로토콜에 따라 이론적으로 고려해야 할 사항 외에도 생성 코드와 필기 코드가 모두 포함된 클라이언트 라이브러리의 존재로 인해 실질적으로 고려해야 할 사항들이 있습니다. 가능하다면 새로운 버전의 클라이언트 라이브러리를 생성한 후 그래도 테스트를 통과하는지 확인하여 고려하고 있는 변경사항들을 테스트하는 것이 좋습니다.

아래 설명에서는 proto 메시지를 3개 카테고리로 나누고 있습니다.

  • 요청 메시지(예: GetBookRequest)
  • 응답 메시지(예: ListBooksResponse)
  • 리소스 메시지(예: Book, 다른 리소스 메시지에서 사용되는 메시지 포함)

요청 메시지는 클라이언트에서 서버로, 응답 메시지는 서버에서 클라이언트로, 그리고 리소스 메시지는 양방향으로 전송되기 때문에 위의 각 카테고리는 각각 규칙이 다릅니다. 특히 업데이트가 가능한 리소스는 읽기/수정/쓰기 주기의 측면에서 고려해야 합니다.

하위 호환이 가능한(이전 버전과 호환되는) 변경사항

API 인터페이스를 API 서비스 정의에 추가

프로토콜 관점에서 보면 이 변경사항은 언제나 안전합니다. 클라이언트 라이브러리가 새로운 API 인터페이스 이름을 필기 코드 내에서 이미 사용했을 수도 있다는 점만 주의하면 됩니다. 새로운 인터페이스가 기존 인터페이스와 전혀 호환되지 않으면 이러한 추가가 불가능합니다. 또한 기존 인터페이스의 간소화 버전이라면 충돌이 일어날 가능성이 더욱 높습니다.

메서드를 API 인터페이스에 추가

이미 클라이언트 라이브러리에 생성되어 있는 메서드와 충돌을 일으키는 메서드만 아니라면 메서드를 추가해도 괜찮습니다.

예를 들어 GetFoo 메서드가 있는 상태에서 C# 코드 생성기가 GetFoo 메서드와 GetFooAsync 메서드를 만들면 이전 버전과 호환되지 않습니다. 이때 GetFooAsync 메서드를 API 인터페이스에 추가하면 클라이언트 라이브러리 관점에서 브레이킹 체인지가 될 수 있습니다.

HTTP 바인딩을 메서드에 추가

바인딩으로 인해 어떠한 모호성도 발생하지 않는다는 가정 하에서 서버가 이전이라면 거부했을 URL에 응답하도록 해도 안전합니다. 이러한 변경사항은 기존 작업을 새로운 리소스 이름 패턴에 적용하려고 할 때 일어날 수 있습니다.

필드를 요청 메시지에 추가

요청 필드를 추가하더라도 필드를 지정하지 않는 클라이언트가 새로운 버전에서도 이전 버전과 동일하게 처리된다면 이번 버전과 호환될 수 있습니다.

이 변경사항이 잘못될 수 있다는 것을 가장 확실하게 나타내는 예시는 페이지로 나누기입니다. API v1.0에 컬렉션의 페이지로 나누기가 포함되어 있지 않으면 page_size 기본값이 무한 값으로 처리될 때까지(일반적으로 좋은 생각이 아님) v1.1에도 추가될 수 없습니다. 그렇지 않으면 단일 요청에서 전체 결과를 가져오는 v1.0 클라이언트가 잘린 결과를 수신하여 컬렉션에 리소스가 더 포함되어 있다는 사실을 인지하지 못하게 됩니다.

필드를 응답 메시지에 추가

리소스가 아닌 응답 메시지(예: ListBooksResponse)는 다른 응답 필드의 동작을 변경하지 않는 한 이전 버전과 호환되지 않는 클라이언트 없이 확장될 수 있습니다. 이전에 응답에서 채워졌던 모든 필드는 중복이 발생하더라도 동일한 시맨틱스로 계속해서 채워져야 합니다.

예를 들어 1.0의 쿼리 응답에 중복으로 인해 일부 결과가 생략되었다는 것을 나타내는 부울 필드 contained_duplicates가 있다고 가정해 보겠습니다. 그러면 1.1에서는 duplicate_count 필드에 더욱 자세한 정보를 제공할 수 있습니다. 1.1 관점에서 보면 중복된다고 생각할 수 있지만 여전히 contained_duplicates 필드를 채워야 합니다.

값을 열거형에 추가

요청 메시지에서만 사용되는 열거형은 자유롭게 확장되어 새로운 요소를 추가할 수 있습니다. 예를 들어 리소스 보기 패턴을 사용하면 새로운 부 버전에 보기를 새롭게 추가할 수 있습니다. 클라이언트는 이 열거형을 수신할 필요가 전혀 없기 때문에 상관없는 값을 몰라도 됩니다.

리소스 메시지와 응답 메시지의 경우에는 클라이언트가 모르는 열거형 값까지 처리해야 한다는 기본적인 가정을 전제로 합니다. 하지만 API 작성자는 새로운 열거형 요소를 올바르게 처리할 수 있는 애플리케이션 개발이 어려울 수 있다는 사실을 알고 있어야 합니다. API 소유자는 알 수 없는 열거형 값을 만났을 때 예상되는 클라이언트 동작을 기록해야 합니다.

Proto3에서는 클라이언트가 모르는 값을 수신한 후 동일한 값을 유지하는 메시지를 다시 직렬화할 수 있기 때문에 값을 열거형에 추가하더라도 읽기/수정/쓰기 주기가 단절되지 않습니다. JSON 형식에서는 값의 '이름'을 모르는 경우에 숫자 값을 전송할 수 있지만 일반적으로 서버는 클라이언트가 특정 값에 대해 실제로 알고 있는지 알 수 없습니다. 따라서 JSON 클라이언트는 이전에 알지 못했던 값을 수신하였다는 사실을 알 수는 있지만 이름이나 숫자만 확인할 뿐 둘 다 확인하지는 못합니다. 읽기/수정/쓰기 주기에서 동일한 값을 서버에게 다시 보낼 경우 서버가 이름과 숫자를 모두 알아야 하기 때문에 해당 필드가 수정되어서는 안 됩니다.

출력 전용 리소스 필드를 추가

서버에서만 제공되는 리소스 개체의 필드는 추가될 수 있습니다. 서버가 클라이언트에서 제공되는 요청 값이 유효한지 확인할 수 있지만 값이 생략된 경우 실패해서는 안 됩니다.

하위 호환이 불가능한(이전 버전과 호환되지 않는) 변경사항

서비스, 필드, 메서드 또는 열거형 값의 삭제 또는 이름 변경

기본적으로 클라이언트 코드가 중요한 것을 나타낼 때 코드를 삭제하거나 이름을 변경하면 이전 버전과 호환되지 않는 변경사항이 되어 주 버전이 증가해야 합니다. 이때 이전 이름을 언급하는 코드는 일부 언어(C#, 자바 등)에서 컴파일 시간 오류가 발생하며, 그 밖에 다른 언어에서는 실행 시간 오류 또는 데이터 손실의 원인이 될 수도 있습니다. 여기에서 통신 형식 호환성은 관련 없습니다.

HTTP 바인딩 변경

여기에서 '변경'이란 실질적으로 '삭제 후 추가'를 의미합니다. 예를 들어 PATCH를 지원하려고 하지만 게시된 버전은 PUT을 지원하는 경우, 혹은 잘못된 커스텀 동사 이름을 사용한 경우에는 새로운 바인딩을 추가할 수 있지만 서비스 메소드의 삭제는 이번 버전과 호환되지 않는 변경사항이라는 것과 동일한 이유로 이전 바인딩을 삭제해서는 안 됩니다.

필드 유형 변경

새로운 유형이 통신 형식과 호환되더라도 클라이언트 라이브러리에 생성된 코드를 변경할 수 있으므로 주 버전이 증가해야 합니다. 컴파일된 정적 유형의 언어에서는 컴파일 시간 오류가 쉽게 발생할 수 있습니다.

리소스 이름 형식 변경

리소스는 이름이 바뀌어서는 안 됩니다. 이는 컬렉션 이름도 변경할 수 없다는 것을 의미합니다.

이전 버전과 호환되지 않는 대부분 변경사항과 달리 이 변경사항은 주 버전에도 영향을 줍니다. 클라이언트가 v2.0을 사용해 v1.0에서 만들어진 리소스에 액세스하거나, 혹은 그 반대 방향으로 액세스할 수 있다면 두 버전 모두에서 동일한 리소스 이름을 사용해야 합니다.

좀 더 미묘하지만 유효한 리소스 이름은 다음 중 어떤 이유로도 바뀌어서는 안 됩니다.

  • 제한이 커지면 이전이라면 성공했을 요청도 실패하게 됩니다.
  • 이전에 작성한 것보다 제한이 줄어들면 이전 문서를 기준으로 가정하는 클라이언트는 호환성이 단절될 수 있습니다. 그러면 클라이언트가 허용되는 문자 집합과 이름 길이에 민감할 수 있는 방식으로 리소스 이름을 다른 곳에 저장할 가능성이 높습니다. 또한 문서를 따르기 위해 자체적인 리소스 이름 확인까지 수행할 수 있습니다. (예를 들어 Amazon은 더욱 긴 EC2 리소스 ID를 허용하기 시작하면서 수많은 경고 메시지가 고객에게 표시되었을 뿐만 아니라 이전 기간까지 발생했습니다)

단, 이러한 변경사항은 proto 문서에서만 볼 수 있습니다. 따라서 CL에서 호환성 단절 여부를 검토할 때 주석이 없는 변경사항을 검토해서는 부족합니다.

기존 요청의 시각적 동작 변경

클라이언트는 명시적으로 지원되거나 작성되지 않더라도 API 동작 및 시맨틱스를 신뢰하는 경우가 많습니다. 따라서 대부분 경우 API 데이터의 동작 또는 시맨틱스를 변경하면 사용자에게는 호환성이 단절되는 것으로 보이게 됩니다. 이 동작이 암호화를 통해 숨겨지지 않으면 사용자가 해당 동작을 발견하여 신뢰하고 있다고 가정해도 좋습니다.

이러한 이유로 (데이터가 흥미롭지 않더라도) 사용자가 자신의 토큰을 만들어 토큰 동작 변경 시 호환성이 단절될 가능성을 배제할 목적으로 페이지 나누기 토큰을 암호화하는 것이 좋습니다.

HTTP 정의에서 URL 형식 변경

여기에서는 위에서 언급한 리소스 이름 변경 외에 고려해야 할 변경사항이 2가지입니다.

  • 커스텀 메서드 이름: 커스텀 메서드 이름은 리소스 이름의 일부는 아니지만 REST 클라이언트에서 게시하는 URL의 일부입니다. 따라서 커스텀 메서드 이름을 변경하더라도 gRPC 클라이언트의 호환성이 단절되어서는 안 됩니다. 단, 공개 API에 REST 클라이언트가 있다는 가정을 전제로 해야 합니다.
  • 리소스 매개변수 이름: v1/shelves/{shelf}/books/{book}에서 v1/shelves/{shelf_id}/books/{book_id}로 변경하더라도 대체 리소스 이름에는 영향을 미치지 않지만 코드 생성에는 영향을 미칠 수 있습니다.

읽기/쓰기 필드를 리소스 메시지에 추가

클라이언트는 종종 읽기/수정/쓰기 작업을 수행합니다. 대부분 클라이언트는 필드에 알지 못하는 값을 제공하지 않으며, 특히 proto3은 이러한 기능을 지원하지 않습니다. (기본 유형이 아닌) 메시지 유형의 누락 필드는 업데이트가 해당 필드에 적용되지 않는다는 것을 의미하도록 지정할 수 있지만 이렇게 할 경우 해당 필드 값을 개체에서 명시적으로 삭제하기 더욱 어려워집니다. proto3에서는 명시적으로 int32 필드를 0으로 지정하는 것과 아무 값도 지정하지 않는 것 사이에 차이가 없으므로 기본 유형(stringbytes 포함)은 이러한 방식으로 처리될 수 없습니다.

모든 업데이트에 필드 마스크를 사용되는 경우에는 클라이언트가 알지 못하는 필드를 암묵적으로 덮어쓰지 않기 때문에 문제가 되지 않습니다. 하지만 비정상적인 API 결정이 될 수는 있습니다. 대부분 API가 '전체 리소스' 업데이트를 허용하기 때문입니다.