F-Lab
🚀
상위 1% 개발자에게 1:1로 멘토링 받아 성장하세요

Python 코드에 타입 검사 도입하기

writer_thumbnail

F-Lab : 상위 1% 개발자들의 멘토링

Python 은 동적 타입 언어입니다. 동적 타입 언어는 코드를 빠르게 작성하고 다양한 표현을 가능하게 하지만 강한 타입 시스템이 지원하는 타입 에러와 같은 안전망이 존재하지 않기 때문에 크고 복잡한 코드베이스에서 디버깅을 어렵게 하기도 합니다. 

 

Python 에서는 이런 문제를 해결하기 위해서 PEP 484가 제안되었고 그 명세를 만족시키는 정적 분석 도구로 mypy가 만들어졌습니다. mypy는 정적 타입 검사를 통해서 잘못된 타입으로 인해서 발생할 수 있는 오류를 빠르게 잡아낼 수 있도록 돕습니다. 

 

이 블로그 글에서는 mypy 도입 과정을 설명하고 Python 프로그램의 구성요소들을 타입으로 표현하기 위해서 어떤 기능들을 사용할 수 있는지 소개합니다. 더해서 mypy 도입을 통해서 어떤 오류들을 잡아낼 수 있는지도 알아보겠습니다.

 

 

도입하기


 

mypy의 도입은 아주 간단합니다. 선호하는 패키지 매니저를 이용해서 mypy를 설치 후 아래와 같이 파일을 검사할 수 있습니다. 필자가 현업에서 큰 코드베이스에 mypy 도입하는 과정에서 코드나 프로젝트 구조를 바꾸지 않고 mypy를 도입할 수 있었기 때문에 비교적 쉽게 팀원들을 설득할 수 있었다고 생각합니다.

 

$ mypy [target file]

 

여기서부터는 아래 피보나치 수열의 열번째 위치의 값을 출력하는 코드를 예시로 들어 보겠습니다.

 

def fibonacci(n):
 if n <= 1:
   return 1
 elif n == 2:
   return 1
 else:
   return fibonacci(n-1) + fibonacci(n-2)
   
print(fibonacci(10))

 

mypy를 이용해서 파일을 검사하면 Success: no issues found in 1 source file 라는 결과를 반환합니다. 코드에 타입 정보가 하나도 존재하지 않는데도 검사가 성공한 이유는 mypy가 점진적 타이핑을 지원하기 위해서 타입 정보가 없는 코드의 경우 검사 시점에 건너뛰기 때문입니다. 점진적 타이핑은 mypy가 가진 가장 큰 장점 중 하나입니다.

 

Dropbox에서 400만줄의 파이썬 코드에 타입 검사를 도입한 과정을 설명한 블로그 글을 보면 점진적 타이핑이 어떻게 처음 만들어졌는지가 나와있습니다. 점진적 타이핑의 핵심은 개발자가 초기에는 동적 타입을 이용해 빠른 속도로 프로토타입을 만들어내고 점차 프로덕션에 더 적합한 정적 타입의 코드로 프로토타입을 개선해나가는데 있습니다.

 

 

함수에 타입 추가

위의 코드에 아래와 같이 fibonacci  함수에 타입 정보를 추가해보겠습니다. 타입 정보에 관한 명세가 궁금하신 경우 PEP 484 또는 typing module 문서를 참고하면 됩니다.

 

def fibonacci(n: int) -> None:
 if n <= 1:
   return 1
 elif n == 2:
   return 1
 else:
   return fibonacci(n-1) + fibonacci(n-2)
   
print(fibonacci(10))

 

위와 같이 타입 정보를 추가해보았습니다. 반환값의 타입이 None 으로 지정되어 있지만 실제로는 int 를 반환하기 때문에 mypy가 아래와 같이 타입 정보와 다른 return 명령이 존재하는 오류를 찾아냅니다.

 

fibonacci.py:3: error: No return value expected  [return-value]
fibonacci.py:5: error: No return value expected  [return-value]
fibonacci.py:7: error: "fibonacci" does not return a value (it only ever returns None)  [func-returns-value]
fibonacci.py:9: error: "fibonacci" does not return a value (it only ever returns None)  [func-returns-value]
Found 4 errors in 1 file (checked 1 source file)

 

 

타입 추론

그렇다면, 코드에 존재하는 모든 변수와 매개변수에 대해서 타입 정보를 기입해줘야 mypy를 사용할수 있는 걸까요? 그렇지는 않습니다. 

 

mypy는 타입 추론을 통해서 작성자가 타입 정보를 기입하지 않은 변수에 대해서도 타입 정보를 확보할 수 있습니다. 예를 들어서 아래 코드를 mypy로 검사하면 Argument 1 to "fibonacci" has incompatible type "str"; expected "int" 와 같은 에러 메시지를 확인할 수 있습니다.

 

def fibonacci(n: int) -> None:
 if n <= 1:
   return 1
 elif n == 2:
   return 1
 else:
   return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci('foo'))

 

타입 추론은 개발자가 코드 전체에 걸쳐서 타입 정보를 기입하는 일을 피할 수 있도록 해 줄 뿐만 아니라 사람이 타입 정보를 기입하면서 생길 수 있는 실수를 줄이는데도 도움을 줍니다.

 

 

typing 모듈

위의 예시 코드들에서는 Python 인터프리터가 제공하는 타입들을 이용해서 타입 정보를 기술했습니다. 보다 풍부한 표현을 위해서 Python 에서는 typing 이라는 모듈을 통해서 추가적인 타입들을 제공합니다. 

 

대표적으로 ‘Any’ 는 어떤 타입이든 가질 수 있는 변수나 매개변수를 의미합니다. ‘Callable’ 은 함수나 익명함수의 타입 정보를 정의하는데 사용될 수 있습니다. ‘Union’ 은 변수나 매개변수가 하나 이상의 타입을 가질 수 있는 경우를 표현하기 위해서 사용됩니다. ‘Optional’ 은 변수나 매개변수가 None이거나 주어진 타입이라는 것을 의미하며, 실제로는 Union 타입의 특정한 사용 사례에 대한 syntax sugar 입니다. ‘Iterator’ 와 ‘Generator’ 는 각각 이터레이터와 제네레이터의 타입 정보를 정의하기 위해 사용될 수 있습니다. 각각의 구체적인 명세는 typing module 문서를 참고할 수 있습니다.

 

any_variable: Any = 1
any_variable = 'a'

callable_variable: Callable[[int], int] = lambda x: x+1

union_variable__int_or_bool: Union[int, bool] = 1
union_variable__int_or_bool = False

optional_variable: Optional[int] = 1
optional_variable = None

 

 

클래스에 타입 도입

Python 함수의 타입 정보는 ‘Callable’ 을 이용해서 표현 가능한 것을 위 문단에서 배웠습니다. 그렇다면 Python 클래스는 어떻게 표현할 수 있을까요? 

 

Python 클래스는 그 자체로서 타입 정보를 표현하는데 사용될 수 있습니다. 하지만 클래스의 구성 요소들에 대해서는 타입 정보를 제공할 필요가 있습니다. 대표적으로 클래스의 메소드들에 대해서 타입 정보가 존재하지 않으면 mypy는 해당 메소드에 대해서 타입 검사를 수행하지 않습니다. 자식 클래스에서 메소드를 오버라이드 하는 경우에는 부모 클래스에서 정의된 타입 정보에 의한 제약이 존재합니다. 가령 자식 클래스에서 오버라이드 된 메소드에서 같은 이름의 매개변수는 같은 타입, 또는 기존에 정의된 타입의 서브타입으로만 존재할 수 있습니다. 메소드의 매개변수의 개수를 줄이는 것은 불가능하고 매개변수를 추가하는 경우 매개변수의 기본 값이 존재해야만 합니다. 

 

또한, 클래스의 정의에 포함된 변수의 값이 인스턴스를 통해서 수정되는 것을 막고 싶다면 ‘ClassVar’ 타입을 이용할 수 있습니다. 만약 클래스에서 사전에 정의되지 않은 타입을 매개 변수나 반환값으로 가져야 할 필요가 있다면 ‘Generic’ 타입을 이용할 수 있습니다.

 

class ClassA:
 a: ClassVar[int] = 0
 b: float = 0.0
 
 def isLargerThan(self, other: float) -> bool:
   return self.b > other

instanceA = ClassA()
# instanceA.a = 2  # mypy 가 에러 메시지를 반환한다

class ClassB(ClassA):
 # int 는 float 의 서브타입이 아니기 때문에 mypy 가 에러 메시지를 반환한다
 # def isLargerThan(self, other: int) -> bool:
 #   return self.b > other 
 def isLargerThan(self, other: float, forceTrue: bool = True):
   return forceTrue or self.b > other
   
# int 의 서브타입인 타입 인자 T를 선언한다
T = TypeVar('T', bound=int)

# T 는 int 의 서브타입이기 때문에 target + 1 은 항상 실행될 수 있다
class GenericClass(Generic[T]):
 def plusOne(self, target: T):
   return target + 1

 

또 한가지 흥미로운 점은 mypy가 추상 클래스와 일반 클래스를 구분하고 추상 클래스, 또는 추상 메소드를 가지는 클래스를 이용해 인스턴스 생성을 시도하는 코드에 대해서 오류를 찾아내는 기능을 가지고 있다는 점입니다. 

 

 

기대 효과

이와 같이 mypy는 풍부한 표현을 가능하게 하는 다양한 도구들을 제공합니다. 이러한 풍부한 표현은 Python 프로그램이 높은 수준의 안정성을 달성할 수 있도록 해 줍니다.

 

mypy를 이용하면 아래와 같은 실수들을 런타임을 거치지 않고 찾아낼 수 있습니다.

 

  • 매개변수에 잘못된 타입의 값을 전달하는 실수
  • 잘못된 타입의 값을 반환하는 실수
  • 변수에 다른 타입의 값을 대입하는 실수
  • 존재하지 않는 빌트인 함수나 메소드를 사용하는 실수
  • Optional 인 변수에 대해서 None인지 검사를 하지 않는 실수
  • 추상 클래스를 인스턴스화하는 실수

 

이러한 실수들은 프로젝트의 크기가 커지고 복잡도가 증가하면서 자연스럽게 나타나는 종류의 것들입니다. 필자는 현업에서 여러 크고 작은 Python 프로젝트를 다루면서 이러한 실수들이 빈번하게 발생하는 것을 보았고 어떤 경우에는 제품과 회사에 큰 손실로 이어지는 사례도 보았습니다. 이러한 실수들이 이어지면 코드를 읽고 쓰는데 더 많은 노력이 들어가게 되고 팀 사기를 떨어뜨릴 수도 있기 때문에 사람의 노력을 적게 들이면서 실수를 줄일 수 있는 방법이 필요합니다. 필자는 mypy가 제공하는 다양한 도구들을 이용해서 Python 코드에 타입 검사를 도입하는 것이 위에서 언급한 실수를 줄일 수 있는 가장 좋은 방법 중 하나라고 생각합니다. 

 

블로그 글의 가장 앞에서 언급했듯이 mypy는 도입 비용이 적기 때문에 프로젝트가 이미 커진 상황에서도 도입이 비교적 쉽고 점진적 타이핑을 통해서 프로젝트 코드를 순차적으로 개선해나갈 수 있다는 장점이 있습니다. 만약 독자들 중 Python 코드에서 위와 같은 실수를 빈번하게 발견하는 분이 있다면 mypy를 도입해보시기를 추천합니다.

 

 

Jacob

* F-Lab 멘토링, 'Python Backend' 과정 멘토
* '센드버드' 출신, 엔지니어링 조직 문화와 생산성에 관심이 많은 개발자
ⓒ F-Lab & Company

이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.

조회수

멘토링 코스 선택하기

  • 코스 이미지
    Java Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Frontend

    기술과 브라우저를 Deep-Dive 하며 성능과 아키텍처, UX에 능한 개발자로 성장하는 과정

  • 코스 이미지
    Android

    아키텍처 설계 능력과 성능 튜닝 능력을 향상시키는 안드로이드 Deep-Dive 과정

  • 코스 이미지
    Python

    대규모 서비스를 지탱할 수 있는 대체 불가능한 백엔드, 데이터 엔지니어, ML엔지니어의 길을 탐구하는 성장 과정

  • 코스 이미지
    iOS

    언어와 프레임워크, 모바일 환경에 대한 탄탄한 이해도를 갖추는 iOS 개발자 성장 과정

  • 코스 이미지
    Node.js Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    ML Engineering

    머신러닝과 엔지니어링 자체에 대한 탄탄한 이해도를 갖추는 머신러닝 엔지니어 성장 과정

  • 코스 이미지
    Data Engineering

    확장성 있는 데이터 처리 및 수급이 가능하도록 시스템을 설계 하고 운영할 수 있는 능력을 갖추는 데이터 엔지니어 성장 과정

  • 코스 이미지
    Game Server

    대규모 라이브 게임을 운영할 수 있는 처리 능력과 아키텍처 설계 능력을 갖추는 게임 서버 개발자 성장 과정

  • 코스 이미지
    Game Client

    대규모 라이브 게임 그래픽 처리 성능과 게임 자체 성능을 높힐 수 있는 능력을 갖추는 게임 클라이언트 개발자 성장 과정

  • 코스 이미지
    해외취업 코스

    해외 취업을 위한 구체적인 액션을 해보고, 해외 취업에 대한 다양한 정보를 얻을 수 있는 과정

  • 코스 이미지
    Devops 코스

    대규모 아키텍처를 설계할 수 있고, 그 인프라를 구성할 수 있는 엔지니어로 성장하는 과정

F-Lab
소개채용멘토 지원
facebook
linkedIn
youtube
instagram
logo
(주)에프랩앤컴퍼니 | 사업자등록번호 : 534-85-01979 | 대표자명 : 박중수 | 전화번호 : 0507-1315-4710 | 제휴 문의 : info@f-lab.kr | 주소 : 서울특별시 강남구 테헤란로63길 12, 438호 | copyright © F-Lab & Company 2024