1. 하한 경계와 값의 생성 (Introduction Forms)
타입 이론에서 모든 타입은 값을 생성하는 방식(Introduction)과 값을 사용하는 방식(Elimination)으로 정의됩니다. 상한 경계가 해당 타입을 어떻게 ‘사용’할 수 있는지(메서드 호출 등)를 결정한다면, 하한 경계는 해당 타입의 값을 어떻게 ‘생성’하거나 ‘할당’할 수 있는지를 정의합니다.
- 예시:
MaybeUninitBox클래스에서 요소 타입Elem에NilClass를 하한 경계로 설정하면, 클래스 내부에서@val = nil과 같은 할당이 가능해집니다. - 제약의 해소: 하한 경계가 없다면 타입 검사기는
nil이Elem의 하위 타입인지 확신할 수 없으므로 에러를 발생시킵니다. 하한 경계는 구현자가 외부에서 주입받지 않고도 특정 타입의 값을 안전하게 ‘도입’할 수 있는 권한을 부여합니다.
2. 합집합 타입(Union Types)과의 비교 및 한계
Sorbet과 같은 언어에서는 T.nilable(Elem)과 같은 합집합 타입을 통해 하한 경계와 유사한 효과를 낼 수 있습니다. 하지만 이는 완벽한 대체제가 아닙니다.
- 사용성 측면: 합집합 타입을 사용하면 사용자가 제네릭 인자를 전달할 때
T.nilable을 명시하지 않아도 되므로 API가 더 깔끔해 보일 수 있습니다. - 서브타이핑의 문제: 합집합 타입을 사용하면 메서드의 반환 타입이 기존 제네릭 타입보다 ‘넓어지는’ 문제가 발생합니다. 이는 상속 관계에서 부모 인터페이스의 시그니처를 위반하게 되는 원인이 됩니다.
3. 복잡한 서브타이핑과 상속 구조에서의 역할
하한 경계가 합집합 타입보다 강력한 이유는 객체 지향의 다형성을 유지하면서도 타입 안전성을 보장하기 때문입니다.
- 인터페이스 준수: 특정 인터페이스가
Elem타입을 반환하도록 정의되어 있을 때, 합집합 타입을 사용한 구현체는T.nilable(Elem)을 반환하게 되어 타입 불일치 에러를 발생시킵니다. 반환 타입은 공변성(Covariance)에 따라 부모보다 좁거나 같아야 하기 때문입니다. - 해결책: 하한 경계를 사용하면
Elem타입의 정의 자체에 하한을 포함시킴으로써, 시그니처를Elem으로 유지하면서도 내부적으로는 하한 타입(예:nil)을 다룰 수 있게 됩니다. 이는 기존 서브타이핑 관계를 깨뜨리지 않고 유연한 구현을 가능하게 합니다.
4. Sorbet의 fixed 제약 조건과 실무적 패턴
Sorbet 사용자라면 자신도 모르게 하한 경계를 사용하고 있을 가능성이 높습니다. 대표적인 사례가 fixed 제약 조건입니다.
- 런타임 구체화(Reification): 제네릭 타입을 런타임에 확인해야 할 때, Sorbet은
fixed를 사용하여 상한과 하한을 동일한 타입으로 고정하는 기법을 권장합니다. - 구현의 안전성: 추상 클래스의
type_member를 특정 클래스로 고정할 때, 하한 경계가 작동하여 해당 클래스의 인스턴스를 안전하게 반환할 수 있도록 보장합니다. 이는 타입 시스템이 추상화된 메서드 구현을 검증하는 데 핵심적인 역할을 합니다.
5. 제네릭 메서드에서의 확장성
Scala와 같은 언어에서는 메서드 수준의 하한 경계([U >: Elem])를 통해 타입 확장(Type Widening)을 지원합니다.
- 컬렉션 연산: 배열에 새로운 요소를 추가할 때, 기존 타입과 다른 타입이 들어오면 두 타입의 공통 조상으로 결과를 추론해야 합니다. 하한 경계는 이러한 추론 과정에서 결과 타입이 최소한 기존 요소 타입을 포함하도록 강제합니다.
- 불변성과의 조화: 특히 불변 컬렉션 라이브러리에서 하한 경계는 타입 안전성을 유지하면서도 더 일반적인 타입으로의 변환을 자연스럽게 유도하는 ‘타입 주도 설계(Type-driven design)’의 정수를 보여줍니다.