Python용 Durable Functions에서 타입 안전 직렬화로 마이그레이션

이 글에서는 Python 프로그래밍 모델을 사용하는 기존 Durable Functions 앱에서 타입 안전(타입 인식이라고도 함) 페이로드 직렬화를 어떻게 도입하는지 보여줍니다. 타입 안전 직렬화는 직렬 해제된 페이로드를 예상 타입에 대해 검증하며, 신뢰할 수 없는 페이로드 역직렬화 위험을 제거하는 강화된 엄격한 모드 에 선택권을 부여합니다.

타입 안전 직렬화 채택은 Python을 사용하는 모든 Durable Functions 앱에 권장되는 모범 사례이며, 보안에 민감하지 않은 앱도 포함됩니다. SDK가 저장된 데이터 이름을 조용히 재구성하는 대신 각 페이로드를 코드가 예상하는 타입에 대해 검증하기 때문에 타입 불일치 버그를 조기에 발견할 수 있습니다. 엄격한 모드는 또한 신뢰할 수 없는 페이로드 디직렬화에 대해 앱을 강화하여 코드를 더 안전하게 만듭니다. SDK는 azure-functions 엄격한 모드를 모범 사례로 홍보하며, 이 글에서는 하위 호환 단계부터 점진적으로 도입하는 방법을 안내합니다.

이 기능은 두 개의 패키지로 함께 제공됩니다:

  • azure-functions는 선택적 타입 유효성 검사 및 엄격한 타입 지정 지원을 갖춘 중앙 집중식 직렬화기(df_dumps / df_loads)를 제공합니다.
  • azure-functions-durable모든 Durable Functions 페이로드 직렬화를 해당 직렬라이저를 통해 라우팅하며, 매개변수 및 자동 타입 탐색을 오케스트레이션 및 엔터티 API에 추가합니다expected_type.

어떤 데이터가 Durable Functions가 지속되는지, 그리고 사용자 정의 타입이 어떻게 직렬화되는지에 대한 배경은 Durable Functions의 데이터 지속화 및 직렬화를 참조하세요.

변경 내용

이 기능이 도입되기 전에는 Durable Functions가 저장된 JSON에 포함된 __module____class__ 필드를 읽고, 클래스를 찾기 위해 importlib.import_module()를 호출하여 사용자 지정 개체 페이로드를 역직렬화했습니다. 페이로드의 클래스가 코드가 예상하는 타입과 일치하는지 확인하지 않았습니다.

타입 안전 직렬화는 다음을 추가합니다:

  • 페이로드를 디직렬화하는 오케스트레이션 및 엔티티 API에 대한 선택적 expected_type 인자입니다.
  • 자동 타입 검색으로 v2 데코레이터가 적용된 활동 및 하위 오케스트레이터 함수의 반환 형식 주석을 읽어 코드 변경 없이 이를 expected_type로 사용합니다.
  • 엄격 모드: AZURE_FUNCTIONS_DURABLE_STRICT_TYPING 환경 변수를 통해 활성화되며, 타입 불일치를 치명적 오류로 처리하고 importlib.import_module()를 호출하지 않고 사용자 지정 객체를 역직렬화하는 모드입니다.

직렬화 형식은 변경되지 않았습니다. 내장 타입은 여전히 일반 JSON으로 직렬화되며, 커스텀 객체는 여전히 이 관례를 {"__class__", "__module__", "__data__"} 사용합니다. 즉, 느슨한 모드는 완전한 하위 호환성을 제공하므로 기존 기록과 현재 진행 중인 오케스트레이션은 이전과 동일하게 계속 역직렬화됩니다.

사전 요구 사항

  • Python 프로그래밍 모델(v1 또는 v2)을 사용하는 기존 Durable Functions 앱입니다.

  • 중앙 집중식 df_dumps / df_loads 시리얼라이저를 제공하는 최소 패키지 버전은 다음과 같습니다:

    Python 버전 최소 azure-functions 버전
    3.13 이후 2.2.0
    3.10 – 3.12 1.26.0
  • azure-functions-durable 1.6.0 이상입니다.

메모

설치된 azure-functions 패키지가 df_dumps / df_loads를 제공하지 않으면 Durable Functions는 레거시 직렬화 파이프라인으로 대체됩니다. 지속되는 JSON 형식은 동일하지만, 인수와 엄격 모드는 expected_type 아무런 영향이 없습니다. 이전 표의 버전으로 업그레이드하여 타입 검증 직렬화를 활성화하세요.

느슨한 모드와 엄격한 모드 비교

타입 안전 직렬화는 두 가지 모드를 가집니다.

작동 방식 루즈 모드 (기본) 엄격 모드
참여하기 선택 항상 켜짐 AZURE_FUNCTIONS_DURABLE_STRICT_TYPING을(를) 1, true 또는 yes(으)로 설정합니다
유형 불일치 경고를 기록한 후 레거시 디코더로 전환합니다 증가 TypeError
커스텀 오브젝트 디코드 용도 importlib.import_module() (레거시 경로) expected_type.from_json()를 직접 호출; import_module는 절대 호출하지 않음
to_json / from_json 계약 변경 안 됨 대칭적이어야 하며, 네이티브로 JSON 직렬화 가능한 데이터를 생성해야 합니다( 업데이트 to_json 및 from_json 참조)
하위 호환 Yes 아니요. 코드 변경이 필요합니다

루즈 모드는 올바르게 타입된 페이로드에 대해 행동이 변하지 않기 때문에 즉시 채택할 수 있습니다. 엄격한 모드는 의도적이고 보안 강화 변경으로, 이후 마이그레이션 단계를 요구합니다.

점진적으로 마이그레이션하세요

타입 안전 직렬화를 단계별로 채택하세요. 1단계와 2단계는 하위 호환이 되어 있고, 별도로 안전하게 배송할 수 있습니다. 3단계와 4단계는 엄격한 모드를 활성화할 준비가 되었을 때만 완료하세요.

1단계: 패키지 업그레이드

Prerequisites의 최소 버전으로 앱 요구사항을 업데이트하세요. 예를 들어, requirements.txt에서 :

azure-functions>=2.2.0
azure-functions-durable>=1.6.0

업그레이드 후에는 앱이 계속 느슨한 상태로 실행되며 동작 변화가 없습니다. 기존 앱을 계속 사용하려면 별도의 변경이 필요하지 않습니다.

2단계: 느슨한 모드 타입 검증 도입

느슨한 모드에서는 예상 타입을 제공해 SDK가 디직렬화된 페이로드를 검증하고 불일치 시 경고를 기록할 수 있도록 합니다. 타입은 세 가지 방식으로 공급할 수 있고, 필요에 따라 섞을 수 있습니다.

활동과 서브오케스트레이터에 반환형 주석을 추가하세요. Python v2 프로그래밍 모델에서는 SDK가 반환 주석을 자동으로 발견하고 이를 사용해 결과를 검증합니다. 통화 장소 변경은 필요 없습니다.

@myApp.activity_trigger(input_name="city")
def get_weather(city: str) -> WeatherReport:
    return WeatherReport(city=city, temperature_c=21)


@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
    # The WeatherReport return annotation on get_weather is discovered
    # automatically and used to validate the result.
    report = yield context.call_activity("get_weather", "Seattle")
    return report.temperature_c

expected_type를 명시적으로 전달하세요. 명시적 expected_type는 발견된 주석보다 우선 적용됩니다. 반환 타입이 구체적 클래스가 아닐 때 사용하세요. 예를 들어, list[Order] 또는 Optional[Order]와 같은 일반적인 별칭은 자동으로 식별할 수 없습니다.

orders = yield context.call_activity("get_orders", customer_id, expected_type=list)

expected_type 인수는 다음 오케스트레이션 API에서 사용할 수 있습니다:

  • call_activitycall_activity_with_retry
  • call_sub_orchestratorcall_sub_orchestrator_with_retry
  • call_entity
  • wait_for_external_event
  • get_input

그리고 이러한 엔티티 API에서는 DurableEntityContext를 통해:

  • get_state
  • get_input

트리거에서 오케스트레이션 입력 타입을 선언하세요. input_type에서 orchestration_trigger 인수를 사용하여 context.get_input()가 입력을 검증하도록 하세요. expected_type의 호출 위치 get_input()가 우선 적용됩니다.

@myApp.orchestration_trigger(context_name="context", input_type=OrderRequest)
def orchestrator(context: df.DurableOrchestrationContext):
    request = context.get_input()  # validated against OrderRequest
    ...

이 단계가 끝나면 앱을 실행하고 azure.functions.DurableFunctions 로거 아래의 로그에서 타입 불일치 경고가 있는지 확인하세요. 엄격한 모드로 넘어가기 전에 경고를 먼저 해결하세요. 이 단계는 경고만 추가하므로 단독으로 배포해도 안전합니다.

팁 (조언)

자동 타입 발견은 구체 type 적인 객체만 해결합니다. , , list[Order] 와 같은 dict[str, Order]Optional[Order]일반 별칭은 "타입 정보 없음"으로 해결되며, 디코딩은 모듈 전용 해상도로 되돌아갑니다. 이 도형들에 대한 검증이 필요할 때 명확히 제공하세요 expected_type .

3단계: 엄격 모드에 맞게 to_json 및 from_json 업데이트하기

Strict 모드는 커스텀 타입의 계약을 변경합니다. 엄격 모드에서는 to_json()가 딕셔너리, 리스트, 문자열, 숫자, 불리언 또는 json.dumps처럼 None가 기본적으로 직렬화할 수 있는 값을 반환해야 합니다. 중첩된 커스텀 객체를 인스턴스로 반환하는 대신 명시적으로 직렬화해야 하며, from_json() 대칭적으로 재구성해야 합니다.

이 요구 사항은 모든 중첩 수준에서 저장된 페이로드에서 __module__ 문자열을 제거하므로, 이제 역직렬화에서 페이로드 데이터의 형식 이름을 확인할 필요가 없습니다.

class Order:
    def __init__(self, item, hat):
        self.item = item
        self.hat = hat

    @staticmethod
    def to_json(obj):
        return {
            "item": obj.item,
            "hat": Hat.to_json(obj.hat),   # explicit, not obj.hat
        }

    @staticmethod
    def from_json(data):
        return Order(
            item=data["item"],
            hat=Hat.from_json(data["hat"]),  # symmetric
        )

롤아웃 시 비행 중 레거시 페이로드를 처리하세요. 만약 업그레이드 전에 느슨한 모드로 작성된 페이로드를 여전히 읽을 수 있다면, 두 형태 모두 허용하도록 하세요 from_json . 느슨하게 인코딩된 중첩 값은 이미 재구성된 인스턴스(레거시 object_hook 파이어)로 도착하고, 엄격하게 인코딩된 값은 일반 명령어로 도착합니다.

    @staticmethod
    def from_json(data):
        hat_data = data["hat"]
        if isinstance(hat_data, Hat):
            hat = hat_data                 # loose-encoded: object already built
        else:
            hat = Hat.from_json(hat_data)  # strict-encoded: plain dict
        return Order(item=data["item"], hat=hat)

4단계: 엄격한 모드 활성화

애플리케이션 설정 AZURE_FUNCTIONS_DURABLE_STRICT_TYPING을(를) 1, true 또는 yes(대소문자를 구분하지 않음)로 설정하세요.

로컬 local.settings.json에서:

{
  "Values": {
    "AZURE_FUNCTIONS_DURABLE_STRICT_TYPING": "true"
  }
}

또는 함수 앱의 애플리케이션 설정으로:

az functionapp config appsettings set --name <APP_NAME> --resource-group <RESOURCE_GROUP> --settings AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=true

엄격 모드에서는:

  • 타입 불일치는 경고를 기록하는 대신 TypeError를 발생시킵니다.
  • 커스텀 객체는 expected_type.from_json()를 직접 호출하여 역직렬화되므로 import_module은 절대 사용되지 않습니다.
  • 커스텀 객체를 없이 expected_type 모든 호출 사이트는 TypeError 예외를 발생시킵니다. 엄격 모드를 활성화하기 전에 그러한 모든 호출 위치에서 2단계의 메커니즘 중 하나를 통해 유형이 제공되도록 하세요.
  • 활동 함수 입력은 커스텀 객체일 수 없습니다. 다음 메모를 확인하세요.

Important

엄격 모드에서는 활동 함수의 입력 이 커스텀 객체일 수 없습니다. 호스트가 활동을 호출하면 Functions 워커가 활동의 매개변수 형식 주석을 변환기에 전달하지 않기 때문에 azure-functions 활동 트리거 변환기는 expected_type 없이 입력을 역직렬화합니다. 따라서 사용자 지정 객체 입력은 ValueError와 함께 실패합니다. 활동 입력은 사전, 리스트, 문자열, 숫자, 불리언 None등과 같은 JSON 직렬화 가능한 기본 값으로 전달합니다. 커스텀 객체를 보내야 하는 경우, 호출 전에 해당 객체의 to_json() 메서드로 변환하고 액티비티 내에서 from_json()로 다시 구성하세요. 이 제한은 활동 입력에만 적용됩니다. 활동 반환 값, 오케스트레이션 및 엔티티 입력, 엔티티 상태, 외부 이벤트 페이로드는 모두 타입을 제공할 때 엄격한 모드에서 커스텀 타입을 지원합니다.

Important

모든 앱 인스턴스가 업그레이드되고 느슨하게 인코딩된 기록을 포함한 진행 중인 오케스트레이션이 모두 처리 완료된 후에만, 또는 from_json 메서드가 두 형식을 모두 처리할 수 있는 경우에만(3단계) 엄격 모드를 활성화하세요. 업그레이드 이전에 시작된 오케스트레이션은 원래의 느슨한 인코딩 방식으로 저장된 이력을 다시 재생합니다. 만약 코드가 엄격한 모드에서 그 기록을 해독하지 못하면 재생이 실패합니다.

기존 오케스트레이션에 대한 버전 관리 함의

타입 안전 직렬화로 업데이트하면, 페이로드 유형이 레거시 구현과 다르면 실행 중인 오케스트레이션이 중단됩니다. 오케스트레이션이 계속될 때마다 저장된 역사를 재생합니다. 만약 디코드 지점이 이제 이전 페이로드에 저장된 타입과 일치하지 않는 타입을 기대한다면, 엄격 모드는 히스토리가 처음 기록될 때는 없었던 TypeError를 발생시키고, 그 새로운 오류가 오케스트레이션을 중단시킵니다. 두 가지 흔한 마이그레이션 변화가 이러한 불일치를 초래합니다:

  • 이전에는 여러 종류가 존재했던 길이었다. 활동 결과와 같은 단일 디직렬라이즈 경로가 이전에 서로 다른 객체 유형을 반환할 수 있었고, 이제 단일 expected_type주석으로 주석을 달았다면, 다른 타입을 사용한 저장된 페이로드는 더 이상 일치하지 않고 디코딩에 실패합니다.
  • 사용자 지정 타입을 활동 입력으로 사용합니다. 엄격한 모드에서는 활동 입력이 커스텀 객체가 될 수 없기 때문에, 엄격한 모드를 채택하려면 입력값을 JSON 직렬화 가능한 값으로 변경해야 하며, 이는 실행 중인 인스턴스가 유지하던 페이로드 형태를 변경해야 합니다.

더 일반적으로, 페이로드의 저장된 타입이 디코딩 사이트가 현재 기대하는 타입과 다르게 하는 모든 변경 사항도 동일한 오류를 일으킵니다. 예를 들어, 인스턴스가 영속화된 후 사용자 지정 클래스를 변경하거나 이동하는 것도 같은 불일치를 초래합니다.

안전하게 마이그레이션하려면 다음 방법 중 하나를 사용하세요:

  • 권장: 오케스트레이션 버전 관리를 사용해 롤아웃을 분할하세요. 오케스트레이션 버전 관리Strict버전 일치 전략과 함께 사용하여 새 엄격 모드 워커가 새 버전에서 시작된 오케스트레이션만 처리하도록 하세요. 이 모범 사례를 사용하면 rolling upgrade 중에 두 버전이 공존할 수 있게 하고 리플레이 실패를 방지할 수 있습니다.
  • 대안으로는 먼저 배수하세요. 모든 비행 중 오케스트레이션이 끝날 때까지 기다린 후 엄격한 모드를 활성화하세요.

프로덕션에서 엄격한 모드를 활성화하기 전에, 모든 커스텀 오브젝트 디코딩 사이트가 타입을 제공하고, 커스텀 클래스가 인스턴스가 페이로드를 유지할 때 사용했던 이름과 모듈을 동일하게 유지하는지 확인하세요.

실행 중인 오케스트레이션에 영향을 미치는 변경 사항을 안전하게 배포하는 광범위한 지침은 Versioning in Durable Functions를 참조하세요.

보안 강화

엄격한 모드는 커스텀 객체 페이로드의 디직렬화 방식을 강화합니다. 저장되었거나 수신된 페이로드에 포함된 모듈 및 클래스 이름을 신뢰하여 형식을 찾는 대신, 엄격 모드는 코드에서 제공하는 expected_type를 사용해 사용자 정의 객체를 재구성하며, 엄격 모드의 to_json() 출력은 중첩 수준과 관계없이 모듈 이름을 저장하지 않습니다. 이 변경은 디직렬화 과정에서 페이로드 데이터에서 임의의 타입 이름을 해석할 필요가 없어, 페이로드에 담긴 타입 정보에 의존하는 것보다 방어 심층 개선을 이룹니다.

페이로드에 민감한 데이터가 포함될 수 있다면 민감한 데이터 작업도 검토하세요.