1. Generic
제네릭 자료형이란 클래스에서 사용하는 변수의 자료형이 여러 개 일 수 있고, 그 기능은 동일한 경우 클래스의 자료형을 특정하지 않고 추후 해당 클래스를 사용할 때 지정할 수 있도록 선언하는 것이다. 실제로 사용되는 자료형의 변환은 컴파일러에 의해 검증되므로 안정적인 프로그래밍 방식이라 할 수 있다. 주로 컬렉션 프레임워크에서 많이 사용된다.
제네릭의 의의 첫 번째는 형 변환을 할 때 생기는 성능적인 이슈를 해결하는 것이다. 두 번째는 제네릭을 사용해 안정적이고 범용적인 API를 개발하기 위함이다.
다음은 제네릭 자료형을 사용하지 않은 예시이다.
public class Cage {
private Cat animal;
public void setAnimal(Cat animal) {
this.animal = animal;
}
public Cat getAnimal() {
return animal;
}
}
public class Cage2 {
private Dog animal;
public void setAnimal(Dog animal) {
this.animal = animal;
}
public Dog getAnimal() {
return animal;
}
}
// 여러 타입을 대체하기 위해 공통의 상위 인터페이스를 사용할 수 있다.
public class Cage3 {
private Animal animal;
public void setAnimal(Animal animal) {
this.animal = animal;
}
public Animal getAnimal() {
return animal;
}
}
하지만 Cage3 같은 경우에는 사용할 때 형 변환이 필요할 수 있다.
Cage3 cage = new Cage3();
Cat cat = new Cat();
cage.setAnimal(cat);
Cat returningCat = (Cat)cage.getAnimal();
이것을 다음과 같이 제네릭 클래스로 정의할 수 있다.
public class GnericCage<T> {
private T animal;
public void setAnimal(T animal) {
this.animal = animal;
}
public T getAnimal() {
return animal;
}
}
- 자료형 매개변수 T(type parameter) : 이 클래스를 사용하는 시점에 실제 사용할 자료형을 지정한다. static 변수는 사용할 수 없다.
- GenericCage : 제네릭 자료형
- E : element, K: key, V: value 등 여러 알파벳을 의미에 따라 사용 가능하다.
public class GenericTest {
public static void main(String[] args){
GenericCage<Cat> catCage = new GenericCage<Cat>();
catCage.setAnimal(new Cat());
System.out.println(catCage);
GenericCage<Dog> dogCage = new GenericCage<Dog>();
dogCage.setAnimal(new Dog());
System.out.println(dogCage);
}
}
꺽쇠를 다이아몬드 연산자라고 한다. 자바 7부터는 다음과 같이 생략도 가능하다.
//컬렉션 프레임워크에서 list는 LinkedList의 상위 인터페이스
List<Integer> list = new LinkedList<>();
제네릭 클래스를 다루는 법을 다뤘으니. 이제 제네릭 메서드를 다루는 법에 대해서 이야기해보겠다. 제네릭 메서드는 자료형 매개변수를 메서드의 매개변수나 반환 값으로 가지는 메서드를 의미한다. 자료형 매개변수가 하나 이상인 경우도 있다. 이때 제네릭 클래스가 아니어도 내부에 제네릭 메서드를 구현해서 사용할 수 있다.
우테코 프리코스 사전과제 중에 유효성 검사 로직을 구현해야 했는데, 이때 제네릭 메서드를 이용했다. 조금 불편했던 것은 타입스크립트는 instanceof에서 만약에 String이 확실하다면 타입 추론이 이루어져 형 변환(단언)을 하지 않아도 String 타입의 메서드를 사용할 수 있었는데 자바에서는 따로 지원해주지 않는다는 것이다.
private static <T> boolean validation(String type, T target){
if(type.equals("user") && target instanceof String) {
int userLength = ((String) target).length();
return 0 < userLength && userLength < 31;
}
else if(type.equals("userId") && target instanceof String) {
int idLength = ((String) target).length();
if(!((String) target).matches("^[a-z]*$")) return false;
return 0 < idLength && idLength < 31;
}
else if(type.equals("visitors") && target instanceof List) {
int visitorsLength = ((List<?>) target).size();
return visitorsLength < 10001;
}
else if(type.equals("edges") && target instanceof List) {
int edgesLength = ((List<?>) target).size();
return edgesLength == 2;
}
return false;
}
제네릭 메서드는 다음과 같은 형식으로 정리할 수 있다.
선언시 => 접근제어자 <타입 매개 변수> 반환타입 메서드이름(매개변수){}
사용시 => 클래스.<타입 인자>메서드이름(인자)
변성에 대한 이야기
나는 주로 자바스크립트나 타입스크립트로 개발을 해왔기 때문에 자바 생태계의 타입 시스템은 아직 익숙하지 않다. 앞으로 우테코 프리코스를 하면서 최대한 많은 것을 배우기 위해선 이러한 타입 시스템 속에서 수영하는 방법을 깨닫고 싶었다. 실제로 나는 코딩하거나 모르는 것을 사용할 때, 공식문서보단 인터페이스를 본다. 이러한 인터페이스를 좀 더 잘 사용하기 위해선 제네릭에 대해서 깊은 이해를 하고 있어야 한다고 생각했다. 그래서 더욱 깊은 이해를 위해서 그동안 미루어 왔던 변성을 배워볼까 했다.
변성(Variance)은 타입 계층 관계에서 서로 다른 타입 간에 어떤 관계가 있는지를 나타내는 개념이다. 기저타입(Base Type)이 같고, 타입 인자(Type arugument)가 다를 때, 서로 어떤 관계가 있는지를 나타내는 것이라 볼 수 있다. 변성을 제대로 이해하기 위해서는 타입 T가 S의 하위 타입일 경우, C <T>가 C <S>의 하위 타입인가?라는 질문으로 시작하면 좋다.
List<String>에서 List는 기저타입이고 String은 타입인자이다.
무공변(invariance)
타입 T가 S의 하위 타입일 경우, C<S>와 C<T> 사이에 상속 관계가 없는 것을 의미한다. 자바는 어떨까? Object의 하위 타입 Integer가 있다. 만약에 List<Object>와 List<Integer>이 존재한다면 List<Integer>은 List<Object>의 하위 타입이 성립하는 가를 생각해보면 아니다. 즉 자바의 제네릭은 기본적으로 무공변(invariance)이다.
PECS 공식으로 공변(convariance)과 반공변(contravariance) 접근하기
제네릭, 그리고 변성(Variance)에 대한 고찰 (1) 글을 기반으로 작성했습니다.
타입 T가 S의 하위 타입일 경우, C<T>는 C<S>의 하위 타입임이 성립된다면 이를 공변(convariance)이라고 한다. 반면 타입 T가 S의 하위 타입일 경우, C<S>는 C<T>의 하위 타입임이 성립된다면 이를 반공변(contravariance)이라고 한다.
자바는 기본적으로 무공변이다. 자바에서 제네릭의 변성을 이용하기 위해서 <? extends T>, <? super T> 키워드를 사용할 수 있다. 이를 설명하는 공식이 PECS 이다.
- 어떤 메서드가 입력 파라미터로 제네릭을 적용한 컨테이너를 받고, 메서드 안에서 해당 컨테이너가 생산하는 작업을 하는 경우에는 ? extends T 타입 파라미터를 사용한다. (공변)
- 어떤 메서드가 입력 파라미터로 제네릭을 적용한 컨테이너를 받고, 메서드 안에서 해당 컨테이너가 소비하는 작업을 하는 경우 ? super T 타입 파라미터를 사용한다. (반공변)
Producers Extends, 공변
// stack api에서 다음과 같은 pushAll이 존재한다고 가정
public void pushAll(Collection<T> src){ //collection 컨테이너에서 생산 작업을 하므로
for(T elem : src){ //src는 producer다. (elem을 생성 중)
push(elem);
}
}
위와 같은 pushAll 메서드가 존재했을 때 다음과 같이 사용하면 에러가 발생한다.
Stack<Number> stack = new Stack<>();
Collection<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
stack.pushAll(integers);
기본적으로 자바는 무변공이기 때문에 생산자인 Collection<Number> src가 Collection<Integer>인 integers를 받지 못한다. 하지만 다음과 같이 한정적 와일드카드 타입이라는 특수한 타입 파라미터를 적용하면 공변을 성립할 수 있게된다.
// stack api에서 다음과 같은 pushAll이 존재한다고 가정
public void pushAll(Collection<? extends T> src){ //collection 컨테이너에서 생산 작업을 하므로
for(T elem : src){ //src는 producer다. (elem을 생성 중)
push(elem);
}
}
위와 같이 시그니처를 작성하면 Collection<Number>는 Collection<Integer>의 상위 타입이 될 수 있다. 즉, 공변을 성립할 수 있다.
Consumers Super, 반공변
//stack api에 다음과 같은 popAll 메서드가 있다고 가정
public void popAll(Collection<T> dst) { //collection 컨테이너에서 소비 작업을 하므로
while(!isEmpty()){
dst.app(pop()); // dst는 consumer이다. (stack 요소를 소비중)
}
}
위와 같은 popAll 메서드가 존재했을 때 다음과 같이 사용하면 에러가 발생한다.
Stack<Number> stack = new Stack<>();
Collection<Object> objects = ...;
stack.popAll(objects);
기본적으로 자바는 무변공이기 때문에 소비자인 Collection<Number> dst가 Collection<Object>인 objects를 받지 못한다. 정확히는 Object 타입이 Number의 상위 타입이기 때문이다. 하지만 다음과 같이 한정적 와일드카드 타입이라는 특수한 타입 파라미터를 적용하면 반공변을 성립할 수 있게 된다.
//stack api에 다음과 같은 popAll 메서드가 있다고 가정
public void popAll(Collection<? super T> dst) { //collection 컨테이너에서 소비 작업을 하므로
while(!isEmpty()){
dst.app(pop()); // dst는 consumer이다. (stack 요소를 소비중)
}
}
위와 같이 시그니처를 작성하면 Collection<Number>는 Collection<Object>의 상위 타입처럼 될 수 있기 때문에 Collection<Number>가 기대되는 자리에 Collection<Object>가 들어갈 수 있다. 즉, 반공변을 성립할 수 있다.
effective java의 저자 조슈아 블로흐는 PECS 공식을 잘 사용한다면, 한정적 와일드카드 타입을 사용한 제네릭으로 타입 안전하고 훨씬 더 유연한 API를 만들 수 있다고 말했는데 직접 경험해볼 만한 상황이 아직은 없는 듯하다.
객체지향 원칙 중 LSP에 해당되는 이야기이므로 LSP도 다뤄볼까도 했지만, 자바를 배우는 과정에서는 많이 벗어나는 것 같아서 다음에 SOLID를 제대로 다룰 때 더욱 자세히 변성에 대한 정리를 해보고 싶다. 아래는 변성을 공부하면서 끄적였던 LSP 정리인데 혹시 몰라 올려본다.
'개발(레거시) > 자바' 카테고리의 다른 글
String, String Buffer, String Builder (0) | 2022.11.12 |
---|---|
DataTypes, Type conversion, Type of variables (0) | 2022.10.30 |
Basic Syntax, DataStructures (0) | 2022.10.30 |