- 프로그래밍 시 타입 시스템을 활용하여 서로 다른 데이터 의미를 명확히 구분할 수 있음
-
문자열이나 정수처럼 일반적인 타입을 그대로 사용하는 것은 맥락을 잃게 하며 버그로 이어질 수 있음
- 동일한 기반 타입이더라도 목적에 맞게 새로운 타입을 정의하면 컴파일 타임 오류로 실수를 방지 가능
- Go 라이브러리 libwx에서는 측정 단위를 명확히 구분하는 타입들을 정의해 float64 혼용에 의한 실수를 방지
- 예시 코드에서 UUID 타입을 UserID와 AccountID로 분리해 잘못된 사용을 컴파일러가 차단
- Go처럼 타입 시스템이 강하지 않은 언어에서도 간단한 타입 래핑으로 버그를 예방할 수 있음
타입 시스템을 적극적으로 활용하자
문제의 출발점: 단순 타입의 혼용
- 프로그래밍에서는 string, int, UUID 같은 기본 타입만으로 많은 값을 표현하는 경우가 많음
- 하지만 프로젝트 규모가 커지면 이런 단순 타입이 서로 구분 없이 혼용되어 사용되는 실수가 잦아짐
- 예: userID 문자열을 실수로 accountID로 넘기거나, int 인자가 3개 있는 함수에서 순서를 잘못 넘기는 실수 등
해결책: 의도를 드러내는 타입 정의
-
int나 string은 빌딩 블록일 뿐, 시스템 전반에 그대로 넘기면 의미 있는 맥락이 사라짐
- 이를 방지하려면 역할별로 고유한 타입을 정의해서 사용해야 함
- 예:
type AccountID uuid.UUID
type UserID uuid.UUID
func UUIDTypeMixup() {
{
userID := UserID(uuid.New())
DeleteUser(userID)
// 에러 없음
}
{
accountID := AccountID(uuid.New())
DeleteUser(accountID)
// 에러: AccountID 타입을 UserID로 사용할 수 없음
}
{
accountID := uuid.New()
DeleteUserUntyped(accountID)
// 컴파일 타임 에러 없음, 런타임에 문제가 발생할 가능성 높음
}
}
- 이렇게 하면 잘못된 타입의 인자를 컴파일 타임에 차단할 수 있음
실제 적용 사례: libwx 라이브러리
- 필자는 자신의 Go 라이브러리 libwx에서 이 기법을 실천 중
- 모든 측정 단위에 대해 전용 타입을 정의하고, 단위 변환 메서드도 타입에 연결
- 예: Km.Miles() 메서드를 통해 단위를 명확히 구분함
- 아래는 잘못된 함수 인자 순서와 단위 혼동을 컴파일러가 차단하는 예시:
// 화씨 온도 선언
temp := libwx.TempF(84)
// 상대습도 선언(퍼센트)
humidity := libwx.RelHumidity(67)
// 화씨 대신 섭씨 온도를 요구하는 함수에 잘못 전달
fmt.Printf("Dew point: %.1fºF\n",
libwx.DewPointC(temp, humidity))
// 컴파일러가 타입 mismatch 오류를 바로 검출
// temp (TempF 타입)는 TempC로 사용할 수 없음
// 함수에 인자 순서 잘못 전달
fmt.Printf("Dew point: %.1fºF\n",
libwx.DewPointF(humidity, temp))
// 컴파일러가 인자 타입 오류를 막아줌
- 단순히 float64를 썼다면 발생할 수 있는 실수들을 모두 예방 가능
결론: 타입 시스템을 적극 활용하자
- 타입 시스템은 단순히 문법 검사용이 아니라 버그 예방 도구
- 모델마다 ID 타입을 따로 정의하고, 함수 인자도 float이나 int 대신 명확한 타입으로 감싸야 함
- 이 방식은 Go처럼 타입 시스템이 강하지 않은 언어에서도 매우 효과적이며 구현도 간단
- 현실에서는 UUID나 문자열 타입 혼용에 의한 버그가 정말 많음
- 이 간단한 방식이 생산 코드에서 흔히 사용되지 않는 현실이 놀랍다고 저자는 강조함
관련 코드