마이크로서비스의 계약, 주소 지정, API

리전 ID

REGION_ID는 앱을 만들 때 선택한 리전을 기준으로 Google에서 할당하는 축약된 코드입니다. 일부 리전 ID는 일반적으로 사용되는 국가 및 주/도 코드와 비슷하게 표시될 수 있지만 코드는 국가 또는 주/도와 일치하지 않습니다. 2020년 2월 이후에 생성된 앱의 경우 REGION_ID.r이 App Engine URL에 포함됩니다. 이 날짜 이전에 만든 기존 앱의 경우 URL에서 리전 ID는 선택사항입니다.

리전 ID에 대해 자세히 알아보세요.

App Engine의 마이크로서비스는 일반적으로 HTTP 기반 RESTful API를 사용하여 서로를 호출합니다. 태스크 큐를 사용하여 백그라운드에서 마이크로서비스를 호출할 수도 있으며 여기에 설명된 API 설계 원칙이 적용됩니다. 안정적이고, 안전하고, 높은 성능을 발휘하는 마이크로서비스 기반 애플리케이션을 만들려면 특정 패턴을 따르는 것이 중요합니다.

강력한 계약 사용

마이크로서비스 기반 애플리케이션의 가장 중요한 측면 중 하나는 여러 마이크로서비스를 서로 완전히 독립적으로 배포하는 것입니다. 이러한 독립성을 위해 각 마이크로서비스는 버전이 지정되고 잘 정의된 계약을 해당 클라이언트(다른 마이크로서비스)에 제공해야 합니다. 버전이 지정된 특정 계약에 의존하는 다른 마이크로서비스가 전혀 없는 것으로 확인되기 전까지 각 서비스는 버전이 지정된 이러한 계약을 위반해서는 안 됩니다. 다른 마이크로서비스가 이전 계약을 필요로 하는 이전 코드 버전으로 롤백되어야 할 수도 있으므로 지원 중단 및 해제 정책에서 이 사실을 고려하는 것이 중요합니다.

버전이 지정된 강력한 계약을 요구하는 문화는 아마도 안정적인 마이크로서비스 기반 애플리케이션의 가장 까다로운 구조적 측면일 것입니다. 개발팀은 브레이킹 체인지와 그렇지 않은 변경을 내부적으로 잘 이해해야 합니다. 개발팀은 언제 새로운 주 릴리스가 필요한지 알아야 하며, 이전 계약의 적용 중단 시기와 그 방법도 파악해야 합니다. 팀은 마이크로서비스 계약의 변경을 잘 고지할 수 있도록 지원 중단 및 해제 공지를 비롯한 적절한 커뮤니케이션 기법을 채택해야 합니다. 이 모든 것이 매우 벅찬 일처럼 들릴 수 있겠지만 이러한 관행을 개발 문화 안에 심으면 점차 속도와 품질이 크게 개선될 것입니다.

마이크로서비스 주소 지정

서비스 및 코드 버전에 주소를 직접 지정할 수 있습니다. 이렇게 하면 새 코드 버전을 기존 코드 버전과 나란히 배포할 수 있으며 새 코드를 기본 제공 버전으로 만들기 전에 테스트할 수 있습니다.

각 App Engine 프로젝트에는 기본 서비스가 있고 각 서비스에는 기본 코드 버전이 있습니다. 프로젝트 기본 버전의 기본 서비스에 대한 주소로는 다음 URL을 사용합니다.
https://PROJECT_ID.REGION_ID.r.appspot.com

이름이 user-service인 서비스를 배포할 경우 다음 URL을 사용하여 이 서비스의 기본 제공 버전에 액세스할 수 있습니다.

https://user-service-dot-my-app.REGION_ID.r.appspot.com

기본 코드가 아닌 두 번째 코드 버전인 bananauser-service 서비스에 배포할 경우 다음 URL을 사용하여 이 코드 버전에 직접 액세스할 수 있습니다.

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

기본 코드가 아닌 두 번째 코드 버전인 cherrydefault 서비스에 배포할 경우 다음 URL을 사용하여 이 코드 버전에 액세스할 수 있습니다.

https://cherry-dot-my-app.REGION_ID.r.appspot.com

App Engine은 기본 서비스에 있는 코드 버전의 이름이 서비스 이름과 충돌해서는 안 된다는 규칙을 적용합니다.

기초 안정성 테스트를 실시할 때와 A/B 테스트, 롤포워드, 롤백을 지원할 때만 특정 코드 버전의 주소를 직접 지정해야 합니다. 클라이언트 코드는 기본 서비스 또는 특정 서비스의 기본 제공 버전에만 주소를 지정해야 합니다.


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

이 주소 지정 방식을 활용하면 마이크로서비스가 클라이언트를 변경하지 않고도 버그 수정 등 새로운 버전의 서비스를 배포할 수 있습니다.

API 버전 사용

모든 마이크로서비스 API에는 다음과 같이 URL에 주 API 버전이 있어야 합니다.

/user-service/v1/

이 주 API 버전은 로그에서 마이크로서비스의 어떤 API 버전이 호출되고 있는지 명확하게 식별합니다. 더 중요한 것은 새로운 주 API 버전을 이전 주 API 버전과 함께 나란히 제공할 수 있도록 주 API 버전이 서로 다른 URL을 생성한다는 점입니다.

/user-service/v1/
/user-service/v2/

부 API 버전은 브레이킹 체인지를 적용하지 않기 때문에 URL에 부 API 버전을 포함할 필요가 없습니다. 사실, URL에 부 API 버전을 포함하면 URL이 급증해 클라이언트가 새로운 부 API 버전으로 전환하는 것이 불확실해집니다.

이 문서에서는 기본 분기가 항상 App Engine에 배포되는 지속적 통합 및 배포 환경을 가정합니다. 이 문서에는 다음과 같은 두 가지 개념의 버전이 사용되었습니다.

  • 코드 버전: App Engine 서비스 버전에 직접 매핑되며 기본 분기의 특정 커밋 태그를 나타냅니다.

  • API 버전: API URL에 직접 매핑되며 요청 인수의 모양, 응답 문서의 모양, API 동작을 나타냅니다.

이 문서에서는 단일 코드 배포를 통해 API의 이전 API 버전과 새로운 API 버전 모두가 공통 코드 버전으로 구현된다고도 가정합니다. 예를 들어 배포된 기본 분기가 /user-service/v1//user-service/v2/를 모두 구현할 수 있습니다. 새로운 부 버전 및 패치 버전을 출시할 때 이 접근 방식을 활용하면 코드를 통해 실제로 구현되는 API 버전과 관계없이 트래픽을 두 코드 버전 사이에 분할할 수 있습니다.

조직에서 /user-service/v1//user-service/v2/를 서로 다른 코드 브랜치에서 개발할 수도 있습니다. 그러면 한 코드 배포를 통해 이 두 가지 모두가 동시에 구현되지 않습니다. 이 모델을 App Engine에 적용할 수 있지만 트래픽을 분할하려면 주 API 버전을 서비스 이름 자체에 넣어야 합니다. 예를 들어 클라이언트는 다음과 같은 URL을 사용합니다.

http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/
http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/

user-service-v1user-service-v2와 같이 주 API 버전이 서비스 이름 자체에 들어갑니다. 이 모델에서 경로의 /v1/, /v2/ 부분은 중복되므로 삭제될 수 있지만 로그 분석 시 유용할 수도 있습니다. 이 모델의 경우 주 API 버전이 변경될 때 새 서비스를 배포하기 위한 배포 스크립트를 업데이트해야 할 수 있으므로 좀 더 많은 작업이 요구됩니다. 한 App Engine 애플리케이션에 허용되는 최대 서비스 수에도 주의하세요.

브레이킹 체인지와 그렇지 않은 변경 비교

브레이킹 체인지와 그렇지 않은 변경의 차이점을 이해하는 것이 중요합니다. 브레이킹 체인지는 무언가를 빼는 형태인 경우가 많습니다. 즉, 요청 또는 응답 문서의 일부분을 없앱니다. 또한 문서 모양을 변경하거나 키 이름을 변경해도 브레이킹 체인지가 발생할 수 있습니다. 새로운 필수 인수는 항상 브레이킹 체인지입니다. 브레이킹 체인지는 마이크로서비스의 동작이 변경될 때도 발생할 수 있습니다.

그렇지 않은 변경은 주로 무언가를 더하는 형태입니다. 새로운 선택적 요청 인수나 응답 문서의 새로운 추가 섹션이 호환성이 이 경우에 해당됩니다. 이러한 변경을 위해서는 실시간 직렬화를 반드시 선택해야 합니다. JSON, Protocol Buffers, Thrift 등 많은 직렬화 도구를 통해 가능합니다. 역직렬화를 진행하면 이러한 직렬화 도구에서 예상치 못한 추가 정보가 자동으로 무시됩니다. 동적 언어에서는 추가 정보가 역직렬화된 객체에 나타납니다.

/user-service/v1/ 서비스의 다음 JSON 정의를 고려해 보세요.

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

다음 브레이킹 체인지의 경우 서비스의 버전을 /user-service/v2/로 변경해야 합니다.

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

하지만 브레이킹 체인지가 아닌 다음 변경의 경우 새 버전이 필요하지 않습니다.

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

브레이킹 체인지가 아닌 새로운 부 API 버전 배포

새로운 부 API 버전을 배포할 때 App Engine에서 새 코드 버전을 이전 코드 버전과 나란히 릴리스할 수 있습니다. App Engine에서는 배포된 버전의 주소를 직접 지정할 수 있지만 한 버전만 기본 제공 버전이 됩니다. 각 서비스에 기본 제공 버전이 있다는 점을 기억하세요. 이 예시에서는 apple이라는 이전 코드 버전이 기본 제공 버전이고 banana라는 새로운 코드 버전을 나란히 배포합니다. 브레이킹 체인지가 아닌 부 API 변경을 배포하기 때문에 둘 모두의 마이크로서비스 URL이 똑같이 /user-service/v1/입니다.

App Engine은 새 코드 버전 banana를 기본 제공 버전으로 지정하여 트래픽을 apple에서 banana로 자동 마이그레이션하는 메커니즘을 제공합니다. 새로운 기본 제공 버전이 설정되면 어떠한 새 요청도 apple로 라우팅되지 않고 모두 banana로 라우팅됩니다. 바로 이것이 클라이언트 마이크로서비스에 영향을 주지 않고 새로운 부 또는 패치 API 버전을 구현하는 새 코드 버전으로 롤포워드하는 방법입니다.

오류가 발생할 경우 위 프로세스를 역순으로 진행하여 롤백을 수행합니다. 이 예시에서는 기본 제공 버전을 이전 버전인 apple로 설정합니다. 모든 새 요청은 다시 이전 코드 버전으로 라우팅되고 banana로 라우팅되지 않습니다. 진행 중인 요청은 기존 버전에서 완료됩니다.

또한 App Engine은 트래픽 중 일정 비율만 새 코드 버전으로 중계하는 기능을 제공합니다. 이 방식을 카나리아 릴리스 프로세스라고 하며 App Engine에서는 이 메커니즘을 트래픽 분할이라고 합니다. 1%, 10%, 50% 등 원하는 일정 비율의 트래픽을 새 코드 버전으로 보낼 수 있으며 시간 경과에 따라 이 양을 조정할 수 있습니다. 예를 들어 15분 동안 새 코드 버전을 롤아웃할 때 트래픽을 천천히 늘리면서 롤백이 필요한 문제가 있는지 살펴볼 수 있습니다. 같은 방법으로 두 코드 버전에 A/B 테스트를 실시할 수 있습니다. 트래픽 분할을 50%로 설정하고 두 코드 버전의 성능 및 오류율 특성을 비교하여 예상 개선사항을 확인하는 것입니다.

다음 이미지는 Google Cloud 콘솔의 트래픽 분할 설정을 보여줍니다.

Google Cloud 콘솔의 트래픽 분할 설정

호환성이 손상되는 새로운 주 API 버전 배포

호환성이 손상되는 주 API 버전을 배포할 때 롤포워드 및 롤백 프로세스는 호환성이 손상되지 않는 부 API 버전과 같습니다. 하지만 호환성이 손상되는 API 버전은 새로 릴리스되는 URL(예: /user-service/v2/)이기 때문에 일반적으로 트래픽 분할이나 A/B 테스트를 하지 않습니다. 물론 이전 주 API 버전의 기본 구현을 변경한 경우에는 트래픽 분할을 사용하여 이전 주 API 버전이 계속해서 예상대로 작동하는지 테스트할 수 있습니다.

새로운 API 주 버전을 배포할 때는 이전 주 API 버전이 여전히 서비스를 제공하고 있을 수도 있다는 점을 염두에 두어야 합니다. 예를 들어 /user-service/v2/가 릴리스될 때 /user-service/v1/이 여전히 서비스를 제공하고 있을 수 있습니다. 이 사실은 독립적 코드 릴리스의 중요한 부분입니다. 이전 코드 버전으로 롤백해야 하는 다른 마이크로서비스를 비롯하여 이전 버전을 필요로 하는 다른 마이크로서비스가 없다는 것을 확인한 후에만 이전 주 API 버전을 해제하는 것이 좋습니다.

구체적인 예시를 위해 web-app이라고 하는 마이크로서비스가 있고 이 서비스가 user-service라고 하는 다른 마이크로서비스에 종속되어 있다고 가정해 보겠습니다. user-service에서 몇 가지 기본 구현을 변경해야 하는데 그러면 web-app이 현재 사용 중인 이전 주 API 버전을 지원할 수 없게 되어 firstNamelastNamename이라는 단일 필드로 축소하는 등의 작업이 불가능해집니다. 즉, user-service가 이전 주 API 버전을 해제해야 합니다.

이 변경사항을 적용하기 위해서는 세 가지 배포를 따로 수행해야 합니다.

  • 먼저 user-service에서 /user-service/v2/를 배포하는 동시에 /user-service/v1/을 지원해야 합니다. 이 배포를 위해서는 이전 버전과의 호환성을 지원하는 임시 코드를 작성해야 할 수 있으며 마이크로서비스 기반 애플리케이션에서 흔히 발생하는 상황입니다.

  • 다음으로 web-app이 종속 항목을 /user-service/v1/에서 /user-service/v2/로 변경하는 업데이트된 코드를 배포해야 합니다.

  • 마지막으로 user-service팀에서 web-app이 더 이상 /user-service/v1/을 필요로 하지 않고 web-app을 롤백하지 않아도 된다는 것을 확인한 후에 이전 /user-service/v1/ 엔드포인트와 이를 지원하기 위해 필요했던 임시 코드를 삭제하는 코드를 팀에서 배포할 수 있습니다.

다소 부담스러울 수 있지만 이 모든 작업이 마이크로서비스 기반 애플리케이션의 필수적인 프로세스이며, 바로 이를 통해 독립적인 개발 릴리스 주기가 가능해집니다. 이 프로세스는 꽤 종속적인 것처럼 보이지만 위의 각 단계가 서로 다른 시기에 진행될 수 있으며 롤포워드 및 롤백은 단일 마이크로서비스의 범위 내에서 이루어집니다. 단계의 순서만 고정되어 있으며 단계는 몇 시간, 며칠, 심지어 몇 주에 걸쳐 진행될 수 있습니다.

다음 단계