본문 바로가기
카테고리 없음

제네릭에 대해

by 찐세 2021. 4. 30.

제네릭에 대해서는 자바의 정석을 공부하면서 한번 쭉 정리를 한적이 있었다.


처음부터 바로 이해하기는 어려운 개념이었고, 매번 볼때마다 새롭게 이해가 되는 부분이 생긴다.

 

이번에도 테코톡을 보고나서 다시 새롭게 알게된 개념들에 대해서도 정리를 해야겠다고 생각했다.

 

10분 테코톡_제네릭

제네릭의 정의는 예전에 정리한 것과 동일하다.

 

제네릭은 다양한 타입의 객체를 다뤄야하는 클래스나 메서드에서 컴파일시에 타입체크를 해주는 기능이다. 이를 통해서 타입 안정성을 보장할 수 있고, 불필요한 형변환을 줄일 수 있다.

 

다음은 테코톡에서 설명해준 제네릭의 정의다.

 

제네릭은 클래스나 메소드에서 사용할 내부 테이터 타입을 외부에서 지정하는 기법이다.

 

어느정도 제네릭에 대해서 학습하고 난 뒤라서 둘 다 이해가 되는 부분이다.

 

그렇다면 제네릭의 정의에서 생각해봐야하는 부분이 있다.

 

  1. 컴파일시에 타입체크를 한다
  2. 이를 통해 타입 안정성을 제공한다.
  3. 불필요한 형변환을 줄일 수 있다.

이것이 제네릭의 특징이고, 등장의 이유라면 제네릭이 등장하기 이전에는 이러한 부분이 제공되지 않았다는 것이다.

 

우선 지네릭을 사용하지 않으면, 타입에 대해서는 컴파일시에 체크를 하지 않는다.

서로 상속관계에 있지 않은 타입끼리 캐스팅을 적용해도 컴파일러는 알지 못하고 런타임에 에러가 발생한다.

 

즉, 제네릭을 사용하면 이러한 타입 체킹이 컴파일 타임에 가능하다는 것이고, 인텔리제이등의 IDE에서 미리 에러를 알려줄 수 있다.

 

그리고 제네릭을 통해 타입에 대한 정보를 컴파일러에게 주기 때문에, 우리는 별도의 형변환을 해주지 않아도 된다.

 

제네릭이 처음 이해하는데는 많은 시간이 들지만, 정말 획기적인 기능이라는 것을 알 수 있었다.

 

 

 

왜 제네릭을 사용할까? 제네릭을 사용하는 이유!


비제네릭 클래스

 

제네릭 클래스

 

 

제네릭을 사용하는 이유 1. 타입 체킹

 

비제네릭클래스의 경우

이처럼 제네릭을 사용하지 않으면 컴파일 타임에 타입에 대한 체크가 이루어지지 않는다. 럼타임에 에러 발생

 

 

제네릭클래스의 경우

제네릭 클래스를 사용하는 경우 이처럼 컴파일 에러를 통해 타입이 잘못되었음을 알 수 있다.

 

 

제네릭을 사용하는 이유 2. 불필요한 형변환의 제거

 

위의 코드에서도 알 수 있듯이 제네릭 클래스를 사용하게 되면, Object 클래스를 사용해서 형변환을 해줘야하는 수고로움이 없다.

 

이처럼 제네릭을 사용하게 되면 여러타입의 객체를 사용하기 위해서 Object 클래스를 사용하지 않고 그때그때 타입에 대한 정보를 컴파일러에게 알려주기 때문에 타입 체킹이 가능하고, 형변환의 수고로움이 없어지는 것이다. 

 

 

 

제네릭 메서드


제네릭 메서드도 동일하게 메서드 내부에서 사용되는 지역변수나 매개변수 등 각종 타입을 외부에서 제공받고, 결국 여러 타입의 객체를 다루기 위해서 사용한다.

 

제네릭 메서드는 제네릭 클래스 내부 또는 비제네릭 클래스 내부 두 군데에서 모두 선언될 수 있다.

 

 그리고 제네릭 메서드의 타입 매개변수와 제네릭 클래스의 타입 매개변수가 동일하다면, 메서드 내부에서는 제네릭 메서드의 타입 매개변수가 우선시 된다.

 

 

 

제네릭의 제한


컴파일 에러가 발생하는 이유에 대해서 알아보자. 

이유는 간단하지만 그래도 한번 정리하고 넘어가면 좋을 듯해서 적어보려고한다. 

 

carList에는 Car 타입의 요소만 넣을 수 있지만, add 하려는 car의 타입은 T 타입으로 이는 외부에서 선언해줘야하는 값이다.

 

따라서 외부에서 Car 클래스를 비롯한 하위 클래스로 선언할 수도 있지만, 그렇지 않은 경우에는 문법을 위배하기 때문에 컴파일러가 이를 막는 것이다.  

 

즉, 제네릭을 사용하면 컴파일러가 모든 경우의 수에 대해 타입 안정성을 100%로 유지하려고 한다.  

 

나는 테코톡을 보고나서 얻은 이 관점으로 제네릭에 접근하는데 훨씬 수월해졌다. 

 

제네릭은 쫄보라고 생각하자.(..?ㅋㅋㅋㅋㅋㅋ)  제네릭은 하나라도 잘못될 여지가 있는 것은 절대 허용하지 않는다. 

하지만 가능한 범위 안에서는 최대의 유연성을 제공한다.

 

따라서 이렇게 타입 매개변수를 Car 타입의 하위 클래스로 제한하면, 어떤 수를 쓰더라고 T 타입에는 Car 타입 또는 그 하위 타입이 오기 때문에 add를 허용한다.

 

 

 

 

와일드 카드


내가 처음 제네릭을 접했을 때, 와일드 카드에서 터졌다. 

 

하지만 반복해서 보고, 또 보면서 점점 친해지고 있는 중이다..

 

이 또한 제네릭이 타입 안정성을 유지한다는 것을 생각하면 조금 수월했다.

 

그리고 처음에 와일드 카드를 언제 사용할 수 있는지 헷갈렸었다.

지네릭 클래스를 사용할 때, 특정 타입 파라미터를 지정해줘야하는데, 이때 와일드 카드를 사용할 수 있다. 

 

예를 통해서 와일드카드를 왜 사용하는 지 한번 알아보자.

 

이처럼 타입 파라미터에 대해서는 다형성이 적용되지 않는 것을 알 수 있다. 

 

List<Object>ArrayList<Object> 는 서로 상속관계이지만,  List<Object> 와  ArrayList<String> 은 상속관계가 아니다.

 

하지만 와일드 카드를 통해서는 이를 해결할 수 있다.  

타입 파라미터에 대해서도 다형성을 제공한다고 생각하면 될 것같다.  

 

List<A>List<?> 의 서브 타입으로, 이처럼 타입 매개변수에 대해서도 다형성을 적용시킬 수 있다.  

즉, 이렇게 와일드 카드를 사용하면 지네릭 클래스 간의 상속관계(타입 파라미터가 다른 경우)도 명확하게 정의할 수 있다.

 

 

와일드 카드의 특징

 

제네릭은 타입 안정성을 보장한다고 했다. 

 

어느 하나라도 타입 안정성을 위배할 수 있다면, 이는 허용하지 않는다.

 

 

? 는 타입 파라미터로 어떠한 타입도 올 수 있다는 뜻이다. 

 

그 말은 아직 정확한 타입이 정해지지 않았다는 의미이기도 하다.

 

그렇기 때문에 ?  와일드 카드로 지정되는 타입은 capture of ? 라고 표시가 된다.

 

그래서 이런 타입은 특징을 가지게 된다. 

 

해당 타입의 요소를 읽어오기 위해서, 아직 ?에 대해서 타입이 정해지지 않았기 때문에 Object 타입으로 받아오게 된다.

 

그리고 해당 타입의 요소를 입력하려고 할 때는 아직 정확한 타입이 결정되지 않았기 때문에 null을 제외하고는 삽입이 불가능하다.

정확히 말하자면 List<?> 타입으로 List<Object> List<String> List<Integer> ... 등의 다양한 타입이 올 수 있기 때문에 

어떤 특정한 타입의 값을 넣는 것은 타입 안정성을 위배하기 때문에 null 만 삽입이 가능하다.

 

 

제네릭 타입에 제한을 거는 것처럼 와일드 카드에도 상한과 하한을 제한할 수 있는데, 이럴 때는 어떻게 작동을 하는 것일까??

 

와일드 카드의 상한 경계를 설정하는 것부터 한번 보도록 하자.

 

우선 요소를 출력하는 경우를 살펴보자. 와일드카드의 상한을 Car 타입으로 제한했기 때문에 가능한 모든 타입은 Car 타입 또는 그 하위 타입이 된다.

그렇기 때문에 요소를 출력하는 경우에는 Car 타입으로 받을 수 있다.

즉 리스트의 타입이 List<Car> List<Sedan> List<Suv>.. 이 될 수 있는데, 이들의 요소의 값은 Car 타입 또는 Sedan 타입 또는 Suv 타입일 것이다. 즉, 이들은 모두 Car 타입의 하위 타입이다.

 

요소를 입력하는 경우는 여전히 null을 제외하고는 아무런 타입을 입력할 수 없다. 

와일드 카드에 의해서 List<Car> List<Sedan> List<Suv> 타입이 가능하다. 

즉, 리스트의 요소의 타입이 Car Sedan Suv 중에 하나 일 것이고, 이들 중 어떤 타입인지 알 수 없다. 

그렇기 때문에 공통으로 참조할 수 있는 null을 제외하고는 아무것도 삽입할 수 없다.

 

 

이번에는 하한 경계의 경우를 보도록 하자.

 

하한경계는 ?에 해당하는 타입이 Car 타입의 상위 타입들이 올 수 있다. 

즉 상한 경계의 경우와 반대라고 보면 된다.  

 

와일드 카드에 의해서 가능한 타입은 List<Car>, ..... , List<Object> 가 된다. (Car 타입의 상위 타입)

 

그렇기 때문에 요소를 출력하는 과정에서 모든 요소의 타입을 수용할 수 있는 타입은 Object 타입이 된다. 

 

요소를 입력하는 과정에서는 반대로 Car 타입의 하위 타입은 모두 입력할 수 있다. 

왜냐하면 와일드 카드에 의해서 올 수 있는 타입들이 모두 Car 차입을 포함한 그 상위 타입이기 때문에, 어떠한 경우에도 Car 타입의 하위 타입에 대해서는 다형성이 성립하게 된다.

 

 

이처럼 제네릭은 컴파일 타임에 강력한 타입 체킹을 지원하기 때문에, 타입 에러로 인해서 런타임 에러가 발생하는 것을 막을 수 있다.

정말 고맙지만, 여전히 어려운 녀석이다.