왜 타입 시스템이 중요한가
원문은 Matthias Endler님이 에 게재했습니다. 이 블로그 구독하기
나는 대부분의 코드를 Python이나 PHP 같은 동적 타입 언어로 작성해 왔다. 하지만 Rust를 조금 만져본 이후로 정적 타입 시스템에 열정을 갖게 되었다.
점점 아주 자연스럽게 느껴졌다. 마치 완전히 새로운 방식으로 나를 표현하는 것 같았다.
타입은 도움을 주기 위해 존재한다
타입을 통해 우리는 보장과 기대를 전달한다. 기계에게도, 다른 개발자에게도 마찬가지다. 타입은 의도를 표현한다.
프로그래머라면 타입에 대한 어느 정도의 직관을 갖고 있을 것이다.
sentence = "hello world"sentence가 문자열일 거라고 짐작할 수 있다. 어쨌든 따옴표로 감싸여 있으니까. 하지만 타입이 다른 곳에서 추론된다면 이야기는 조금 더 복잡해진다.
sentence = xsentence는 여전히 문자열일까? 음… 알 수 없다. x의 타입에 달려 있다. 어쩌면 x는 숫자이고, 그래서 sentence도 숫자일지 모른다? 어쩌면 x는 원래 문자열이었지만 리팩터링 과정에서 이제는 바이트 배열이 되었을지도 모른다? 정말 즐거운 상황이 아닐 수 없다. 🎉
이런 경우는 어떨까?
filesize = "5000" # Size in bytes여기서는 파일 크기를 문자열로 표현했다.
이렇게 해도 동작은 하겠지만, 어딘가 불안한 발상이다.
간단한 계산조차 예상치 못한 결과를 낳을 수 있다:
file1 = "5000"
file2 = "3000"
total = file1 + file2
print(total) # prints '50003000'어떻게 고칠 수 있을까?
파일 크기는 항상 숫자라고 안전하게 가정할 수 있다. 더 정확히 말하면, 양의 자연수여야 한다. 파일 크기가 음수일 수는 없으며, 메모리의 가장 작은 단위는 1바이트다(가장 특수한 시스템을 제외하면). 그리고 우리는 이산적인 기계를 다루고 있으므로, 컴퓨터가 처리할 수 있는 범위 내의 파일 크기여야 한다는 것도 안다. 이 모든 것을 정확한 방식으로 표현할 수만 있다면…?
바로 이 지점에서 타입 시스템이 등장한다.
Rust에서는 File 타입을 만들고 size라는 필드를 정의할 수 있다.
struct File {
name: String,
size: usize,
}usize는 메모리의 어떤 포인터든 담을 수 있을 만큼 항상 충분히 크다는 것을 보장한다(64비트 컴퓨터에서는 usize = u64). 이제 size의 타입에 대한 모호함은 사라졌다. 잘못된 파일 객체를 생성할 수조차 없다:
// Error: `size` can't be a string.
let weird_file = File { name: 123, size: "hello" };타입 시스템은 잘못된 상태를 막아준다. 스스로 정한 규칙을 어기는 것을 단순히 허용하지 않는다. 설계상의 선택에 대해 책임을 지게 만든다. 감히 말하자면, 타입 시스템은 뇌의 연장이 된다. 시간이 지나면 타입 검사기에 의존하게 된다. “컴파일되면 동작한다”는 강력한 주문이다.
타입은 가독성을 높이고 맥락을 제공한다
다음 Python 코드를 보자:
def filter_files(files):
matches = []
for file in files:
if file.status == 0:
matches.append(file)
return matches0은 무엇을 의미할까? 알 수 없다. 맥락이 부족하기 때문이다!
다음과 같이 enum 타입을 정의하면 이야기가 조금 더 명확해진다:
from enum import Enum
class FileStatus(Enum):
OPEN = 0
CLOSED = 1위의 예시는 이렇게 바뀐다
def filter_files(files):
matches = []
for file in files:
if file.status == FileStatus.OPEN:
matches.append(file)
return matches규모가 큰 코드베이스에서는 0보다 FileStatus.OPEN을 검색하는 것이 훨씬 쉽다.
참고: 네이티브 enum 타입은 Python 역사에서 아주 늦게 도입되었다. 이는 타입 시스템을 강화하는 것이 가독성 향상에 어떻게 도움이 되는지 보여주는 좋은 예다.
서로 다른 타입을 조합하면 마법이 일어난다.
타입을 현명하게 선택하면 모든 조각이 갑자기 제자리를 찾는다. 어디선가 컴파일러가 설계 결정을 검증하기 시작하고, 모든 타입이 서로 잘 어울리는지 확인해 준다. 머릿속 모델의 결함을 짚어낸다. 덕분에 리팩터링할 때 큰 자신감을 얻을 수 있다.
예를 들어 정렬을 생각해 보자. 정렬이라고 하면 나는 가장 먼저 숫자로 이루어진 리스트를 떠올린다:
sorted([1,5,4,3,2]) # [1,2,3,4,5]이건 이상적인 경우다. 그럼 이건 어떨까?
sorted(1)이런. 이건 동작할 수 없다. 1은 단일 숫자이지 컬렉션이 아니기 때문이다! sorted에 전달하기 전에 타입을 확인하는 것을 잊으면 프로그램 실행 중에 에러가 발생한다.
sorted([1, "fish"])Python 2에서는 결과가 [1, 'fish']가 된다(문자열이 길이로 비교되기 때문에)
수정: Reddit 사용자 jcdyer3가 지적했듯이 그 이유는 비교할 수 없는 타입들을 비교할 때 타입을 기준으로 정렬되어 모든 정수가 모든 문자열보다 앞에 오기 때문이다. 이는 CPython 구현상의 세부 사항이다.)
출처: Freepik 제공 일러스트
Python 3부터는 예외가 발생한다.
TypeError: '<' not supported between instances of 'str' and 'int'훨씬 낫다! 에러 원인이 하나 줄었다. 다만 문제는 이런 일이 런타임에 일어난다는 점이다. Python의 동적 타이핑 때문이다. 정적 타입 언어였다면 이를 피할 수 있었을 것이다.
fn sorted<T>(collection: &mut [T]) where T: PartialOrd {
// TODO: Sort the collection here.
}무섭게 보이지만 사실 그렇지 않다.
sorted라는 함수를 정의하는데, collection이라는 입력 매개변수 하나를 받는다.
collection의 타입은 네 부분으로 이루어져 있다:
&는 컬렉션을 “빌린다”는 뜻으로, 소유하지 않는다는 의미다. 함수가 반환된 후에도 컬렉션은 계속 존재한다. 정리되지 않는다.mut는 컬렉션이 가변적이라는 뜻이다. 수정할 수 있다.[T]는 입력으로 리스트/슬라이스/벡터를 기대한다는 뜻이다. 그 외의 것은 컴파일 타임에(프로그램이 실행되기도 전에) 거부된다.PartialOrd가 핵심 비법이다. 이는 trait으로, 일종의 인터페이스 같은 것이다. 컬렉션 안의 모든 원소T가 부분 순서를 가져야 한다는 뜻이다.
이 모든 정보는 컴파일러가 우리가 스스로 발등을 찍는 일을 방지하도록 돕는다. 그리고 우리는 다른 곳을 찾아보지 않고도 함수의 입력과 출력을 이해할 수 있다.
정리
- 타입은 개발자가 숙제를 하도록 강제하고 코드의 보장과 한계에 대해 고민하게 만든다.
- 타입을 제약으로 생각하지 말고, 자신의 결함 있는 멘탈 모델로부터 보호해 줄 안전망이라고 생각하라.
- 항상 자신의 의도를 가장 정확하게 표현하는 타입을 선택하라.
- 표준 라이브러리에 완벽한 타입이 없다면, 더 단순한 타입들을 조합해 직접 만들어라.
이런 규칙들을 따르다 보니, 마법처럼 내 아이디어를 가장 우아하게 표현하는 방향으로 이끌린다는 것을 알게 되었다. 내 코드는 훨씬 더 관용적으로 변했다.
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기