꾸준한 스터디
article thumbnail

제네릭 용어 정리

  • 로 타입 (raw type): List
  • 제네릭 타입 (generic type): List<E>
  • 매개변수화 타입 (parameterized type): List<String>
  • 정규 타입 매개변수 (formal type parameter): E
  • 실제 타입 매개변수 (actual type parameter): String
  • 한정적 타입 매개변수 (bounded type parameter): List<E extends Number>
  • 비한정적 와일드카드 타입 (unbounded wildcard type): Class<?>
  • 한정적 와일드카드 타입 (bounded wildcard type): Class<? extends Annotaion>

 

 

로 타입

코드 11번 라인 List에 정규 타입 매개변수가 없는 경우 '로 타입'이라고 한다.

 

List 인터페이스 시그니쳐를 보면 <E> 정규 타입 매개변수를 지정할 수 있음에도 선언하지 않은경우 로타입을 사용했다고 한다. 정규 타입 매개변수를 선언하지 않은 로타입의 경우 어떠한 객체가 담긴 컬렉션인지 선언하지 않았기 때문에 컬렉션에 객체를 넣을 때 아무 객체나 넣을 수 있다.

로 타입 List라면 객체를 담을 때 특정한 타입을 갖는게 아니라 Object를 넣을 수 있어서 모든 객체가 들어갈 수 있다.

 

컬렉션에 객체를 넣을때는 큰 문제가 되지 않지만 막상 이 컬렉션에서 객체를 꺼내와 사용할 때 에러가 발생할 수 있다.

가령 코드 16번 라인에 List 객체에 Integer 타입의 객체만 넣어서 쓰려고 했는데 누가 String 타입의 객체를 넣어 버렸고 출력시 ClassCastExeption이 발생하게 된다.

해당 에러는 컴파일 타임에 잡을 수 없어서 런타임 때, 서비스 구동 중 에러가 날 수 있는 위험한 코드로 문제를 찾아내기 어렵다.

 

제네릭이 등장한 이후 컬렉션에 들어갈 객체 타입은 Integer 로 실제 타입 매개변수가 정해지면 해당 컬렉션에는 Integer 밖에 넣을 수 없게 된다.

String을 넣게되면 순간 컴파일 에러가 나서 오류를 금방 잡을 수 있게 된다.

개발자는 오류가 날 코드들을 컴파일 타임에 발견되어 수정할 수 있는 것이 프로그램 오류를 줄일 수 있는 좋은 방법이다.

 

이미지에서 보이듯 Generic 사용전 add 메서드 매개변수 타입이 Object이고 Generic 사용 후 실제 타입 매개변수인 Integer로 메서드 매개변수가 고정이 되어 컴파일 타임에 오류를 잡아낼 수 있다.

 

그리고 제네릭 타입의 컬렉션은 이미 어떤 객체가 들어간 컬렉션인지 알 수 있으니 바로 Integer타입으로 꺼내서 사용할 때 형변환을 사용하지 않아도 된다.

 

 

 

제네릭 타입, 정규 타입 매개변수

 

클래스 시그니쳐에 정규 타입 매개변수가 선언되어 있다면 '제네릭 타입' 혹인 '제네릭 클래스'라고 한다.

저 <E>는 '정규 타입 매개변수'라고 하며 선언된 제네릭 클래스를 생성하거나 사용할 때 매개변수 요소가 지정되면 해당 요소로 클래스에 있는 모든 E부분이 실제 타입 매개변수로 바뀐다.

 

 

 

실제 타입 매개변수, 매개변수화 타입

 

Box<Integer> 라면 Integer가 '실제 타입 매개변수'이며 Box 클래스의 필드인 item도 Integer이고 add메서드도 Integer 인자값을 받고 get 메서드 리턴 객체도 Integer로 변경이 되어 실제 타입 매개변수로 들어오는 요소(타입, 객체)로 지정되어 사용된다. 실제 타입 매개변수로 선언된 Box<Integer> 자체는 '매개변수화 타입'이라고 부른다.

 

 

 

한정적 타입 매개변수

타입 매개변수는 어떠한 객체든 올 수 있지만 제한적으로 사용할 수 있게 '한정적 타입 매개변수'로 만들 수 있다.

만약 <E extends Number> 라고 '한정적 타입 매개변수'로 선언한다면 Number를 상속받은 하위 객체들만 사용할 수 있다.

메인 메서드에서 Integer는 Number의 하위 객체여서 사용이 가능하다.

 

 

 

비한정적 와일드카드 타입, 한정적 와일드카드 타입

printBox 메서드를 보면 인자값에 Box<?> box 를 받고 있다. 이 물음표는 와일드 카드라고 하고 제네릭 타입의 다형성을 적용한 기능이다. '비한정적 와일드카드 타입' 이라 부르고 제한 없이 모든 타입이 올 수 있다는 표시이다.

인자 값으로 넘겨받는 box가 Integer 이든 String이던 다 받아낼 수 있다.

와일드 카드에도 extends 키워드와 super 키워드로 제한할 수 있는데

<? extends T> T타입과 T타입 하위객체만 가능

<? super T> T타입과 T타입 조상들만 가능하게 제한을 줄 수 있는 '한정적 와일드카드 타입' 이라고 한다.

 

사실 <?> '비 한정적 와일드카드 타입은' <? extends Object> 와 동일한 것이어서 Object 의 모든 하위 객체를 받을 수 있는 것이다.

 

 

 

로 타입은 사용하지 말라 핵심 정리

매개변수화 타입을 사용해야 하는 이유

  • 런타임이 아닌 컴파일 타임에 문제를 찾을 수 있다. (안전성)
  • 제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다. (표현력)
    • "로 타입"을 사용하면 안정성과 표현력을 잃는다.
  • "로 타입"을 지원하는 이유
  • List와 List<Object>의 차이
  • Set과 Set<?>의 차이
  • 예외 : class 리터럴과 instanceof 연산자

 

런타임이 아닌 컴파일 타임에 문제를 찾을 수 있다. (안전성)

이전 용어 정리에도 봤듯이 제네릭 사용 이전에는 아무 Object 객체 자식들이 들어가기 때문에 사용시에 형변환 할 수 없는 객체가 있다면 런타임시 에러가 날 가능성이 있어서 안전성이 깨지게 된다.

제네릭을 사용하게 되면 이러한 문제를 컴파일 타임에 알 수 있어서 좀 더 안전한 코딩이 가능하다.

 

제네릭을 활용하면 이 정보가 주석이 아닌 타입 선언 자체에 녹아든다. (표현력)

컬렉션 내부에 1가지 객체만 넣어야 할 경우 제네릭이 없었을 때는 주석으로 컬렉션 안에 지정된 객체만 넣어야 한다는 주석을 달았지만 이제는 명시적으로 리스트 내부에 들어갈 객체를 선언해주게 되니 별도의 설명이 필요 없게 된다.

 

로 타입을 지원하는 이유

하위 버전 호환성 때문이다. 자바 5버전 이전의 코드들에서 제네릭을 사용하지 않은 기존코드를 수용하면서 제네릭을 사용하는 새로운 코드들과 맞물려 돌아가게 해야만 했기 때문이다. 이 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거 방식을 사용하기로 했다.

제네릭을 사용한 코드를 컴파일 하면 모든 제네릭이 다 사라진채 바이트 코드가 만들어진다.

바이트 코드를 살펴보면 Object로 받아온 값들을 Integer로 캐스팅 하는 코드를 자바가 컴파일 타임에 넣어주는 것.

제네릭으로 선언된 타입 매개변수는 .java 를 만드는 개발자의 눈에만 보이는 것.

 

List와 List<Object>의 차이

List는 제네릭 타입에서 완전히 발을 뺀 것이고 List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에 명확히 전달한 것이다.

매개변수로 List를 받는 메서드에 List<String>을 넘길 수 있지만 List<Object>를 받는 메서드에는 제네릭의 하위 타입 규칙때문에 넘길 수 없다.

 즉, List<String>은 로 타입인 List의 하위 객체이지만 List<Object>의 하위 타입은 아니다.

List 같은 로 타입을 사용하면 타입 안정성을 잃게 된다.

List에 Object 객체는 모두 들어갈 수 있으니 컬렉션에 넣는데는 문제가 없어서 컴파일은 되지만 로 타입인 List를 값들을 꺼낼 때 ClassCastException 예외가 발생한다.

Integer 값이 들어가 형변환 할 수 없기 때문이다.

로타입 List를 매개변수화 타입인 List<Object>로 바꾸면 오류로 인해 컴파일조차 되지 않는다.

 

Set과 Set<?>의 차이

numElementsInCommon메서드는 동작은 하지만 로 타입을 사용해 안전하지 않다. 비한정적 와일드 카드 타입을 대신 사용하는 것이 좋다. 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 어떤 객체가 들어오는지 신경 쓰고 싶지 않다면 ? 물을표를 사용하자. Set<?> 이것이 어떤 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입이다.

Set과 Set<?>의 차이는 와일드카드 타입은 안전하고 로 타입은 안전하지 않다.

로 타입 컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다. 반면 Collection<?>에는 null 외에는 어떤 원소도 넣을 수 없다. 다른 원소를 넣으려 하면 컴파일시 오류메세지를 볼 수 있다.

*와일드 카드를 사용한 컬렉션이라면 한 번 주입되면 다른 객체가 들어갈 수 없고 null만 들어갈 수 있는 것 같다.

 

예외 class 리터럴과 instanceof 연산자

class 리터럴에는 로타입을 써야한다. UseRawType<Integer>.class 처럼 제네릭 타입이 아닌 로타입을 사용해야 한다. 컴파일 하고나면 타입 매개변수는 소거되기 때문이다.

instanceof 연산자는 비한정적 와일드카드 타입을 사용은 할 수있지만 컴파일시 타입 매개변수가 사라지는데 코드만 길어지므로 로타입을 사용하는 것이 깔끔하다.

 

 

 

제네릭을 사용한 DAO

 

 

 

https://www.inflearn.com/course/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-2/dashboard

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!