[리팩토링] mypy를 이용한 타입힌트, 서브 클래스 시그니처 불일치, 타입 불일치 리팩토링 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 타입을 str로 설정하게 된 배경

  • .env에 저장된 값을 함수 호출부에서 이용해야 한다.
  • 이때 os.getenv를 통해 가져온 Option[str]을 함수의 str 인자 자리에 넣어야 한다.
  • 여기서 타입 힌팅과 관련해서 타입이 맞지 않는다는 문제 발생. (기대하는 타입: str, 들어온 타입: Option[str])
  • 이럴 막기 위해 assert로 None이 아님을 보장함으로써 타입이 str인것을 명확히 해 타입 힌팅 에러를 없애려고 했으나 아래와 같은 이유로 인해 되지 않았다.
  • assert의 한계
    • .env에서 값을 불러온 후 다음처럼 검사를 할 수 있다:

      assert BUCKET_NAME is not None, "버킷 이름이 필요합니다"
      
    • 하지만 이는 런타임에만 작동하며, 정적 타입 검사기(mypy)는 여전히 해당 변수를 Optional[str]로 인식한다.

    • 즉, assert는 타입 체커가 변수의 타입을 좁히는 데 도움을 주지 못한다.


2. 해결 전략

a. assert 대신 ifraise로 변경

if BUCKET_NAME is None:
    raise ValueError("BUCKET_NAME은 .env에 설정되어야 합니다.")
  • mypy는 위처럼 조건문을 통한 흐름 제어를 인식하여 타입을 str로 좁힌다.
  • 따라서 정적 분석 + 런타임 모두 안전하게 보장된다.

b. 로깅 데코레이터의 타입 힌팅을 비활성화한 배경


def log_exception(func: Callable[P, R]) -> Callable[P, R]:
    """예외 발생 시 자동으로 로깅하는 데코레이터"""

    @wraps(func)
    def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        try:
            return func(*args, **kwargs)
        except Exception as e:
            logger.opt(depth=1).exception(
                f"{func.__name__} 함수 예외 발생: {e}"
            )
            raise

    @wraps(func)
    async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        try:
            return await func(*args, **kwargs)  # type: ignore
        except Exception as e:
            logger.opt(depth=1).exception(
                f"{func.__name__} 함수 예외 발생: {e}"
            )
            raise

    return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper  # type: ignore

  • 로깅 데코레이터 정의하는 부분에서도 타입 힌팅이 필요하다.
    • 이때, 비동기 함수에 이용할 데코레이터 부분은 Callable[P, R]로 커버되지 않는다.
  • 데코레이터 정의 부분에서 인자와 반환형이 각각 동기, 비동기 wrapper인지에 따라 분기된다.
    • sync_wrapper, async_wrapper는 각기 다른 타입 (Callable[P, R], Callable[P, Awaitable[R]])이므로, 분기 반환 시 타입 오류가 발생
  • 그런데 정적 타입 체커로는 둘 중 어느 타입인지 추론할 수 없기 때문에 에러가 발생한다.
    • 정적 분석에만 문제가 있고, 로깅 데코레이터를 런타임에 사용하는 것은 정상 동작하는 상황.
  • 이를 막기 위한 방법으로는 2가지가 있다:
    1. type: ignore ← 타입 체크를 하지 않는 방식
    2. @overload를 이용해 여러 타입 분기해서 나타내기
  • 선택한 방법: # type: ignore를 데코레이터 반환부에 붙여서 타입 체커 에러를 무시
    • 이유:
    1. 현재 동작 자체에는 문제가 없고 정적 타입 체킹에서 로깅 데코레이터 부분 경고가 뜨지 않게 하는 것이 목적

      1. 타입 체킹을 안 뜨게 하기 위해서 overload를 하면 코드가 지저분해진다.

      2. overload를 해서 타입 체킹을 유지할만큼 로깅 데코레이터의 타입 체킹이 필요하지 않다.

      3. 런타임 동작에서는 타입 분기가 적절히 이루어지지만 단순히 정적 타입 분석기가 이를 인식하지 못하는 상황인 만큼 정적 타입 분석기를 이 부분에 대해 비활성화함으로써 간단히 문제를 해결해도 된다고 판단했다.