본문으로 건너뛰기

제네릭 타입에서 하한 경계(Lower Bounds)가 필요한 이유와 실무적 활용

Why have lower bounds on generics?

작성자
발행일
2025년 12월 24일
https://blog.jez.io/lower-bounds/

핵심 요약

  • 1 제네릭의 하한 경계는 해당 타입의 값을 직접 생성하거나 초기화하는 '도입 형태(Introduction forms)'를 정의함으로써 타입 안전성을 유지하며 유연한 객체 생성을 가능하게 합니다.
  • 2 하한 경계는 합집합 타입(Union types)으로 대체 가능해 보이지만, 상속 구조에서 인터페이스의 반환 타입을 엄격하게 준수해야 하는 복잡한 서브타이핑 관계에서는 필수적인 역할을 수행합니다.
  • 3 Sorbet의 fixed 제약 조건은 내부적으로 하한 경계를 활용하여 런타임에 제네릭 타입을 구체화하고, 추상 메서드를 오버라이드할 때 타입 시스템의 제약을 해결하는 핵심 기제입니다.

도입

제네릭 프로그래밍에서 상한 경계(Upper Bounds)는 익숙한 개념이지만, 하한 경계(Lower Bounds)의 필요성은 간과되기 쉽습니다. 본 글은 Sorbet과 Scala의 사례를 통해 하한 경계가 단순한 이론적 대칭을 넘어 실무에서 어떤 가치를 지니는지 탐구합니다. 특히 타입이 정의되는 방식인 '도입(Introduction)'과 '제거(Elimination)'의 관점에서 하한 경계가 객체 내부에서 값을 생성하고 다루는 데 어떻게 기여하는지 설명하며, 합집합 타입과의 차이점을 통해 그 독자적인 영역을 명확히 규명합니다.

1. 하한 경계와 값의 생성 (Introduction Forms)

타입 이론에서 모든 타입은 값을 생성하는 방식(Introduction)과 값을 사용하는 방식(Elimination)으로 정의됩니다. 상한 경계가 해당 타입을 어떻게 ‘사용’할 수 있는지(메서드 호출 등)를 결정한다면, 하한 경계는 해당 타입의 값을 어떻게 ‘생성’하거나 ‘할당’할 수 있는지를 정의합니다.

  • 예시: MaybeUninitBox 클래스에서 요소 타입 ElemNilClass를 하한 경계로 설정하면, 클래스 내부에서 @val = nil과 같은 할당이 가능해집니다.
  • 제약의 해소: 하한 경계가 없다면 타입 검사기는 nilElem의 하위 타입인지 확신할 수 없으므로 에러를 발생시킵니다. 하한 경계는 구현자가 외부에서 주입받지 않고도 특정 타입의 값을 안전하게 ‘도입’할 수 있는 권한을 부여합니다.

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)’의 정수를 보여줍니다.

결론

하한 경계는 상한 경계와 쌍대성을 이루며 타입 시스템의 완성도를 높이는 도구입니다. 비록 합집합 타입이 존재하는 현대적인 언어에서는 그 역할이 일부 대체되기도 하지만, 상속과 서브타이핑이 결합된 복잡한 구조에서는 여전히 대체 불가능한 가치를 제공합니다. 특히 Sorbet의 fixed와 같은 패턴을 통해 알 수 있듯, 정교한 타입 설계를 위해서는 하한 경계에 대한 깊은 이해가 필수적입니다. 이는 단순히 에러를 피하는 것을 넘어, 더 일반적이고 재사용 가능한 인터페이스를 설계하는 기반이 됩니다.

댓글0

댓글 작성

댓글 삭제 시 비밀번호가 필요합니다.

이미 계정이 있으신가요? 로그인 후 댓글을 작성하세요.

0/1000
정중하고 건설적인 댓글을 작성해 주세요.