JAVA/Generic

2. Subtyping, Substitution 원리

cla9 2020. 3. 5. 18:24

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로 추론되므로 메소드 호출시, IntegerDouble 값을 할당할 수 있습니다.

 

이것이 가능한 이유는 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를 통해서 해당 이슈를 다루어보도록 하겠습니다.