Dart 완전 타입 시스템(Sound Type System)
핵심 정의
Dart는 완전 정적 타입 언어로, ‘정적 컴파일 검사 + 런타임 유효성 검사’ 이중 보호 체계를 갖추고 있습니다. 변수가 런타임에 담는 값은 정적으로 선언한 타입과 항상 일치합니다.
타입 규칙은 강제로 적용되지만 타입 어노테이션은 생략 가능하며 컴파일러가 자동으로 타입을 추론합니다.
컴파일 단계의 정적 분석으로 대부분의 타입 오류를 미리 차단합니다.
완전성(Soundness)이란 무엇인가
코드가 잘못된 타입 상태에 빠질 수 없습니다. 어떤 표현식의 정적 타입이 String이라면 런타임에는 문자열 값만 반환되고 다른 타입은 나올 수 없습니다. 유사 언어: Java, C#.
- 컴파일 타임 차단:
int a = "abc";즉시 오류 발생; - 런타임 차단:
as로 강제 캐스팅 시 타입이 일치하지 않으면 예외를 던집니다.
4가지 장점
컴파일 단계에서 타입 버그가 드러남, 재현하기 어려운 운영 환경 타입 크래시를 미리 방지;
코드 가독성이 높음, 타입이 개발자를 오도하지 않음;
유지보수가 쉬움, 한 곳의 코드를 수정하면 타입 검사로 영향받는 모든 구간을 표시;
AOT 컴파일 효율이 좋음, 타입 정보로 컴파일러가 더 최적화된 기계어를 생성.
예시 코드
동적 컬렉션으로 인한 정적 오류
// 매개변수는 List<int>를 받아야 함
void printInts(List<int> a) => print(a);
void main() {
// 제네릭 선언 없음, List<dynamic>로 자동 추론
final list = [];
list.add(1);
list.add('2'); // 동적 리스트는 여러 타입 혼합 가능
printInts(list);
// 컴파일 오류: List<dynamic>를 List<int>에 할당할 수 없음
}Code language: Dart (dart)
제네릭 제약 명시 선언
void printInts(List<int> a) => print(a);
void main() {
final list = <int>[]; // 제네릭 int 명시 지정
list.add(1);
// list.add('2'); // 컴파일 오류, 문자열 추가 불가
list.add(2);
printInts(list); // 정적 검사 통과
}Code language: Dart (dart)
3가지 핵심 규칙
1 메서드 오버라이드 시 반환 타입은 원본 타입 또는 그 하위 타입이어야 함(생산자 규칙)
상위 클래스 메서드의 반환값은 생산자로 간주되며 하위 클래스는 더 구체적인 하위 타입을 반환할 수 있지만 무관한 타입은 사용 불가합니다.
예시
정상 예시
class Animal {
Animal get parent => Animal();
}
class HoneyBadger extends Animal {
@override
HoneyBadger get parent => HoneyBadger(); // 하위 타입이므로 유효
}Code language: Dart (dart)
잘못된 예시
class Root {}
class HoneyBadger extends Animal {
@override
Root get parent => Root(); // 무관한 타입으로 컴파일 오류
}Code language: Dart (dart)
2 메서드 오버라이드 시 매개변수 타입은 원본 타입 또는 그 상위 타입이어야 함
메서드 매개변수는 소비자로 간주되며 하위 클래스는 매개변수 범위를 넓히는(상위 타입 사용) 것만 가능하고 하위 타입으로 좁히면 타입 안전성이 깨집니다.
정상 예시(매개변수를 Object로 넓힘)
class Animal {
void chase(Animal a) {}
}
class HoneyBadger extends Animal {
@override
void chase(Object a) {} // Object는 Animal의 상위 타입이므로 유효
}Code language: Dart (dart)
잘못된 예시(매개변수를 하위 클래스 Mouse로 좁힘)
class Mouse extends Animal {}
class Cat extends Animal {
@override
void chase(Mouse a) {} // 매개변수 타입 축소로 정적 오류 발생
}
void main() {
Animal cat = Cat();
cat.chase(Alligator()); // 이론상 Alligator 전달 가능해 타입 불안전
}Code language: Dart (dart)
규칙 3: 동적 제네릭 컬렉션을 확정형 제네릭 컬렉션에 직접 할당 불가
List<dynamic>, List<Dog>는 List<Cat>에 직접 할당할 수 없으며 컴파일 오류가 발생합니다. 서로 다른 제네릭 컬렉션 간 암시적 변환은 지원되지 않습니다.
class Cat {}
class Dog {}
void main() {
List<Cat> foo = <dynamic>[Dog()]; // 정적 컴파일 오류
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // 유효, 동적 리스트는 타입 혼용 가능
}Code language: Dart (dart)
런타임 타입 검증
정적 검사로 모든 상황을 커버할 수 없으므로 런타임에 추가 타입 검사가 진행되고 타입 불일치 시 예외를 던집니다.
제네릭 캐스팅 시 런타임 오류
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
// 런타임 예외: List<Dog>를 List<Cat>로 캐스트할 수 없음
}Code language: Dart (dart)
dynamic 암시적 다운캐스트(런타임 검증)
dynamic 변수를 구체 타입에 할당하면 암시적 변환이 발생하며 런타임에서 타입 검증 후 불일치하면 오류를 반환합니다.
int assumeString(dynamic object) {
String string = object; // 런타임에서 object가 String인지 검사
return string.length;
}
void main() {
assumeString("test"); // 정상 실행
assumeString(123); // 런타임 오류: int를 String으로 변환 불가
}Code language: Dart (dart)
암시적 캐스트 비활성화(엄격 타입 모드)
analysis_options.yaml에 엄격 캐스트 옵션을 활성화해 dynamic에서의 암시적 다운캐스트 차단
analyzer:
language:
strict-casts: trueCode language: JavaScript (javascript)
타입 추론 Type Inference
Dart는 변수, 제네릭, 메서드 반환값의 타입을 자동 추론합니다. 타입 정보가 부족한 경우 기본값으로 dynamic이 추론됩니다.
일반 변수 추론
// Map<String, Object>로 자동 추론
var arguments = {'argA': 'hello', 'argB': 42};
var x = 3; // x는 int로 추론
x = 4.0; // 오류, int에 double 저장 불가
num y = 3; // num을 명시 선언, int/double 모두 저장 가능
y = 4.0; // 유효Code language: Dart (dart)
제네릭 생성자 / 컬렉션 추론
List<int> listOfInt = []; // <int>[]로 자동 추론
var listOfDouble = [3.0]; // List<double>로 추론
var ints = listOfDouble.map((x) => x.toInt());
// x는 double, 콜백 반환값 int → ints는 Iterable<int> 추론Code language: Dart (dart)
완전 타입 시스템