변성(Variance)

변성(Variance)

변성은 무엇이고, 자바에는 어떻게 적용되는가?


변성(Variance)


최근 함수형 프로그래밍을 공부하면서 변성이라는 키워드가 나왔는데, 이게 무엇인지 모르겠어서 알아봤다.

쉽게 얘기해보자면 변성(Variance)이란 것은 타입간의 관계에 대한 표현이다.

이를 자바에 대입해 어렵게 얘기하면 기저타입(Base Type)이 같고, 타입 인자(Type Argument)가 다른 경우 두 타입간에 어떠한 관계가 있느냐인데, 자바를 사용하면서 가장 많이 사용하게 되는 제네릭인 List<Integer>와 같은것을 예로 들면, List가 기저타입이고, <Integer>가 타입인자라고 보면 된다.


image


용어가 좀 거부감들어서 그렇지 조금 더 쉽게 풀어보면 다음과 같다.


가정: 타입 S타입 T의 하위타입일 경우 Box<S> Box<T>의 하위 타입인가?

  • 무공변(invariance): 관계없다
  • 공변(covariance): 그렇다
  • 반공변(contravariance): 오히려 Box<S>Box<T>의 상위타입이다


이를 왜 공부해야 할까?

작성하는 프로그램의 유연성과 안전성을 위함이다.

여기서 안정성이라는것은 타입 안정성을 의미하며, 유연성이란 것은 쉽게 확장 혹은 축소 될 수 있음을 의미한다.


무공변(invariance)


먼저 알아두어야 할 것은 자바의 배열은 기본적으로 공변이며, 제네릭은 무공변이라는 것이다.

이 말이 무슨 의미이냐?

자바의 자료형은 다음과 같은 계층구조를 따른다는 것을 알 것이다.


image


무공변이라는 것은 타입 S타입 T의 하위타입이지만 Box<S>Box<T>간에는 상하 관계가 없다는 것이다.

이를 자바 코드로 풀어보면 다음과 같다.


List<Number> integers = new ArrayList<Integer>(); // 컴파일 에러 
List<Number> doubles = new ArrayList<Double>(); // 컴파일 에러 
List<Number> longs = new ArrayList<Long>(); // 컴파일 에러 


Integer, Double, LongNumber의 하위타입이지만 List<Number>List<Integer>, List<Double>, List<Long>을 할당할 수 없다.

자바의 제네릭은 무공변, 즉 List<Integer>List<Number>의 하위타입이 아니기 때문이다.


공변(covariance)


자바의 배열은 기본적으로 공변이라 하였다.

공변이라는 것은 타입 S타입 T의 하위타입일 경우 Box<S>Box<T>의 하위 타입이라는 것이다.

Integer, Double, LongNumber의 하위타입이기 때문에 Integer[], Double[], Long[]은 모두 Number[]의 하위타입이다.

이를 자바 코드로 풀어보면 다음과 같다.


Number[] integers = new Integer[5]; // ok
Number[] doubles = new Double[5]; // ok
Number[] longs = new Long[5]; // ok


위 코드가 컴파일 에러 없이 아주 잘 작성된다.


자바의 제네릭은 배열과 다르게 기본적으로 무공변이지만 extends 예약어를 사용하면 공변 혹은 반공변으로 바꿀수도 있다.


List<? extends Number> integers = new ArrayList<Integer>(); // ok
List<? extends Number> doubles = new ArrayList<Double>(); // ok
List<? extends Number> longs = new ArrayList<Long>(); // ok


위 자바 코드는 무리없이 컴파일이된다.

하지만 한 가지 특징이 생긴다.

이렇게 공변으로 빚어낸 제네릭 컬렉션은 읽기전용(read-only)이 돼버린다는 것이다.


List<? extends Number> integers = new ArrayList<Integer>();

Number number = integers.get(1); // 읽기 - 정상
integers.add(1); // 삽입 - 컴파일 에러


image


컴파일 에러가 발생하는 부분의 인수 타입을 보면 capture of ? extends Number e 라는 문구를 볼 수 있는데, 이 의미는 자바 컴파일러가 ? extends Number e 타입에 대해 캡쳐한 어떤 타입이라는 의미이다.

하지만 결국 이 어떤 타입이라는 캡쳐 타입은 Number의 하위 타입이긴 하지만 정확히 뭔지는 알 수 없는 타입임을 의미한다.

따라서, 정확히 어떤 타입인지를 모르기 때문에 1, 1.0 등을 삽입하려 하면 컴파일 에러가 발생하게 된다.

실제 삽입하려는 타입이 Integer, Double, Long보다도 더 하위의 타입일수도 있기 때문이다.

결국 이렇게 정확히 어떤 타입인지를 알 수 없으니 null을 제외한 그 어떤 타입도 삽입을 하지 못하게 개발자가 강제적으로 막을 수 있게 된다.

반대로 해당 List에 들어있는 모든 원소들은 절대적으로 Number의 하위타입들이기 때문에 Number 타입으로 꺼내어 읽을수는 있는것이다.


반공변(contravariance)


반공변이라는 것은 타입 S타입 T의 하위타입일 경우 Box<S> Box<T>의 상위 타입이라는 것이다.

일단 자바 제네릭에서는 super 예약어를 사용해 무공변인 제네릭을 반공변으로 빚어낼 수 있다.

반공변은 무공변과 다르게 읽기에 제한이 생기며, 오로지 삽입만 원활하게 가능해진다.

일단 코드를 보자.


List<? super Number> numbers = new ArrayList<>();

numbers.add(1); // Integer 삽입 - 정상
numbers.add(1.0); // Double 삽입 - 정상
Number number = numbers.get(1); // 읽기 - 컴파일 에러
Object someElement = numbers.get(1); // 읽기 - 정상


image


이번엔 컴파일 에러가 난 부분의 반환타입(Provided)을 보면 capture of ? super Number 라는 문구를 볼 수 있는데, 이는 공변에서의 예와 같이 자바 컴파일러가 ? super Number에 대해 캡처한 어떤 타입이라는 의미이다.

즉, 반환 타입이 Number의 상위타입이긴 한데, 정확히 어떤 타입인지를 알수가 없는것이다.

하지만 ? super Number라는 것은 해당 List에 들어있는 모든 원소들은 최소한 Number 타입이라는 것이 절대적으로 보장되기 때문에 Number 타입을 만족한다면 삽입이 되며, 자바의 최상위 타입인 Object로 반환을 하게 한다면 어떻게든 꺼내어 읽을수는 있게 된다. (타입체크 및 타입 캐스팅이 필요해지긴 하지만 !)


정리


이제까지 변성이 무엇인지에 대해 대략적인 감을 잡았다.

그래서 언제 extends를 사용해야 하며, 언제 super를 사용해야 하는지에 대한 의문이 들 수 있는데, 📕 이펙티브 자바에서는 이를 PECS라는 용어로 표현하고 있다.

PECS생산자(Producer) - extends, 소비자(Consumer) - super 라는 의미인데 요 부분에서 또 혼란이 크게 왔다.

나는 List에 원소를 집어넣으면 생산자이고, List에서 원소를 꺼내가면 소비자라고 생각하고 코드를 작성했는데, 작성하고 보니 생각한것과 완전 반대로 동작하는 것이었다. 즉, 제대로 이해하지 못한것이었다.

이에 대해 찾아보니 PECS는 오로지 컬렉션 관점에서 생각해야만 하며, 외부에서 List의 원소를 가져갈 경우 컬렉션 관점에서는 자신이 보유하고 있던 원소가 사라진것이기 때문에 이를 생산한다고 표현하고, 외부에서 List에 원소를 삽입하는 경우 컬렉션 관점에서는 자신에게 원소가 하나 생긴것이기 때문에 이를 소비한다고 표현하는 듯 하다 (머리가 어질어질하다… 이게… 영어적인 사고…? 😩)

굉장히 헷갈리고 머리가 아픈데, 그냥 간단하게 생각해서 다른 개발자가 읽기만 안전하게 사용하도록 하고 싶다면 extends를 사용하고, 삽입만 안전하게 사용하도록 하고 싶다면 super를 사용하자고 생각하기로 하였다.


참고


© 2022. All rights reserved.