2. Subtyping, Substitution 원리
Subtyping
이번 포스팅을 시작하기 앞서 Subtyping개념을 먼저 알아보겠습니다.
위와 같이 A 클래스가 가진 Spec이 요구되는 상황에서 B의 클래스가 A 역할을 대체하여 사용할 수 있을 때 B를 A의 Subtype, A는 B의 Supertype이라고 불립니다. 또한 모든 참조타입은 자기자신에 대해서 Subtype입니다.
(※ Subtype을 기호로 나타내면 <: 로 표현되며 A <: B 일 경우 A는 B의 Subtype 입니다.)
광의적인 의미에서 A 클래스의 Spec을 B 클래스에도 코드상에서 준수하고 있으면 이를 Subtype 관계로 보지만, Java에서는 상속 관계에 놓여있을 때만 교체가 가능하기에 이에 집중해서 살펴보겠습니다.
예를 통해서 알아보겠습니다.
ArrayList는 List의 인터페이스를 구현하였습니다. 따라서 List가 사용될 수 있는 영역에는 ArrayList로 대체가 가능합니다.
따라서, 이때 ArrayList는 List의 Subtype, List는 Supertype입니다.
Java에서 기본적으로 제공되는 라이브러리 예를 통해 해당 개념을 적용해보겠습니다.
- Integer <: Number
- Double <: Number
- ArrayList<E> <: List<E>
- Collection<E> <: Iterable<E>
Subtyping은 이행적인 특징을 지니고 있습니다.
즉 A <: B이고, B <: C 이면 A <: C입니다.
따라서, 위 예에서 List<E> <: Iterable<E> 라고 볼 수 있습니다.
Substitution Principle(치환 원리)
Wiki에는 해당 원리에 대하여 다음과 같이 정의하고 있습니다.
if S is a subtype of T, then objects of type T may be replcaed with objects of type S
요약하자면, 주어진 타입 변수는 해당 타입 어떠한 Subtype으로도 대체가 가능합니다.
Collection<E> 인터페이스에 정의된 add 메소드 사용예를 통해서 해당 원리을 적용해보겠습니다.
interface Collection<E>{
...
public boolean add(E elt);
...
}
public class Main{
public void static main(int argc, String[] argv){
List<Number> nums = new ArrayList<>();
nums.add(2);
nums.add(3.14);
}
}
nums 객체의 Type Argument는 Number 입니다.
따라서 add 메소드 파라미터 Type은 Number로 추론되므로 메소드 호출시, Integer나 Double 값을 할당할 수 있습니다.
이것이 가능한 이유는 Integer <: Number, Double <: Number 이기 때문입니다.
그렇기에 위 코드를 컴파일하면 아무런 문제 없이 완료됩니다.
그렇다면 해당 개념을 확장하여 다음과 같은 생각 또한 해볼 수 있습니다.
Integer <: Number이니까 List<Integer> <: List<Number> 라고 볼 수 있지 않을까?
위 문장만을 봐서는 가능할 것 같습니다.
예시 코드를 보면서 가능 여부를 확인해보겠습니다.
public class Main{
public void static main(int argc, String[] argv){
List<Integer> ints = Arrays.asList(1,2);
List<Number> nums = ints; //Compile 에러 발생
}
}
위 코드를 실행하면 컴파일 에러가 발생합니다. 과연 무엇이 문제였을까요?
위 코드가 정상적으로 컴파일된다면, 두 객체는 Heap 같은 메모리 영역을 바라보게 될 것입니다.
지금까지는 아무런 이상이 없는 것 처럼 보입니다.
public boolean add(Number elt){}
하지만 List<Number> 타입이 해당 메모리 영역을 가르킬 수 있으면 add 함수의 타입 파라미터는 Number로 추론됩니다. 따라서 Substitution Principle을 적용한다면 Number의 Subtype인 Double 값을 논리적으로 할당할 수 있습니다.
public class Main{
public void static main(int argc, String[] argv){
List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
List<Number> nums = ints //Compile 에러 발생
nums.add(3.14);
}
}
실제로 Intellij 에서 위 코드를 입력하면 Compile 에러 발생라인은 컴파일전에 에러가 발생하고 있다고 highlight 표시해주지만 nums 객체의 add 함수호출부는 아무런 표시를 하지 않는 것을 확인할 수 있습니다.
위 구조에서의 문제는 해당 객체가 저장된 공간은 Parameterized Type이 List<Integer>인 ints 객체 또한 가르키는 영역이라는 점입니다.
만약 nums 객체에 ints를 할당할 수 있다면 자신이 가르키고 있는 영역에 int가 아닌 다른 타입이 존재할 수 있기 때문에 타입 안정성이 깨지게됩니다.
이러한 문제를 해결하기 위해서 Java에서는 해당 경우에 Substitution Principle을 적용하지 않습니다. 즉 List<Integer>는 List<Number>의 Subtype이 아닙니다.
이번에는 반대로 한번 생각해보겠습니다.
List<Number> <: List<Integer>일까?
public class Main{
public void static main(int argc, String[] argv){
List<Number> nums = new ArrayList<>();
nums.add(2.78);
nums.add(3.14);
List<Integer> ints = nums; //Compile 에러 발생
}
}
Number 타입은 Integer의 Supertype이지 Subtype이 아닙니다. 따라서 위 경우에 nums 객체 값을 ints에 할당 가능하다면 List<Integer> 타입인 ints 입장에서는 Number 타입의 다른 Subtype이 존재하는 영역을 가르키게 됩니다.
문제는 ints 객체가 가르키는 메모리 영역으로부터 데이터를 읽고자 한다면 Integer 타입이 아닌 Double 타입을 참조할 수 있습니다. 따라서 이러한 경우에는 타입 안정성이 깨지게 됩니다. 그러한 이유로 이 또한 허용되지 않습니다.
지금까지 살펴본 내용을 정리해보겠습니다.
- List<Integer>는 List<Number>의 Subtype이 아니다.
- List<Number>는 List<Integer>의 Subtype이 아니다.
- List<Integer>는 자기 자신에 대하여 Subtype이다.
- List<E>는 Collection<E>를 상속받으므로 List<Integer>는 Collection<Integer>의 Subtype이다.
위 정리를 살펴보면, 주어진 타입과 동일한 타입을 지닌 객체에 대해서만 변수 할당이 가능한 것을 확인할 수 있습니다. 일반화를 하면 Generic을 사용하면 타입의 상속 관계와 상관 없이 동일 타입만 가능한 Invariant 특성을 지니고 있습니다.
하지만 이렇게만 사용한다면, Generic을 사용함에 있어 많은 제약이 따를 수 밖에 없습니다.
이전에 살펴본 코드를 잠시 다시 살펴보겠습니다.
public class Main{
public void static main(int argc, String[] argv){
List<Integer> ints = new ArrayList<>();
ints.add(1);
ints.add(2);
List<Number> nums = ints; //Compile 에러 발생
}
}
위 코드는 nums 객체 할당이후 Number 타입의 다른 Subtype 할당이 가능했기 때문에 문제가 발생했습니다.
하지만 해당 nums 객체가 지정하는 타입에 대하여 Subtype 값 할당은 하지 않고 단순히 참조의 목적이라면, List<Number>에 List<Integer> 객체를 할당하는 것은 논리적으로 문제 없어 보입니다.
위와 같은 상황에서 사용할 수 있는 방법이 없을까요?
다음 포스팅에서 다룰 Wildcard를 통해서 해당 이슈를 다루어보도록 하겠습니다.