Generic Variance 제네릭 가변성

Gongmeda·2023년 1월 15일
1
post-thumbnail

최근에 진행했던 프로젝트에서 레거시 자바 프로젝트의 DB를 건들이지 않고 로직을 짜야했습니다.

레거시 DB의 구조 중 완벽히 동일한 형태를 갖고 있지만 로직상의 파라미터에 따라 서로 다른 테이블을 선택해서 사용하는 구조가 있었는데요, 이를 JDBC 기반의 하드코딩으로 처리했기 때문에 코드에는 중복이 어마무시하게 많았습니다.

그래서 저는 JPA를 활용해서 엔티티 클래스 기반으로 로직을 처리하되, 동일한 구조의 여러 테이블을 모두 별도의 엔티티로 만들지 않고 상속을 통해 코드 중복을 줄였습니다.

이때, 상속 구조 때문에 제네릭 컬렉션을 사용하는 경우가 많았는데 와일드카드, super , extends 등의 문법이 정확히 이해가 안된 상태에서 작업을 하다보니 난관이 많았습니다.

그래서 이번 글을 통해 제네릭의 가변성과 이와 관련된 내용들을 정리해보겠습니다.

제네릭

List<String> names = new ArrayList<>();
names.add("gongmeda");
names.add(1); // 컴파일 에러

제네릭은 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법입니다.

사용되는 이유는 다음과 같습니다.

  1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있음
  2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있음

즉, 코드의 안정성재사용성이 높아진다는 것 입니다.

제네릭과 하위 타입

public class Tiger extends Animal {}
public class Cage<T> {

    private List<T> animals = new ArrayList<>();

    public void push(T animal) {
        this.animals.add(animal);
    }

    public List<T> getAll() {
        return animals;
    }
}

위와 같이 Animal 클래스를 상속받는 Tiger 클래스와 제네릭 클래스 Cage 를 만들었습니다.

Animal a = new Tiger(); // OK
Cage<Animal> ca = new Cage<Tiger>(); // 컴파일 에러

위 코드에서 TigerAnimal 의 하위 타입이기 때문에 Cage<Tiger> 또한 Cage<Animal> 의 하위 타입이 될 수 있을 것 같지만 그렇지 않습니다.

이는 '만약 하위 타입이 맞다면?' 을 가정해보면 알 수 있습니다.

public class Lion extends Animal {}
Cage<Animal> ca = new Cage<Tiger>(); // 하위 타입이므로 가능

ca.push(new Lion()); // Lion은 Animal이므로 가능

List<Tiger> tigers = ct.getAll(); // Lion 리스트 반환 (?)

위와 같이 가정이 맞다고 하고 코드를 짰을 때 말도 안되는 경우가 발생하는 것을 알 수 있습니다.

따라서 이런 경우는 AnimalTiger 의 상위 타입이지만, Cage<Animal>Cage<Tiger> 상위 타입이 아닌 경우로 무변성(invariant)이라고 합니다.

가변성 (Variance)

가변성은 타입 파라미터가 클래스 계층에 어떤 영향을 미치는지를 나타냅니다.

가변성에는 3가지 종류가 있습니다.

종류의미
공변성(covariant)T’T 의 서브타입이면, C<T’>C<T> 의 서브타입이다.
반공변성(contravariant)T’T 의 서브타입이면, C<T>C<T’> 의 서브타입이다.
무변성(invariant)C<T>C<T’> 는 아무 관계가 없다.

위에서 AnimalTiger 의 관계에서 본 것과 같이 자바는 기본적으로 무변성입니다.

근데 무변성으로 유지한채로 개발을 하다보면 문제가 발생할 수 있습니다.

public class Animal {}
public class Carnivore extends Animal {}
public class Tiger extends Carnivore {}
public class Lion extends Carnivore {}
public class ZooKeeper {
    public void giveMeat(Cage<Carnivore> cage, Meat meat) {
        // ...
    }
}
ZooKeeper zk = new ZooKeeper();
Cage<Tiger> ct = new Cage<>();
Meat m = new Meat();

zk.giveMeat(ct, m); // 컴파일 에러

위 코드는 육식동물 우리에 고기를 먹이로 주는 사육사를 표현한 클래스 구조입니다.

에러는 giveMeat 메소드가 Cage<Tiger> 타입을 받을 수 없다고 나오는데요, 이는 Cage<Carnivore> 는 무공변이기 때문에 생기는 문제입니다.

위 코드의 원래 의도는 CarnivoreTiger 의 상위 타입이면 Cage<Carnivore>Cage<Tiger> 의 상위 타입인 공변성(covariant)이기 때문에 무변성을 공변성으로 바꿔줘야 합니다.

? extends T

public class ZooKeeper {
    public void giveMeat(Cage<? extends Carnivore> cage, Meat meat) {
        // ...
    }
}
ZooKeeper zk = new ZooKeeper();
Meat m = new Meat();

Cage<Tiger> ct = new Cage<>();
zk.giveMeat(ct, m);

Cage<Lion> cl = new Cage<>();
zk.giveMeat(cl, m);

Cage 의 타입을 ? extends Carnivore 로 지정해서 공변으로 바꿨습니다.

이제 Cage<? extends Carnivore> 타입에 Cage<Tiger> 또는 Cage<Lion> 을 할당 할 수 있게 되었습니다. 즉, Cage<? extends Carnivore>Cage<Tiger>Cage<Lion> 의 상위 타입이 된 것이죠.

위와 같이 공변성은 ? extends 문법으로 표현할 수 있습니다.

하지만 개발 중 또 다른 문제가 생깁니다.

? super T

Cage<? extends Carnivore> cage = new Cage<>();
cage.push(new Tiger()); // 컴파일 에러

위 코드는 왜 에러가 날까요?

잘 이해가 안됩니다. 'cage 에는 Carnivore 의 하위 타입인 객체가 들어갈 수 있는거 아닌가?' 라고 생각하실 수 있지만 이는 올바른 에러입니다.

? extends CarnivoreCarnivore , Tiger , Lion 세 가지 중 하나가 될 수 있습니다. 따라서 cage.push(new Tiger()) 코드가 실행될 때 cage 가 실제로는 Cage<Tiger> 또는 Cage<Carnivore> 타입이라면 이는 올바른 동작이지만 Cage<Lion> 이라면 이는 올바르지 않습니다. 즉, 불확정성이 생긴다는 것이죠.

이때는 ? super 문법을 사용하여 해결할 수 있습니다.

? super CarnivoreCarnivore 와 그 부모인 Animal 가 될 수 있습니다. 따라서, cage.push(new Tiger()) 코드가 실행될 때 cage 는 실제로 Cage<Carnivore> 또는 Cage<Animal> 타입이 될 수 있고 둘 다 Tiger 타입 객체를 파라미터로 받을 수 있으므로 오류가 나지 않게 됩니다.

즉, Cage<? super Carnivore>Cage<Carnivore>Cage<Animal> 의 상위 타입이 된 것입니다. 이는 'CarnivoreAnimal 의 하위 타입일 때, Cage<Animal>Cage<? super Carnivore> 의 하위 타입이다'라는 반공변성의 정의를 따르게 됩니다.

위와 같이 반공변성은 ? super 문법으로 표현할 수 있습니다.

참고

profile
백엔드 깎는 장인

0개의 댓글