Learn business/Java

[3편] 제네릭이란?


제한(상한, 하한)된 와일드 카드의 개념에 대한 내용이다. 왜 제한된 와일드 카드를 쓰는지 이해하고, 이를 통해 논리적인 오류까지 잡아낼 수 있는 자바의 힘에 대해 느낄 수 있다.

 

와일드 카드를 제한하는 방법은 크게 두 가지가 있다. 상한 제한(Upper-Bounded)과 하한 제한(Lower-Bounded)이다. 그 내용에 대해서 확실하게 이해하는게 목표이다. 참고로 제네렉은 제한하는 방법이 ‘extends’ 하나 이고, 와일드 카드는 제한하는 방법이 ‘extends’와 ‘super’이다.

 

wildcard의 상한 제한(Upper-Bounded)

위 메소드의 파라미터를 보면, 이렇게 설명할 수 있을 것 같다. box 파라미터 변수는 Box 타입의 인스턴스의 참조 값을 전달받는 매개변수이다. 그런데 제네릭 타입이 와일드 카드이므로 어떠한 타입이 오든 상관은 없다. 다만, extends 라는 예약어를 통해 제한을 걸었다. Number 이거나 Number를 상속하는 클래스만 와라! 상한 제한을 한 것이다. 왜냐하면 Number 클래스 위에 있는 Object 클래스는 올 수 없기 때문이다.

 

wildcard의 하한 제한(Lower-Bounded)

(위 설명과 비슷한 구조로 비교해 보겠음) 위 메소드의 파라미터를 보면, 이렇게 설명할 수 있을 것 같다. box 파라미터 변수는 Box 타입의 인스턴스의 참조 값을 전달받는 매개변수이다. 그런데 제네릭 타입이 와일드 카드이므로 어떠한 타입이 오든 상관은 없다. 다만, super 라는 예약어를 통해 하한을 걸었다. Number 이거나 Number가 상속하는 클래스만 와라! 하한 제한을 한 것이다. 왜냐하면 Number 클래스 아래 있는 Integer 클래스는 올 수 없기 때문이다.

 

그림으로 정리

제네릭 하한 상한 그림으로 설명

 

wildcard를 제한하는 이유를 알기 위한 도입부

현업에 있다보면 수 많은 ctrl + c/v 기법으로 컴파일 에러는 발견되지 않는데 논리적인 오류를 야기하는 예기치 못한 버그들이 발견되곤 한다. 아래 코드를 보자.

BoxHanlder 클래스에 주석을 달아 놓았 듯이 박스에 Toy를 꺼내고 넣는 단순한 작업이 있는 클래스이다. 그런데 만약 밤을 샌 개발자가 복붙을 잘못하여 아래 코드와 같이 outBox 메소드에 “box.set()”을 inBox 메소드에 “box.get()”으로 논리적인 오류를 범했다면 어떨까?

분명 outBox 메소드에 set을 하는 것은 논리적인 오류이고 넣는 inBox 메소드에 get을 하는 것은 논리적인 오류이다. 하지만 좋은 프로그래밍 언어는 개발자의 의도에서 벗어난 것에 대해서 컴파일 오류를 발생시켜주는 것이 좋은 언어이다. 그러한 문법적 장치가 있는 것은 좋은 언어의 조건 중 하나이다. 그럼 위 예시에서 어떻게 컴파일 오류를 낼 수 있을까? 지금부터 그 장치에 대해 이해해 보도록 하자. (뜬금 없겠지만, wildcard의 제한이 문법적 장치이다.)

 

wildcard의 상한 제한의 목적!

기본적으로 ‘extends’ 예약어는 위에서 설명한 바와 같이상한 제한(Number 클래스를 상속받은 애들만 와!)의 목적이 있다. 그런데 그것 외에도 전혀 예측하지 못한 이유로도 상한 제한을 하는 경우가 있다. 아래 예시를 보자.

상한 제한(‘extends Toy’)을 추가한 순간 box 인스턴스에서 꺼내는 것은 가능하고 box 인스턴스에 넣는 것을 불가능하다. 위 코드를 돌려보면 box.set(new Toy())에서 컴파일 에러가 발생하는 것을 알 수 있다. 그냥 상한 제한 하나만 추가한 건데 왜 box 인스턴스에 set을 하는 것에 컴파일 에러가 발생하는 것일까? 왜 그런걸까?

 

outBox 메소드의 파라미터를 보도록 하자. ‘Box<? extends Toy>’ 는 Toy 클래스이거나 Toy를 상속받는 Car 클래스와 Robot 클래스가 올 수 있다. 즉, Box<Toy>, Box<Car>, Box<Robot> 인스턴스가 매개변수로 전달될 수 있다. 컴파일러는 상속 구조를 전부 기록하고 있지 않다. Car가 Toy를 상속받고 있고 Robot이 Toy를 상속받고 있다는 것을 어디 메모리에 저장해두고 있지 않다는 것이다. 이런 상황에서 컴파일러는 걱정한다. new Toy()를 매개변수를 set 메소드를 호출하려고 하는데 Box<Car> 인스턴스나 Box<Robot> 인스턴스가 오면 어떡하지?

 

위 문장을 보고 이해가 안될 수 있다. 좀 더 세세하게 집고 넘어가면 만약 outBox 메소드에 Box<Car> 타입의 인스턴스가 왔다고 하자. 그럼 아래와 같이 Box<T> 타입에서 T는 Car 클래스로 바뀔 것 이다.

그럼 위와 같이 생성된 인스턴스에서 ‘box.set(new Toy())’ 를 할 수 있을까? Car는 Toy를 상속받는 클래스이기 때문에 Car를 인자로 전달받을 수 있도록 생성된 메소드에 부모 클래스를 인스턴스를 인자로 전달할 수 없다. 이로 인해 컴파일 에러가 발생하는 것이다. 이 문장으로 이해가 되었을 것 같다. (안됐다면 제네릭 상속 공부를...ㅜㅜ)

그렇다며 최상위 부모인 Box<Toy> 인스턴스가 온다면 어떨까? 아쉽게도 컴파일 에러를 발생한다. 위에서도 말했지만 컴파일러는 메모리 공간에 상속구조 관계를 하나하나 저장해두지 않는다. 즉 Toy가 최상위 부모인지 알 수 없다는 것이다.

 

정리를 하자면, Box<? extends Toy> box 가 보인다면, 인자로 전달되는 box 매개변수를 대상으로 넣는 것이 불가능하다! 꺼내는 것만 돼!라고 외우자!

 

wildcard의 하한 제한의 목적!

하한 제한(‘super Toy’)을 추가하는 순간 box 인스턴스에 넣는 것은 가능하고 box 인스턴스에 꺼내는 것은 불가능하다. 위 코드를 돌려보면 box.get()을 하여서 t 변수에 넣어주려는 행위를 할 때 컴파일 오류가 발생한다. 결론은 상한 제한과 마찬가지로 상속과 관련된 이유 때문에 컴파일 오류가 발생한다.

 

inBox 메소드의 파라미터를 보도록 하자. ‘Box<? super Toy>’는 Toy 클래스이거나 Toy가 상속하는 Plastic 클래스가 올 수 있다. 즉, Box<Toy>, Box<Plastic>, Box<Object> 인스턴가 매개변수로 올 수 있다. 이런 상황에서 컴파일러는 걱정한다. Toy t 매개변수에 box.get() 메서드를 호출하여 넣어주려는데 Box<Plastic> 인스턴스나 Box<Object>가 오면 어떡하지?

 

마찬가지로 위 문장을 보고 이해가 안될 수 있다. 좀 더 세세하게 집고 넘어가면 만약 inBox 메소드에 Box<Plastic> 타입의 인스턴스가 왔다고 하자. 그럼 아래와 같이 Box<T> 타입에서 T는 Plastic 클래스로 바뀔 것 이다.

그럼 위와 같이 생성된 인스턴스에서 ‘Toy t = box.get()’ 문장을 실행할 수 있을까? Toy의 부모 클래스가 Plastic이기 때문에 box.get()을 하여 Plastic으로 나올 경우 Toy클래스에 초기화해줄 수 없다. 정리를 하자면, Box<? super Toy> box 가 보인다면, 인자로 전달되는 box 매개변수를 대상으로 꺼내는 것이 불가능하다! 넣는 것만 돼! 라고 외우자!

 

wildcard 상한, 하한 제한 난이도를 높여보자.

위 내용을 이해한 것을 바탕으로, BoxHandler를 제네릭 클래스 타입으로 만들어서 이해해보자.

아마 단 번에 에러가 나는 부분을 찾은 사람도 있을 것 같지만, 혹시 나와 같이 한번에 찾지 못한 사람이 있을까봐 에러가 나는 부분을 주석으로 코멘트를 달았다. 왜 에러가 발생할까? 개념은 똑같다. 다만, 제네릭 클래스로 바뀌면서 헷갈릴 뿐이다. 다시 차근차근 확인해보자.

 

BoxHandler<Toy> toyBoxHandler = new BoxHandler<>();

이렇게 toyBoxHandler 변수를 만들어보자. 그러면 BoxHandler는 아래와 같은 모양으로 인스턴스가 생성될 것이다.

그렇다면 위와 같은 상태에서 아래와 같이 carBox 인스턴스를 생성해서 outBox 메소드를 호출하면 어떻게 될지 생각해보자.

Box<Car> carBox = new Box<>();

toyBoxHandler.outBox(carBox, new Toy()); 

 

Toy temp = box.get(); 이 실행문은 금방 이해가 될 것이다. box.get()를 호출한 결과 Car 타입이 나올 것이고 상속관계에 의해서 납득이 되는 문장이다. 그럼 box.set(t); 이 실행문은 어떻게 될까? 위에 Box<Car> 타입 인스턴스 생성에 의해서 Box 제네릭 클래스는 아래와 같은 모양이다.

그런데 저 Car 타입을 매개변수로 받는 set 메소드에 Toy 객체를 넣을 수 있을까? 상속구조에 의해서 당연히 불가능하다. 그렇다면 toyBoxHandler.outBox(carBox, new Toy());  이렇게 호출하지 않고 toyBoxHandler.outBox(carBox, new Car()); 로 호출하면 되지 않나? 라고 생각할 수 있지만 위에서 말했듯이 메모리에 모든 상속구조를 담고 있지 않기 때문에 컴파일러는 에러를 발생하는 것이다.

 

자, 그럼 반대의 경우를 보자. 똑같이 BoxHandler<Toy> toyBoxHandler = new BoxHandler<>(); 로 인스턴스를 생성했다. 그리고 plasticBox 인스턴스를 생성해서 inBox 메소드를 호출하면 어떻게 될지 생각해보자.

Box<Plastic> plasticBox = new Box<>();

toyBoxHandler.inBox(plasticBox, new Plastic()); 

 

Toy temp = box.get(); 실행문은 왜 에러가 발생할까? 이제 좀 이해가 확 되지 않을까 싶다. box.get을 하면 당연히 Plastic 타입이 나올 것이고 상속관계에 의해서 에러가 발생할 것이라는 것을 알 수 있다. 그렇다면 왜 box.set(t); 이 실행문은 정상적으로 동작하는 것일까? 마찬가지로 Box<Plastic> 타입 인스턴스 생성에 의해서 Box 제네릭 클래스는 아래와 같은 모양이다.

아마 바로 이해할 수 있을 것이다. 상속구조에 의해서 Plastic 타입에 Toy 타입을 넣는 것은 당연히 가능하다. 

extends : 꺼내는 것만 가능해

super : 넣는 것만 가능해 

 

기억해두자.

 

wildcard 상한, 하한 제한 제대로 이해했니?

위 코드에서 1)번과 2)번 어떤 것이 맞을까? super과 extends의 성격을 이해했다면 1)번이 정답인 것을 알 수 있다. 다시 한번 말하지만 extends는 꺼내는 것만 되고 super는 넣는 것만 된다.

 

이펙티브 자바를 하다가 내용이 너무 산으로 간 것 같은데 위 내용을 정확하게 숙지해야 이펙티브 자바에 나오는 “[ITEM 31] 한정적 와일드카드를 이용해 API 유연성을 높여라” 를 이해할 수 있다. 내용이 길어진 것 같아서 다음 포스팅으로 이어가자. 아마 다음 포스팅은 제네릭과 와일드 카드가 합쳐졌을때 갖는 확장성에 대해서 포스팅을 할 예정이다.

'Learn business > Java' 카테고리의 다른 글

자바 클래스패스  (1) 2020.03.31
[5편] 제네릭이란?  (3) 2020.01.05
[4편] 제네릭이란?  (1) 2020.01.05
[2편] 제네릭이란?  (0) 2019.11.02
[1편] 제네릭이란?  (0) 2019.10.31