[Final] PEP 673 - Self Type

원문 링크: PEP 673 - Self Type

상태: Final 유형: Standards Track 작성일: 10-Nov-2021

PEP 673 – Self 타입

요약 (Abstract)

이 PEP는 메서드가 자신의 클래스 인스턴스를 반환할 때 이를 어노테이션(annotation)하는 간단하고 직관적인 방법을 제안합니다. 이 방식은 기존의 TypeVar 기반 접근 방식(PEP 484에 명시됨)과 동일하게 작동하지만, 더 간결하고 이해하기 쉽습니다.

동기 (Motivation)

자신의 클래스와 동일한 인스턴스를 반환하는 메서드를 작성하는 것은 흔한 사용 사례입니다. 예를 들어, 일반적으로 self를 반환하는 경우입니다.

class Shape:
    def set_scale(self, scale: float):
        self.scale = scale
        return self

Shape().set_scale(0.5) # => Shape 타입이어야 합니다.

반환 타입을 명시하는 한 가지 방법은 현재 클래스(예: Shape)로 지정하는 것입니다. 이 메서드를 사용하면 타입 검사기가 예상대로 Shape 타입을 추론합니다.

class Shape:
    def set_scale(self, scale: float) -> Shape:
        self.scale = scale
        return self

Shape().set_scale(0.5) # => Shape

그러나 Shape의 서브클래스에서 set_scale을 호출하면, 타입 검사기는 여전히 반환 타입을 Shape으로 추론합니다. 이는 아래와 같은 상황에서 문제가 될 수 있습니다. 예를 들어, 기본 클래스에는 없는 속성이나 메서드를 사용하려고 할 때 타입 검사기가 오류를 발생시킵니다.

class Circle(Shape):
    def set_radius(self, r: float) -> Circle:
        self.radius = r
        return self

Circle().set_scale(0.5) # *Shape* 타입으로 추론, Circle이 아님
Circle().set_scale(0.5).set_radius(2.7) # => 에러: Shape에는 set_radius 속성이 없습니다.

이러한 경우에 대한 현재의 해결책은 TypeVar를 정의하고, 기본 클래스를 바운드(bound)로 지정한 후, 이를 self 파라미터와 반환 타입의 어노테이션으로 사용하는 것입니다.

from typing import TypeVar

TShape = TypeVar("TShape", bound="Shape")

class Shape:
    def set_scale(self: TShape, scale: float) -> TShape:
        self.scale = scale
        return self

class Circle(Shape):
    def set_radius(self, radius: float) -> Circle:
        self.radius = radius
        return self

Circle().set_scale(0.5).set_radius(2.7) # => Circle

하지만 이 방법은 장황하고 직관적이지 않습니다. self는 일반적으로 명시적으로 어노테이션되지 않기 때문에, 위의 해결책은 쉽게 떠오르지 않을 뿐더러, TypeVar의 바운드(bound="Shape")나 self에 대한 어노테이션을 잊어버리기 쉽습니다.

이러한 어려움 때문에 사용자들은 종종 포기하고 Any와 같은 대체 타입을 사용하거나, 아예 타입 어노테이션을 생략하게 됩니다. 이 두 가지 모두 코드의 타입 안전성(type safety)을 떨어뜨립니다.

이 PEP는 위의 의도를 표현하는 더 직관적이고 간결한 방법을 제안합니다. 우리는 감싸고 있는(encapsulating) 클래스에 바운드된 타입 변수를 나타내는 특별한 형태인 Self를 도입합니다. 위와 같은 상황에서, 사용자는 단순히 반환 타입을 Self로 어노테이션하면 됩니다.

from typing import Self

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

class Circle(Shape):
    def set_radius(self, radius: float) -> Self:
        self.radius = radius
        return self

반환 타입을 Self로 어노테이션함으로써, 우리는 더 이상 기본 클래스에 명시적인 바운드를 가진 TypeVar를 선언할 필요가 없습니다. 반환 타입 Self는 함수가 self를 반환한다는 사실을 반영하며 이해하기 쉽습니다.

위 예제에서처럼, 타입 검사기는 Circle().set_scale(0.5)의 타입을 예상대로 Circle로 정확하게 추론할 것입니다.

사용 통계 (Usage statistics)

우리는 인기 있는 오픈 소스 프로젝트들을 분석했으며, 위와 같은 패턴이 dict 또는 Callable과 같은 인기 있는 타입만큼 자주 사용된다는 것을 발견했습니다 (약 40%). 예를 들어, 2021년 10월 기준으로 typeshed에서만 이러한 “Self” 타입은 523회 사용되었으며, dict는 1286회, Callable은 1314회 사용되었습니다. 이는 Self 타입이 상당히 자주 사용될 것이며, 사용자들은 이 더 간단한 접근 방식으로부터 많은 이점을 얻을 수 있음을 시사합니다.

Python 타입 사용자들도 제안 문서와 GitHub에서 이 기능을 자주 요청했습니다.

명세 (Specification)

메서드 시그니처에서의 사용 (Use in Method Signatures)

메서드 시그니처에서 사용되는 Self는 해당 클래스에 바운드된 TypeVar처럼 처리됩니다.

from typing import Self

class Shape:
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

위 코드는 다음 코드와 동일하게 처리됩니다.

from typing import TypeVar

SelfShape = TypeVar("SelfShape", bound="Shape")

class Shape:
    def set_scale(self: SelfShape, scale: float) -> SelfShape:
        self.scale = scale
        return self

이것은 서브클래스에서도 동일하게 작동합니다.

class Circle(Shape):
    def set_radius(self, radius: float) -> Self:
        self.radius = radius
        return self

위 코드는 다음 코드와 동일하게 처리됩니다.

SelfCircle = TypeVar("SelfCircle", bound="Circle")

class Circle(Shape):
    def set_radius(self: SelfCircle, radius: float) -> SelfCircle:
        self.radius = radius
        return self

한 가지 구현 전략은 전처리 단계에서 전자를 후자로 단순히 “desugar”(구문 설탕 제거)하는 것입니다. 메서드가 시그니처에 Self를 사용하면, 메서드 내 self의 타입은 Self가 됩니다. 다른 경우에는 self의 타입은 감싸는(enclosing) 클래스로 유지됩니다.

클래스메서드 시그니처에서의 사용 (Use in Classmethod Signatures)

Self 타입 어노테이션은 자신이 작동하는 클래스의 인스턴스를 반환하는 클래스메서드(classmethod)에서도 유용합니다. 예를 들어, 다음 스니펫의 from_config는 주어진 config로부터 Shape 객체를 만듭니다.

class Shape:
    def __init__(self, scale: float) -> None: ...
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Shape:
        return cls(config["scale"])

그러나 이렇게 하면 Circle.from_config(...)Shape 타입의 값을 반환하는 것으로 추론되는데, 실제로는 Circle이어야 합니다.

class Circle(Shape):
    def circumference(self) -> float: ...

shape = Shape.from_config({"scale": 7.0}) # => Shape
circle = Circle.from_config({"scale": 7.0}) # => *Shape* 타입으로 추론, Circle이 아님
circle.circumference() # 에러: `Shape`에는 `circumference` 속성이 없습니다.

이에 대한 현재의 해결책은 직관적이지 않고 오류 발생 가능성이 높습니다.

Self = TypeVar("Self", bound="Shape") # TypeVar 이름 충돌을 피하기 위해 이름을 다르게 할 수도 있습니다.

class Shape:
    @classmethod
    def from_config(
        cls: type[Self], # 복잡한 어노테이션
        config: dict[str, float]
    ) -> Self:
        return cls(config["scale"])

우리는 Self를 직접 사용하는 것을 제안합니다.

from typing import Self

class Shape:
    @classmethod
    def from_config(cls, config: dict[str, float]) -> Self:
        return cls(config["scale"])

이는 복잡한 cls: type[Self] 어노테이션과 바운드가 있는 TypeVar 선언을 피하게 해줍니다. 다시 말하지만, 후자의 코드는 전자의 코드와 동일하게 동작합니다.

파라미터 타입에서의 사용 (Use in Parameter Types)

Self의 또 다른 용도는 현재 클래스의 인스턴스를 기대하는 파라미터를 어노테이션하는 것입니다.

Self = TypeVar("Self", bound="Shape")

class Shape:
    def difference(self: Self, other: Self) -> float: ...
    def apply(self: Self, f: Callable[[Self], None]) -> None: ...

우리는 동일한 동작을 위해 Self를 직접 사용하는 것을 제안합니다.

from typing import Self

class Shape:
    def difference(self, other: Self) -> float: ...
    def apply(self, f: Callable[[Self], None]) -> None: ...

self: Self를 명시하는 것이 무해하기 때문에, 일부 사용자들은 위 코드를 다음과 같이 작성하는 것이 더 읽기 쉽다고 생각할 수도 있습니다.

class Shape:
    def difference(self: Self, other: Self) -> float: ...

속성 어노테이션에서의 사용 (Use in Attribute Annotations)

Self의 또 다른 용도는 속성(attribute)을 어노테이션하는 것입니다. 한 가지 예시는 요소가 현재 클래스의 서브클래스여야 하는 LinkedList를 가질 때입니다.

from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")

@dataclass
class LinkedList(Generic[T]):
    value: T
    next: LinkedList[T] | None = None # OK
    # LinkedList[int](value=1, next=LinkedList[int](value=2)) # OK
    # LinkedList[int](value=1, next=LinkedList[str](value="hello")) # Not OK

그러나 next 속성을 LinkedList[T]로 어노테이션하면 서브클래스와 함께 유효하지 않은 구성이 허용됩니다.

@dataclass
class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return as_ordinal(self.value)

# LinkedList[int]는 OrdinalLinkedList의 서브클래스가 아니므로
# 허용되어서는 안 되지만, 타입 검사기는 이를 허용합니다.
xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
if xs.next:
    print(xs.next.ordinal_value()) # 런타임 에러.

우리는 이 제약 조건을 next: Self | None을 사용하여 표현하는 것을 제안합니다.

from typing import Self
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")

@dataclass
class LinkedList(Generic[T]):
    value: T
    next: Self | None = None

@dataclass
class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return str(self.value) # 예제에 맞게 변경

xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
# 타입 에러: OrdinalLinkedList가 예상되었지만, LinkedList[int]를 받았습니다.

if xs.next is not None:
    xs.next = OrdinalLinkedList(value=3, next=None) # OK
    xs.next = LinkedList[int](value=3, next=None) # Not OK (타입 에러)

위 코드는 Self 타입을 포함하는 각 속성을 해당 타입을 반환하는 @property처럼 취급하는 것과 의미론적으로 동일합니다.

from dataclasses import dataclass
from typing import Any, Generic, TypeVar

T = TypeVar("T")
Self = TypeVar("Self", bound="LinkedList") # 내부적으로 이렇게 처리될 수 있음

class LinkedList(Generic[T]):
    value: T
    _next: Any # 실제 내부 저장소

    @property
    def next(self: Self) -> Self | None:
        return self._next

    @next.setter
    def next(self: Self, next: Self | None) -> None:
        self._next = next

class OrdinalLinkedList(LinkedList[int]):
    def ordinal_value(self) -> str:
        return str(self.value)

제네릭 클래스에서의 사용 (Use in Generic Classes)

Self는 제네릭(Generic) 클래스 메서드에서도 사용할 수 있습니다.

from typing import Generic, TypeVar

T = TypeVar("T")

class Container(Generic[T]):
    value: T
    def set_value(self, value: T) -> Self: ...

이는 다음 코드와 동일합니다.

from typing import Any, Generic, TypeVar

T = TypeVar("T")
Self = TypeVar("Self", bound="Container[Any]") # 내부적으로 이렇게 처리될 수 있음

class Container(Generic[T]):
    value: T
    def set_value(self: Self, value: T) -> Self: ...

동작은 메서드가 호출된 객체의 타입 인자(type argument)를 보존하는 것입니다. 구체적인 타입 Container[int]를 가진 객체에서 호출될 때, SelfContainer[int]에 바인딩됩니다. 제네릭 타입 Container[T]를 가진 객체에서 호출될 때, SelfContainer[T]에 바인딩됩니다.

def object_with_concrete_type() -> None:
    int_container: Container[int] = Container() # 실제 인스턴스화
    str_container: Container[str] = Container() # 실제 인스턴스화
    # reveal_type(int_container.set_value(42)) # => Container[int] (타입 검사기에서)
    # reveal_type(str_container.set_value("hello")) # => Container[str] (타입 검사기에서)

def object_with_generic_type(
    container: Container[T],
    value: T,
) -> Container[T]:
    return container.set_value(value) # => Container[T]

이 PEP는 메서드 set_valueself.value의 정확한 타입을 명시하지 않습니다. 일부 타입 검사기는 Self = TypeVar("Self", bound=Container[T])와 같은 클래스-로컬 타입 변수를 사용하여 Self 타입을 구현하여 정확한 타입 T를 추론할 수 있습니다. 그러나 클래스-로컬 타입 변수가 표준화된 타입 시스템 기능이 아니라는 점을 감안할 때, self.value에 대해 Any를 추론하는 것도 허용됩니다. 이 부분은 타입 검사기에게 맡겨집니다.

Self[int]와 같이 타입 인자와 함께 Self를 사용하는 것은 거부됩니다. 이는 self 파라미터의 타입에 대한 모호성을 유발하고 불필요한 복잡성을 초래하기 때문입니다.

class Container(Generic[T]):
    def foo(
        self,
        other: Self[int], # 거부됨
        other2: Self,
    ) -> Self[str]: # 거부됨
        # ...

이러한 경우, self에 대해 명시적인 타입을 사용하는 것을 권장합니다.

class Container(Generic[T]):
    def foo(
        self: Container[T],
        other: Container[int],
        other2: Container[T]
    ) -> Container[str]: ...

프로토콜에서의 사용 (Use in Protocols)

Self는 클래스에서의 사용과 유사하게 프로토콜(Protocol) 내에서도 유효합니다.

from typing import Protocol, Self

class ShapeProtocol(Protocol):
    scale: float
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

위 코드는 다음 코드와 동일하게 처리됩니다.

from typing import TypeVar, Protocol

SelfShape = TypeVar("SelfShape", bound="ShapeProtocol")

class ShapeProtocol(Protocol):
    scale: float
    def set_scale(self: SelfShape, scale: float) -> SelfShape:
        self.scale = scale
        return self

프로토콜에 바운드된 TypeVar의 동작에 대한 자세한 내용은 PEP 544를 참조하세요.

클래스의 프로토콜 호환성 검사: 프로토콜이 메서드 또는 속성 어노테이션에 Self를 사용하는 경우, 클래스 Foo는 해당 메서드 및 속성 어노테이션에 Self 또는 Foo 또는 Foo의 서브클래스 중 하나를 사용하는 경우 프로토콜과 호환되는 것으로 간주됩니다. 다음 예시를 참조하세요.

from typing import Protocol

class ShapeProtocol(Protocol):
    def set_scale(self, scale: float) -> Self: ...

class ReturnSelf:
    scale: float = 1.0
    def set_scale(self, scale: float) -> Self:
        self.scale = scale
        return self

class ReturnConcreteShape:
    scale: float = 1.0
    def set_scale(self, scale: float) -> ReturnConcreteShape:
        self.scale = scale
        return self

class BadReturnType:
    scale: float = 1.0
    def set_scale(self, scale: float) -> int:
        self.scale = scale
        return 42

class ReturnDifferentClass:
    scale: float = 1.0
    def set_scale(self, scale: float) -> ReturnConcreteShape:
        return ReturnConcreteShape() # 인스턴스 생성

def accepts_shape(shape: ShapeProtocol) -> None:
    y = shape.set_scale(0.5)
    # reveal_type(y) # 타입 검사기에서 타입 확인

def main() -> None:
    return_self_shape: ReturnSelf
    return_concrete_shape: ReturnConcreteShape
    bad_return_type: BadReturnType
    return_different_class: ReturnDifferentClass

    accepts_shape(return_self_shape) # OK
    accepts_shape(return_concrete_shape) # OK
    accepts_shape(bad_return_type) # Not OK (반환 타입이 int이므로)
    # Not OK (서브클래스가 아닌 다른 클래스를 반환하므로)
    accepts_shape(return_different_class)

Self의 유효한 위치 (Valid Locations for Self)

Self 어노테이션은 클래스 컨텍스트 내에서만 유효하며, 항상 감싸는(encapsulating) 클래스를 참조합니다. 중첩된 클래스(nested classes)를 포함하는 컨텍스트에서는 Self가 항상 가장 안쪽 클래스를 참조합니다.

다음과 같은 Self 사용은 허용됩니다.

class ReturnsSelf:
    def foo(self) -> Self: ... # 허용됨
    @classmethod
    def bar(cls) -> Self: # 허용됨
        return cls()
    def __new__(cls, value: int) -> Self: ... # 허용됨
    def explicitly_use_self(self: Self) -> Self: ... # 허용됨
    # 허용됨 (Self는 다른 타입 내에 중첩될 수 있습니다)
    def returns_list(self) -> list[Self]: ... # 허용됨 (Self는 다른 타입 내에 중첩될 수 있습니다)
    @classmethod
    def return_cls(cls) -> type[Self]: return cls

class Child(ReturnsSelf): # 허용됨 (Self 어노테이션을 사용하는 메서드를 오버라이드할 수 있습니다)
    def foo(self) -> Self: ...

class TakesSelf:
    def foo(self, other: Self) -> bool: ... # 허용됨

class Recursive: # 허용됨 (@property가 Self | None을 반환하는 것으로 처리됩니다)
    next: Self | None

class CallableAttribute:
    def foo(self) -> int: ...
    # 허용됨 (@property가 Callable 타입을 반환하는 것으로 처리됩니다)
    bar: Callable[[Self], int] = foo

class HasNestedFunction:
    x: int = 42
    def foo(self) -> None:
        # 허용됨 (Self는 HasNestedFunction에 바인딩됩니다).
        def nested(z: int, inner_self: Self) -> Self:
            print(z)
            print(inner_self.x)
            return inner_self
        nested(42, self) # OK

class Outer:
    class Inner:
        def foo(self) -> Self: ... # 허용됨 (Self는 Inner에 바인딩됩니다)

다음과 같은 Self 사용은 거부됩니다.

def foo(bar: Self) -> Self: ... # 거부됨 (클래스 내부에 있지 않음)
bar: Self # 거부됨 (클래스 내부에 있지 않음)

class Foo:
    # 거부됨 (Self는 unknown으로 처리됩니다).
    def has_existing_self_annotation(self: T) -> Self: ...

class Foo:
    def return_concrete_type(self) -> Self:
        return Foo() # 거부됨 (아래 FooChild 참조)

class FooChild(Foo):
    child_value: int = 42
    def child_method(self) -> None:
        # 런타임 시, 이것은 Foo이며 FooChild가 아닙니다.
        y = self.return_concrete_type()
        # y.child_value # 런타임 에러: Foo에는 child_value 속성이 없습니다.
        # 위 시나리오가 문제가 되므로, Self를 반환할 때는 해당 서브클래스의 인스턴스를
        # 생성하여 반환해야 합니다 (예: return self.__class__())
class Bar(Generic[T]):
    def bar(self) -> T: ...
class Baz(Bar[Self]): ... # 거부됨

Self를 포함하는 타입 별칭(type aliases)은 거부됩니다. 클래스 정의 외부에서 Self를 지원하려면 타입 검사기에서 많은 특별 처리가 필요할 수 있습니다. 또한 Self를 클래스 정의 외부에서 사용하는 것이 이 PEP의 나머지 부분과도 상반된다는 점을 고려할 때, 별칭의 추가적인 편리함이 그만한 가치가 있다고 보지 않습니다.

TupleSelf = Tuple[Self, Self] # 거부됨
class Alias:
    def return_tuple(self) -> TupleSelf: # 거부됨
        return (self, self)

staticmethod에서는 Self가 거부됩니다. selfcls를 반환할 필요가 없으므로 Self는 큰 가치를 추가하지 않습니다. 유일하게 가능한 사용 사례는 파라미터 자체를 반환하거나 파라미터로 전달된 컨테이너의 요소를 반환하는 것일 것입니다. 이러한 것들은 추가적인 복잡성만큼의 가치가 없는 것으로 판단됩니다.

class Base:
    @staticmethod
    def make() -> Self: # 거부됨
        ...
    @staticmethod
    def return_parameter(foo: Self) -> Self: # 거부됨
        ...

마찬가지로, 메타클래스(metaclasses)에서는 Self가 거부됩니다. 이 PEP의 Self는 일관되게 동일한 타입(self의 타입)을 참조합니다. 하지만 메타클래스에서는 서로 다른 메서드 시그니처에서 다른 타입을 참조해야 할 것입니다. 예를 들어, __mul__에서 반환 타입의 Self는 감싸는 클래스 MyMetaclass가 아닌 구현 클래스 Foo를 참조할 것입니다. 그러나 __new__에서는 반환 타입의 Self가 감싸는 클래스 MyMetaclass를 참조할 것입니다. 혼동을 피하기 위해 이 예외적인 경우는 거부됩니다.

class MyMetaclass(type):
    def __new__(cls, *args: Any) -> Self: # 거부됨
        return super().__new__(cls, *args)
    def __mul__(cls, count: int) -> list[Self]: # 거부됨
        return [cls()] * count

class Foo(metaclass=MyMetaclass): ...

런타임 동작 (Runtime behavior)

Self는 subscriptable(첨자 사용 가능)하지 않으므로, typing.NoReturn과 유사한 구현을 제안합니다.

@_SpecialForm
def Self(self, params):
    """클래스에서 "self"의 타입을 나타내는 데 사용됩니다.
    예시::
        from typing import Self
        class ReturnsSelf:
            def parse(self, data: bytes) -> Self:
                ...
                return self
    """
    raise TypeError(f"{self} is not subscriptable")

거부된 대안 (Rejected Alternatives)

타입 검사기가 반환 타입을 추론하도록 허용 (Allow the Type Checker to Infer the Return Type)

한 가지 제안은 Self 타입을 암묵적으로 두고, 타입 검사기가 메서드 본문을 분석하여 반환 타입이 self 파라미터의 타입과 동일해야 한다고 추론하도록 하는 것입니다.

class Shape:
    def set_scale(self, scale: float):
        self.scale = scale
        return self # 타입 검사기가 self를 반환한다는 것을 추론합니다.

우리는 “명시적인 것이 암묵적인 것보다 낫다(Explicit Is Better Than Implicit)”는 이유로 이를 거부합니다. 또한, 위 접근 방식은 분석할 메서드 본문이 없는 타입 스텁(type stubs)에서는 실패할 것입니다.

참고 구현 (Reference Implementations)

  • Mypy: Mypy의 개념 증명(Proof of concept) 구현.
  • Pyright: v1.1.184
  • Self의 런타임 구현: PR.

자료 (Resources)

Python의 Self 타입에 대한 유사한 논의는 2016년경 Mypy에서 시작되었습니다. Mypy issue #1212 - SelfType or another way to spell “type of self”를 참조하세요. 그러나 최종적으로 채택된 접근 방식은 “before” 예시에서 보여진 바운드된 TypeVar 접근 방식이었습니다. 이와 관련된 다른 이슈로는 Mypy issue #2354 - Self types in generic classes가 있습니다.

Pradeep은 PyCon Typing Summit 2021에서 구체적인 제안을 발표했습니다. (녹화된 발표, 슬라이드 참조). James는 typing-sig에서 독립적으로 이 제안을 제기했습니다. (Typing-sig 스레드 참조).

다른 언어들도 감싸는 클래스의 타입을 표현하는 유사한 방법을 가지고 있습니다.

  • TypeScript는 this 타입을 가지고 있습니다 (TypeScript docs).
  • Rust는 Self 타입을 가지고 있습니다 (Rust docs).

이 PEP에 대한 피드백을 주신 다음 분들께 감사드립니다. Jia Chen, Rebecca Chen, Sergei Lebedev, Kaylynn Morgan, Tuomas Suutari, Eric Traut, Alex Waygood, Shannon Zhu, and Никита Соболев.

이 문서는 퍼블릭 도메인 또는 CC0-1.0-Universal 라이선스 중 더 관대한 조건으로 배포됩니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments