Python의 강력한 타입 힌트 기능 중 하나인 typing.Protocol은 코드의 가독성과 유지보수성을 크게 향상시킵니다. 하지만 때로는 Protocol의 엄격한 규칙 때문에 예상치 못한 에러를 만나기도 합니다. 특히, 여러 클래스가 동일한 추상 메서드를 구현하지만, 메서드의 시그니처(인자 이름 등)가 미묘하게 다를 때 문제가 발생할 수 있습니다.
이 글에서는 typing.Protocol 사용 중 발생하는 함수 시그니처 불일치 에러의 원인을 분석하고, 이를 해결하여 유연하면서도 타입 안정성을 유지하는 방법에 대해 자세히 알아보겠습니다. Pylance와 같은 정적 타입 검사기가 보고하는 에러를 해결하여 여러분의 파이썬 프로젝트를 더욱 견고하게 만들어보세요.
1. 에러 발생 상황
typing.Protocol은 특정 인터페이스를 정의하고, 해당 인터페이스를 따르는 모든 클래스가 동일한 메서드 집합을 가지고 있음을 보장하는 데 사용됩니다. 예를 들어, put이라는 메서드를 가진 객체를 처리하기 위한 SupportsPut 프로토콜을 정의하고 싶다고 가정해 봅시다. 파이썬 표준 라이브러리에는 multiprocessing.Queue와 queue.Queue라는 두 가지 큐 구현체가 있습니다. 이들은 모두 put 메서드를 지원하지만, 첫 번째 인자의 이름이 서로 다릅니다:
# multiprocessing.Queue의 put 메서드:
def put(self, obj, block=True, timeout=None): ...
# queue.Queue의 put 메서드:
def put(self, item, block=True, timeout=None): ...
이 두 큐를 모두 SupportsPut 프로토콜의 대상으로 지정하려고 하면 문제가 발생합니다. 만약 프로토콜의 put 메서드 인자를 item으로 정의한다면 다음과 같은 상황이 벌어집니다.
from typing import Protocol
import multiprocessing as mp
import queue as q
class SupportsPut(Protocol):
def put(self, item, block=True, timeout=None):
...
def do_something_with(thing: SupportsPut):
# 실제 호출 시에는 인자 이름을 명시하지 않고 위치 인자로 호출할 수 있습니다.
thing.put("data")
# queue.Queue는 프로토콜을 잘 따릅니다.
do_something_with(q.Queue())
# 하지만 multiprocessing.Queue는 에러를 발생시킵니다.
# Pylance 등의 타입 검사기에서 "mp.Queue.put doesn’t conform to the protocol’s put" 에러 발생
do_something_with(mp.Queue())
위 코드에서 multiprocessing.Queue()를 do_something_with 함수에 전달하려 할 때, Pylance와 같은 정적 타입 검사기는 mp.Queue.put이 SupportsPut 프로토콜의 put 메서드와 일치하지 않는다고 에러를 보고합니다. 비록 런타임에 thing.put("data")와 같이 위치 인자로 호출하면 정상적으로 작동하더라도, 정적 타입 검사는 메서드의 시그니처, 특히 인자 이름까지도 엄격하게 비교하기 때문입니다.
2. 명확한 발생 원인
이러한 에러는 typing.Protocol이 정적 타입 검사의 정확성을 보장하기 위해 메서드 시그니처에 대해 매우 엄격한 규칙을 적용하기 때문에 발생합니다.
- 시그니처 일치 강제: Python의
typing.Protocol(PEP 544)은 프로토콜을 구현하는 클래스의 메서드 시그니처가 프로토콜에 정의된 메서드 시그니처와 호환되어야 한다고 명시합니다. 이 호환성에는 인자의 개수, 순서뿐만 아니라 필수 인자의 이름까지 포함됩니다. - 정적 타입 검사의 목적: Pylance와 같은 정적 타입 검사기는 코드를 실행하기 전에 잠재적인 타입 관련 문제를 찾아내는 것을 목표로 합니다. 메서드 시그니처의 엄격한 검사는 이러한 목적을 달성하기 위한 핵심 요소입니다. 인자 이름의 불일치는 타입 검사기 입장에서 예측 불가능한 호출 패턴(예: 키워드 인자 사용 시)으로 이어질 수 있으므로, 이를 에러로 간주합니다.
- 덕 타이핑(Duck Typing)과의 차이: 런타임 덕 타이핑(“오리처럼 꽥꽥거리고 오리처럼 걷는다면, 그것은 오리다”)은 메서드의 존재 여부와 호출 가능성에 중점을 둡니다. 반면
typing.Protocol은 이러한 런타임 덕 타이핑의 개념을 정적 타입 검사 영역으로 가져왔지만, 타입 안전성을 위해 더 엄격한 규칙(예: 시그니처 일치)을 적용합니다.
결론적으로, multiprocessing.Queue.put의 첫 번째 인자가 obj이고 SupportsPut.put의 첫 번째 인자가 item이므로, 타입 검사기는 이를 호환되지 않는 시그니처로 판단하고 에러를 발생시키는 것입니다.
3. 해결 방법 및 코드 예시
typing.Protocol의 엄격함 때문에 인자 이름 불일치를 직접적으로 “유연하게” 처리하는 내장 메커니즘은 없습니다. 하지만 몇 가지 전략을 통해 이 문제를 효과적으로 해결하고 타입 안정성을 유지할 수 있습니다.
3.1. 어댑터(Adapter) 패턴 활용
가장 권장되는 해결책은 Adapter 패턴을 사용하는 것입니다. 이는 기존 클래스의 인터페이스가 Protocol의 요구사항과 일치하지 않을 때, 중간에 래퍼(wrapper) 클래스를 두어 Protocol을 준수하도록 만드는 방법입니다.
from typing import Protocol, Any
import multiprocessing as mp
import queue as q
# 기존 Protocol 정의
class SupportsPut(Protocol):
def put(self, item: Any, block: bool = True, timeout: float | None = None) -> None:
...
# multiprocessing.Queue를 SupportsPut에 맞게 조정하는 어댑터 클래스
class MpQueueAdapter:
def __init__(self, mp_queue: mp.Queue):
self._mp_queue = mp_queue
# Protocol의 put 시그니처를 따르도록 구현
def put(self, item: Any, block: bool = True, timeout: float | None = None) -> None:
# 실제 호출 시에는 내부 mp_queue의 obj 인자에 item을 전달
self._mp_queue.put(item, block=block, timeout=timeout)
def do_something_with(thing: SupportsPut):
print(f"Using {type(thing).__name__}")
thing.put("Important Data 1")
thing.put("Important Data 2", block=False)
#thing.put(item="Important Data 3", timeout=1) # 키워드 인자로 호출 시에도 문제 없습니다.
# 이제 두 큐 모두 do_something_with 함수와 함께 사용할 수 있습니다.
print("--- Using queue.Queue ---")
do_something_with(q.Queue()) # q.Queue는 SupportsPut 프로토콜을 직접 만족합니다.
print("\n--- Using multiprocessing.Queue with Adapter ---")
mp_q_adapter = MpQueueAdapter(mp.Queue())
do_something_with(mp_q_adapter) # 어댑터를 통해 mp.Queue도 SupportsPut 프로토콜을 만족합니다.
# 원본 multiprocessing.Queue는 여전히 프로토콜을 직접 만족하지 않습니다.
# do_something_with(mp.Queue()) # 여전히 Pylance 에러 발생!
이 방법은 기존 코드를 수정할 필요 없이 Protocol의 요구사항을 충족시키면서도, 타입 검사기가 만족하는 안전한 코드를 작성할 수 있게 해줍니다.
3.2. Protocol의 인자 이름을 가장 일반적인 것으로 통일 (제한적 해결)
만약 인자 이름이 아닌 다른 시그니처 요소(예: 추가 키워드 인자) 때문에 문제가 발생했다면, Protocol에서 가장 기본적인 시그니처를 정의하고 구현체에서 추가적인 인자를 허용하는 방식으로 유연성을 확보할 수 있습니다. 예를 들어:
# Protocol은 최소한의 인자만 정의
class SupportsProcess(Protocol):
def process(self, data: Any) -> Any: ...
# 구현체는 더 많은 인자를 가질 수 있습니다.
class MyProcessor:
def process(self, data: Any, config: str = "default") -> Any: # config 인자 추가
print(f"Processing {data} with config: {config}")
return data
def use_processor(processor: SupportsProcess):
processor.process("My Data") # 타입 검사 통과
# use_processor(MyProcessor()) # OK
하지만, 원래 질문의 핵심인 ‘필수 인자 이름 불일치’ 문제(obj vs item)는 이 방법으로 해결되지 않습니다. Protocol은 필수 인자의 이름까지도 일치해야 한다고 간주하기 때문입니다. 따라서 이 경우에는 3.1에서 설명한 어댑터 패턴이 가장 적합한 해결책입니다.
4. 향후 예방을 위한 팁
typing.Protocol을 효과적으로 사용하고 유사한 에러를 예방하기 위한 몇 가지 팁입니다.
- 일관된 API 설계: 새로운 라이브러리나 모듈을 설계할 때는 가능한 한 메서드 시그니처를 일관되게 유지하도록 노력하세요. 이는
Protocol을 사용하는 개발자가 직면할 수 있는 혼란을 줄여줍니다. Protocol의 목적 이해:typing.Protocol은 덕 타이핑의 이점을 제공하면서도 정적 타입 검사를 통해 강력한 타입 안전성을 보장하기 위해 설계되었습니다. 따라서 시그니처의 엄격함은 의도된 동작이며, 이를 우회하려 하기보다는Protocol의 규칙에 맞추어 코드를 작성하는 것이 바람직합니다.- 어댑터 패턴의 적극 활용: 외부 라이브러리나 제어할 수 없는 레거시 코드에
Protocol을 적용해야 할 경우,Adapter패턴은 매우 유용합니다. 이는 기존 코드를 변경하지 않고도 인터페이스를 유연하게 연결하는 강력한 디자인 패턴입니다. - 타입 힌트의 점진적 적용: 모든 시그니처를 처음부터 완벽하게 맞추기 어렵다면, 가장 핵심적인 부분부터
Protocol과 타입 힌트를 적용하고, 복잡한 부분은 필요에 따라 어댑터 등을 활용하여 점진적으로 확장하는 방법을 고려할 수 있습니다. typing.Any의 신중한 사용: 때때로 인자의 타입이나 심지어 이름이 중요하지 않다면Any를 사용할 수도 있지만, 이는 타입 검사의 이점을 크게 줄입니다. 인자 이름 불일치 문제에서는Any가 인자의 타입을 유연하게 만들 수는 있으나, 인자 이름 자체의 불일치로 인한 에러를 직접적으로 해결하지 못합니다.
typing.Protocol은 Python 코드베이스의 견고성을 높이는 강력한 도구입니다. 이 글에서 제시된 해결책과 팁들을 활용하여 Protocol의 엄격함 속에서도 유연하게 코드를 작성하고, Pylance 에러 없이 안정적인 애플리케이션을 구축하시길 바랍니다.
![[에러 해결] [Python/PyTorch] invalid-method-override: 메서드 오버라이드 경고 원인 및 해결 [에러 해결] [Python/PyTorch] invalid-method-override: 메서드 오버라이드 경고 원인 및 해결](https://dev-error.com/wp-content/plugins/contextual-related-posts/default.png)