Launguage

Python TypeVar와 제네릭, 제대로 알고 쓰자

Somaz 2026. 1. 2. 00:00
728x90
반응형

Overview

Python 코드를 작성하다 보면 `TypeVar('T')` 나 `Generic[T]` 를 사용하게 된다.

 

코드를 복사해서 쓰긴 하는데, 정확히 뭘 하는 건지 모르겠다면?

이번 글에서 TypeVar와 제네릭의 개념부터 실전 활용까지 완벽하게 정리한다.

 

 

 

 

 


 

TypeVar란 무엇인가?

 

 

기본 개념

from typing import TypeVar

T = TypeVar('T')

 

TypeVar는 "제네릭 타입 변수"를 선언하는 것이다.

 

쉽게 말하면

  • T는 "어떤 타입이든 될 수 있는" 플레이스홀더
  • 사용할 때 구체적인 타입이 결정됨
  • 한 번 정해지면 그 타입으로 고정됨

 

 

제네릭이 필요한 이유

 

 

문제 상황: 타입이 불명확한 코드

# 제네릭 없이
class ProcessResult:
    def __init__(self, data, success: bool):
        self.data = data  # 타입이 뭔지 모름
        self.success = success

result = ProcessResult([1, 2, 3], True)
# IDE가 result.data의 타입을 알 수 없음
# 자동완성 안됨, 타입 체크 불가능

 

 

문제점

  • data가 무슨 타입인지 알 수 없음
  • IDE 자동완성 지원 안됨
  • 타입 체크가 불가능해 런타임 에러 발생 가능

 

해결책: 제네릭 사용

# 제네릭 사용
from typing import Generic, TypeVar

T = TypeVar('T')

class ProcessResult(Generic[T]):
    def __init__(self, data: T, success: bool):
        self.data: T = data
        self.success = success

# 사용 시 타입 지정
result: ProcessResult[list[int]] = ProcessResult([1, 2, 3], True)
# IDE가 result.data가 list[int]인 걸 인식!
# 자동완성 지원, 타입 체크 가능

 

 

장점

  • 타입 안정성 확보
  • IDE 자동완성 지원
  • 코드 가독성 향상
  • 버그 사전 방지

 

 

 

 

실전 활용 예시

 

 

1. 기본 사용법

from typing import Generic, TypeVar

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, content: T):
        self.content: T = content
    
    def get(self) -> T:
        return self.content

# 사용 예시
int_box: Box[int] = Box(123)
str_box: Box[str] = Box("Hello")
list_box: Box[list[str]] = Box(["a", "b", "c"])

print(int_box.get())  # 123 (타입: int)
print(str_box.get())  # "Hello" (타입: str)

 

 

2. 여러 타입 변수 사용

from typing import Generic, TypeVar

K = TypeVar('K')
V = TypeVar('V')

class KeyValuePair(Generic[K, V]):
    def __init__(self, key: K, value: V):
        self.key = key
        self.value = value

# 사용 예시
pair1: KeyValuePair[str, int] = KeyValuePair("age", 25)
pair2: KeyValuePair[int, list[str]] = KeyValuePair(1, ["a", "b"])

 

 

3. 제약이 있는 TypeVar

from typing import TypeVar

# 숫자 타입만 허용
T = TypeVar('T', int, float)

def add(a: T, b: T) -> T:
    return a + b

result1 = add(1, 2)        # ✅ OK (int)
result2 = add(1.5, 2.5)    # ✅ OK (float)
# result3 = add("a", "b")  # ❌ 에러 (str은 허용 안됨)

 

 

 

비교 분석: Any vs Generic

 

 

시나리오 1: Any 사용

from typing import Any

class Container:
    def __init__(self, value: Any):
        self.value = value
    
    def get(self) -> Any:
        return self.value

container = Container(123)
result = container.get()
# IDE: result의 타입을 알 수 없음 (Any)
# result.upper()  # 런타임 에러!

시나리오 2: Generic 사용

from typing import Generic, TypeVar

T = TypeVar('T')

class Container(Generic[T]):
    def __init__(self, value: T):
        self.value = value
    
    def get(self) -> T:
        return self.value

container: Container[int] = Container(123)
result = container.get()
# IDE: result가 int임을 정확히 인식
# result.upper()  # IDE가 미리 경고!

 

 

비교표

항목 Any Generic[T]
타입 안정성 없음 강력함
IDE 자동완성 불가능 완벽 지원
타입 체크 불가능 가능
유연성 매우 높음 제한적
권장 용도 레거시 코드 신규 코드

 

 

흔한 실수와 해결법

 

 

실수 1: TypeVar 선언만 하고 활용 안함

# 잘못된 사용
T = TypeVar('T')

class ProcessResult:  # Generic[T] 빠짐!
    def __init__(self, data: T, success: bool):
        self.data = data
        self.success = success

# 제네릭이 작동 안함!

 

 

올바른 사용

# 올바른 사용
T = TypeVar('T')

class ProcessResult(Generic[T]):  # Generic[T] 상속!
    def __init__(self, data: T, success: bool):
        self.data: T = data
        self.success = success

 

 

실수 2: 타입 힌트 생략

# 타입 힌트 없음
def process(data):
    result = ProcessResult(data, True)
    return result

# 타입 힌트 명시
def process(data: T) -> ProcessResult[T]:
    result = ProcessResult(data, True)
    return result

 

 

실수 3: 여러 TypeVar를 섞어서 사용

# 잘못된 사용
T = TypeVar('T')
U = TypeVar('U')

class Wrong(Generic[T]):
    def method(self, value: U) -> U:  # U는 클래스 레벨에 없음!
        return value

# 올바른 사용
class Correct(Generic[T, U]):
    def method(self, value: U) -> U:
        return value

 

 

 

 

 

 

실무 활용 사례

 

 

API 응답 래퍼

from typing import Generic, TypeVar, Optional

T = TypeVar('T')

class ApiResponse(Generic[T]):
    def __init__(
        self,
        data: Optional[T] = None,
        error: Optional[str] = None,
        status_code: int = 200
    ):
        self.data = data
        self.error = error
        self.status_code = status_code
    
    def is_success(self) -> bool:
        return self.error is None and self.data is not None

# 사용 예시
class User:
    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name

def get_user(user_id: int) -> ApiResponse[User]:
    # API 호출
    user = User(user_id, "홍길동")
    return ApiResponse(data=user, status_code=200)

response = get_user(123)
if response.is_success():
    user = response.data  # 타입: Optional[User]
    print(user.name)  # IDE가 User 타입임을 인식!

 

 

데이터 캐시 구현

from typing import Generic, TypeVar, Dict, Optional
from datetime import datetime, timedelta

K = TypeVar('K')
V = TypeVar('V')

class Cache(Generic[K, V]):
    def __init__(self, ttl_seconds: int = 300):
        self._cache: Dict[K, tuple[V, datetime]] = {}
        self._ttl = timedelta(seconds=ttl_seconds)
    
    def set(self, key: K, value: V) -> None:
        self._cache[key] = (value, datetime.now())
    
    def get(self, key: K) -> Optional[V]:
        if key not in self._cache:
            return None
        
        value, timestamp = self._cache[key]
        if datetime.now() - timestamp > self._ttl:
            del self._cache[key]
            return None
        
        return value

# 사용 예시
user_cache: Cache[int, User] = Cache(ttl_seconds=600)
user_cache.set(123, User(123, "홍길동"))

user = user_cache.get(123)  # 타입: Optional[User]
if user:
    print(user.name)

 

 

 

 

 

 

TypeVar 고급 기능

 

 

1. bound 파라미터

from typing import TypeVar

# T는 반드시 Number의 서브클래스여야 함
class Number:
    def __init__(self, value: float):
        self.value = value

T = TypeVar('T', bound=Number)

def double(n: T) -> T:
    n.value *= 2
    return n

 

 

2. covariant와 contravariant

from typing import TypeVar

# covariant: 서브타입 관계 유지
T_co = TypeVar('T_co', covariant=True)

# contravariant: 서브타입 관계 역전
T_contra = TypeVar('T_contra', contravariant=True)

 

 

 

 

 

요약 및 체크리스트

 

 

핵심 요약 

항목 설명
T "어떤 타입이든 될 수 있는" 플레이스홀더
TypeVar('T') 제네릭 타입 변수 선언
Generic[T] 클래스를 제네릭으로 만듦
목적 타입 안정성 + 재사용성
효과 IDE 자동완성, 타입 체크

 

 

 

사용 전 체크리스트

 

 

TypeVar를 선언했는가?

T = TypeVar('T')

 

 

Generic[T]를 상속했는가?

class MyClass(Generic[T]):

 

 

타입 힌트를 명시했는가?

def __init__(self, data: T):
    self.data: T = data

 

 

사용 시 타입을 지정했는가?

result: MyClass[int] = MyClass(123)

 

 

 

 

 

 

 

 


 

 

 

 

결론

TypeVar와 제네릭은 Python의 타입 시스템을 강력하게 만드는 핵심 도구이다.

 

 

언제 사용해야 하나?

  • 여러 타입을 다루는 컨테이너 클래스
  • API 응답 래퍼
  • 캐시, 큐 등 자료구조
  • 재사용 가능한 유틸리티 함수

 

 

사용의 이점

  • 타입 안정성 확보
  • IDE 지원 최대화
  • 코드 가독성 향상
  • 런타임 에러 사전 방지

 

 

 

 

 

 


Reference

https://docs.python.org/3/library/typing.html

https://typing.python.org/en/latest/reference/generics.html

https://mypy.readthedocs.io/en/stable/generics.html

https://medium.com/pythoneers/understanding-typevar-in-python-f78e5108471d

728x90
반응형