본문 바로가기
언어 공부/Java

[Java] 가변성 (feat. Generic)

by 희조당 2023. 7. 5.
728x90

🙋 들어가며

제네릭에 대해서 자세하게 공부를 하다가 가변성(variance)에 대해서 알게 되었습니다.

어떤 종류가 존재하는지 알아보고, 왜 이런 개념이 있는지 알아보겠습니다.
제네릭을 위해서 다루는 내용이므로 이해가 어렵다면 해당 글에서 예시를 통해 다뤄보도록 하겠습니다.


🔄 가변성이란?

이름에서 예상할 수 있듯이 가변성이란, 특정 타입의 객체를 다른 타입의 객체로 변환할 수 있음을 말합니다.
반대의 개념은 불변성으로, 무공변(Invariance)으로도 부릅니다.

 

여기서 가변성은 공변과 반공변 두가지가 존재합니다.
어떤 객체 a와 b에 대해서, a가 b로 변환할 수 있다고 가정하겠습니다.

그러면 다음과 같이 총 3가지로 가변성과 불변성을 구분할 수 있습니다.

1️⃣ 공변(covariant)

a가 b로 변환할 수 있다면, Box<a>를 Box<b>로 변환할 수 있다.

구체적인 방향으로 타입 변환을 허용해주는 것을 의미합니다. (자기 자신과 자식 객체 허용)

자바를 사용하신 분들이라면 무언가 떠오르는 것이 있을거라고 생각합니다.

Object[] array = new Long[SIZE]; // ok..!
List<Object> list = new ArrayList<String>(); // error..!

위의 예시처럼 배열은 공변이지만, 자바에서 제네릭은 무공변입니다.

이는 배열은 구체화 타입이지만 제네릭은 비구체화 타입이기 때문입니다.

여기서 소거(erasure)에 대한 개념이 나오는데, 간단하게 정리하고 다음 글에서 자세하게 다루겠습니다. 😋

  • 구체화 타입 : 런타임 시에도 자신의 타입 정보를 알고 지킨다.
  • 비구체화 타입 : 타입 소거자에 의해서 컴파일 시점에서 타입 정보가 사라진다.

2️⃣ 반공변(contravariant)

a가 b로 변환할 수 있다면, Box<b>를 Box<a>로 변환할 수 있다.

추상적인 방향으로 타입 변환을 허용해주는 것을 의미합니다. (자기 자신과 부모 객체 허용)

반공변에 대한 자바에서 가장 대표적인 예시는 Comparator 인터페이스가 있습니다.

public class Item {
    private final String name;
    private final int price;
    // 생성자, getter...
}

public class Laptop extends Item {
    // 생성자, getter...
}

public class Phone extends Item {
    // 생성자, getter...
}

 

물건의 가격을 기준으로 비교하기 위해서 compare()를 구현하고, 바로 비교해보겠습니다.

// 예시를 위해 익명 인터페이스로 구현
Comparator<Item> comparator = (o1, o2) -> o1.getPrice() - o2.getPrice();

Laptop macBook = new Laptop("MacBook", 3_250_000);
Phone iPhone13 = new Phone("iPhone13", 850_000);

int compared = comparator.compare(macBook, iPhone13);

// MacBook is expensive than iPhone13 : true!!
System.out.println(MessageFormat.format("{0} is expensive than {1} : {2}", macBook.getName(), iPhone13.getName(), compared > 0));

물건을 기준으로 구현했는데 서브 클래스인 노트북과 핸드폰 타입에도 잘 적용되고 있습니다.

이게 반공변에 대한 대표적인 예시입니다 👍

3️⃣ 무공변(Invariance)

a가 b로 변환할 수 있지만, Box<b>와 Box<a>는 서로 변환할 수 없다.

오로지 자기 타입만을 허용하는 것을 의미합니다.

앞서 언급했듯이 자바 제네릭은 무공변입니다. 이해를 위해서 다이어그램을 보겠습니다.

Object, Number, Integer는 하나의 상속관계지만 List는 전혀 그렇지 않습니다.

List<Number>는 오직 Number만을, List<Integer>는 오직 Integer만을 지원하며 어떤 연관관계도 없습니다.

 

만약 관계를 형성하고 싶으면 다음과 같이 사용할 수 있습니다.

class ItemList<E, V> implements List<E> {
    public void addItem(E e, V v);
}


🤔 왜 이런 개념이 존재할까?

우선 자바는 왜 제네릭은 무공변성을 선택했을까요? 그리고 왜 가변성이라는 개념이 존재할까요?

다양한 이유가 있지만 가장 중요한 이유는 바로 타입 안정성 때문입니다.

 

우선 제네릭을 무공변성으로 선택함으로 다음과 같은 이점이 존재합니다.

  • 일관성 : 타입이 명확해진다면 동작하는데 일관성을 보장합니다.
  • 신뢰성 : 의도치 않은 타입 변환을 방지해 신뢰성을 보장합니다.
  • 안정성 : 타입과 관련된 문제를 컴파일 시점에 잡아서 보다 안정된 프로그램을 보장할 수 있습니다.

이런 것들이 보장된다면 코드를 통해서 쉽게 행동을 예측할 수 있고 이해할 수 있습니다.

 

처음 제네릭을 접했을 때, 이것은 오히려 다형성을 제한하는 것이 아닐까? 라는 생각을 한 적이 있습니다.

무공변성을 통해서 의도와 다르게 사용할 수 있는 여지를 많이 제약했기 때문입니다.

지금은 공변과 반공변이라는 개념을 통해서 확장성까지 챙길 수 있다고 생각합니다.

 


-reference

https://wjdtn7823.tistory.com/88

 

😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋

 

 

댓글