알지만 안 했던 것: 정적 분석

의외로 알고 있지만 하지 않는 것들이 있다. 작년에 파이썬 클린 코딩을 강의하면서 소개했던 것 중 하나가 typing이다.
파이썬은 변수의 타입을 강제하지 않기 때문에 코드가 간단하다.
변수를 만들 때 타입을 명시하지 않기 때문에, 처음 시작하는 사람들에게는 오히려 직관적이기도 하다.

typing을 하면 직관적으로 좋은 점은 IDE에 힌트를 주기 때문에 코드 자동 완성이나 레퍼런스를 보기 정말 편하다.
만약 파이썬 언어를 사용하는데 typing을 하지 않고 프레임워크를 통해 개발하게 되면, 대부분의 레퍼런스가 연결되지 않아 코드 작성하는 것이 꽤 불편해진다.
이런 장점도 있지만, 가장 큰 장점은 정적 분석이 가능하다는 것인데, 안 해왔던 이유와 지금은 왜 하려는 지를 써보려고 한다.

안 해온 이유는 간단하다. 그 이전에도 안 해왔기 때문이다. 개발자가 아니더라도 많은 분야에서 나타날 것 같은데, 새로운 인풋이 없으면 일은 관성대로 흘러간다. 이런 관성은 분야에 따라서 몇십년 경력의 베테랑이 될 수도 있지만, 옛 방식을 고집하는 고인물이 될 수도 있다. 나도 마찬가지로 고이고 있었다.

그러던 와중에 플러터를 공부하면서, 자바나 C만큼의 강한 타입 언어는 아니지만 타입을 사용하면서 장점을 다시 깨닫게 되었다. 그렇게 파이썬 프로젝트들에도 정적 분석을 적용해야겠다는 생각이 들었다. 생각해 보면 typescript의 lint와 같은 최소한의 정적 분석도 없이, 부실하고 빈약한 테스트 코드에만 의존했다는 것이 얼마나 용감했나 싶다.

그렇게 다짐하고 자료를 찾아봤는데, 마치 유튜브 알고리즘 같았다. 이런 인지를 하기 전에 파이썬 관련된 자료를 볼 때면 대부분 쓰지 않고 공식 문서처럼 typing 없이 쓰고 있는 줄 알았는데, 인지하고 난 뒤에 자료를 보니 다들 귀신같이 typing을 쓰고있었다.

파이썬에서 typing은 PEP 484 라는 프로포절에서 시작되어 들어갔다. 그리고 정적 분석 도구는 mypy라는 도구를 사용했다.

일반적으로 파이썬 코드는

# app.py
def my_sum(a, b):
    return a + b


value = my_sum(100, 200)
print(value)

value = my_sum("test", "test2")
print(value)

와 같이 작성한다. 이렇게 작성한 코드를 mypy로 테스트해 보면 문제가 없다고 나온다.

(venv) ➜ mypy app.py
Success: no issues found in 1 source file

Typing한 파이썬 코드는

# app.py
def my_sum(a: int, b: int) -> int:
    return a + b


value = my_sum(100, 200)
print(value)

value = my_sum("test", "test2")
print(value)

와 같이 작성한다. 이렇게 작성한 코드를 mypy로 테스트해 보면 my_sum 함수에 문자열을 전달하는 부분에서 에러가 발생한다.

(venv) ➜ mypy app.py
app.py:8: error: Argument 1 to "my_sum" has incompatible type "str"; expected "int"  [arg-type]
app.py:8: error: Argument 2 to "my_sum" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

하지만, mypy로 테스트하지 않고 실행하면 문제없이 실행된다.

(venv) ➜ python app.py
300
testtest2

이처럼 파이썬은 (PEP 484의 이름도 Type Hints 인 점에서 알 수 있듯이) 타입을 강제하는 것이 아니라 힌트를 전달하는 것이다.

이처럼 정적 분석 도구를 이용해서 타입 검사를 강제할 수 있다. 심지어 첫 번째 예시처럼 타입을 명시하지 않았을 때를 허용하지 않는 옵션(–disallow-untyped-defs)를 추가로 넣어 에러를 발생시킬 수도 있다.

현재 회사에서 개발 중인 프로젝트에 적용해 보기로 했으니, DRF 용 패키지를 설치해서 테스트했다. djangorestframework-stubs 라는 패키지를 사용했다.
이미 운영중인 서비스에 적용해 보려고 했으나, 혹-시나 발생할 사이드 이펙트가 두려워서, 많이 진행되지 않은 프로젝트에 적용해 보기로 했다.

기존에 단순히 파일만 적용하는 것이 아니라 프레임워크 내 파일들을 찾아 검사 해야하기 때문에 설정 파일이 필요하다.

[mypy]
plugins = mypy_drf_plugin.main
ignore_missing_imports = True

[mypy.plugins.djangorestframework-stubs]
django_settings_module = "{project_name}.settings"

mypy 설정에 플러그인을 활성화 하고, django 프로젝트의 설정 파일을 명시해서 활성화할 수 있다. ignore_missing_imports는 사용하는 외부 패키지에 대한 typed를 설정하지 않더라도 무시하겠다는 의미이다. 예를 들어 DRF 외에 ckeditor라는 패키지를 사용하고 있다면, 해당 패키지에 대한 mypy 설정을 로드 해야 하는데 이 부분이 누락되었다고 에러를 내는 것이다.
모든 패키지에 대한 mypy 설정은 차차 늘려가야 할 부분이라 설정값으로 해결했다.

(venv) ➜ mypy .
...
Found 383 errors in 49 files (checked 197 source files)

패키지와 mypy 설정만 한 후에 아무런 코드 수정 없이 바로 테스트하니 수많은 에러가 나왔다. 이제 이 에러를 해결하는 것이 첫 번째 단계가 될 것 같다.

이 과정은 분명히 Trade-Off가 있지만, 이제는 미룰 수 없는 것 같다. 혼자 개발할 때, 두 명이 개발할 때는 괜찮다고 생각했던 적이 있다. 이제 보니 몇 명인지는 중요하지 않은 것 같다.
1인 개발자도 사실 n명의 개발자다. 과거의 내가 작성한 코드는 남의 코드보다 기억이 안 날 때가 있다. 내가 작성하는 코드에 대한 신뢰는 적을수록 좋은 것 같다.. 그리고 그것을 보완할 장치를 계속해서 고민하는 것이 좋겠다는 생각이 든다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다