용어 정리
아이템 26부터 33까지의 내용은 제네릭 타입과 관련된 챕터이며, 이에 따라 제네릭 관련 용어 정리부터 진행하겠습니다.
1. 로 타입 (raw type)
제네릭 타입을 하나 정의하면 그에 딸린 로 타입도 함께 정의되며 로 타입이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말합니다.
- ex) List
2. 제네릭 타입 (generic type)
클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 제네릭 클래스 혹은 제네릭 인터페이스라고 합니다.
제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라고 합니다.
- ex) List<E>
3. 매개변수화 타입 (parameterized type)
각각의 제네릭 타입은 일련의 매개변수화 타입을 정의합니다.
먼저 클래스 혹은 인터페이스 명이 나오고 이어서 <> 안에 실제 타입 매개변수들을 나열합니다.
- ex) List<String>
- 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
4. 타입 매개변수 (type parameter)
제네릭 타입 내 <>에 명시하는 매개변수를 타입 매개변수라고 합니다.
- ex) 제네릭 타입 List<E> 내 E
5. 실제 타입 매개변수 (actual type paramter)
매개변수화 타입에서 <> 안에 명시하는 타입을 실제 타입 매개변수라고 합니다.
- ex) 앞선 List<String> 내 String
6. 한정적 타입 매개변수 (bounded type paramter)
타입 매개변수를 특정 상위 클래스의 하위 클래스로 한정 짓는 것을 한정적 타입 매개변수라고 합니다.
- ex) List<E extends Number>
7. 비한정적 와일드카드 타입 (unbounded wildcard type)
제네릭 클래스나 메서드에서 타입 매개변수를 완전히 무제한으로 처리하는 것을 나타냅니다.
이를 통해 모든 타입의 객체를 다룰 수 있게 되며, 런타임 시에는 알 수 없는 타입의 객체도 처리할 수 있습니다.
Unbounded wildcard type은 ?로 표현되며, 주로 다양한 타입의 컬렉션에서 유연한 사용을 위해 활용됩니다.
- ex) Class<?>
8. 한정적 와일드카드 타입 (bounded wildcard type)
특정 범위 내의 타입만을 허용하도록 제한하는 것을 나타냅니다.
이는 상한 경계(upper bound)와 하한 경계(lower bound)로 나눌 수 있습니다.
- 상한 경계(upper bound): 타입 매개변수가 특정 클래스나 그 클래스의 하위 클래스여야 한다는 제약을 의미
- 상한 경계는 extends 키워드를 사용하여 정의
- ex) <? extends Number>은 Number 클래스나 Number의 하위 클래스만을 허용한다는 의미
- 하한 경계(lower bound): 타입 매개변수가 특정 클래스나 그 클래스의 상위 클래스여야 한다는 제약을 의미
- 하한 경계는 super 키워드를 사용하여 정의
- ex) <? super Integer>은 Integer 클래스나 Integer의 상위 클래스만을 허용한다는 의미
매개변수화 타입을 사용해야 하는 이유
매개변수화 타입을 사용하는 이유는 안전성 확보와 표현력 향상을 위함입니다.
1. 안전성
로 타입을 사용할 경우 아무 타입이나 넣을 수 있기 때문에 안전성이 깨집니다.
가령 숫자만 들어있을 것을 기대한 List에 실수로 String 타입이 들어갈 경우 타입 캐스팅하는 과정에 ClassCastException이 발생할 것입니다.
즉 지원하지 않는 타입을 추가하더라도 컴파일 타임에 알 수가 없고 설령 런타임 에러가 발생하더라도 버그 원인을 찾아내기 어렵기 때문에 안전성이 떨어집니다.
반면 제네릭을 사용할 경우 특정 타입만 넣을 수 있다고 명시할 수 있고 명시한 타입 외 다른 타입을 넣으려고 하면 컴파일 에러가 발생하기 때문에 안전성을 보장할 수 있습니다.
런타임이 아닌 컴파일 타임에 실패하기 때문에 fail-fast라고 합니다.
2. 표현력
앞서 언급했다시피 제네릭을 사용할 경우 어떤 타입을 받을 수 있는지 명시하기 때문에 표현력이 좋아져 코드가 명확해집니다.
자바에서 로 타입을 지원하는 이유
그렇다면 자바에서 왜 안전성과 표현력이 떨어지는 로 타입을 허용할까?
자바가 아이템 15에서 언급했다시피 하위 버전 호환성을 유지하는 것을 추구하기 때문입니다.
자바가 제네릭을 받아들이기까지 거의 10년이 걸린 탓에 제네릭 없이 구현한 코드가 너무 많아졌고 제네릭을 도입했다 하더라도 기존 코드들을 모두 수용해야 했습니다.
즉, 로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야만 했던 것입니다.
실제로 바이트 코드를 보면 제네릭으로 선언한 클래스도 컴파일 과정에서 타입이 벗겨지고 Object로 받은 뒤 제네릭일 경우 컴파일러가 자체적으로 타입 체킹하는 것을 볼 수 있습니다.
정리하자면 Integer 타입은 소스 코드 상에서만 보이는 것이지 컴파일된 파일에는 타입이 다 벗겨지고 로 타입처럼 보이고 컴파일러가 실제 타입 매개변수 기반으로 타입 체킹을 진행합니다.
제네릭 타입을 잘 활용한 예시
DB 연동을 하는 CRUD 레포지토리를 만들 때 별다른 비즈니스 로직이 없을 경우 클래스 타입만 다르고 동작 방식은 동일한 레포지토리를 테이블 개수만큼 중복 생성하는 경우가 있습니다.
이때 템플릿 레포지토리를 제네릭 타입으로 만들고 각 CRUD 레포지토리가 해당 레포지토리를 상속하게 구현하면 중복 코드를 제거할 수 있습니다.
1. 제네릭 타입 도입 전
2. 제네릭 타입 도입 후
매개변수화 타입을 사용할 수 없는 경우
제네릭 타입을 사용할 수 없는 케이스는 두 가지가 있습니다.
- class 리터럴
- instanceof 연산자
1. class 리터럴
앞서 언급했다시피 컴파일하는 과정에서 실제 매개변수 타입은 소거가 되기 때문에 실제 제네릭 클래스는 존재하지 않고 로 타입 클래스만 존재합니다.
따라서 class 리터럴에서는 제네릭 타입을 사용할 수 없습니다.
- ex) List<String>.class (X)
- ex) List.class (O)
2. instanceof 연산자
instnaceof 연산자 같은 경우 제네릭 타입을 사용할 수는 있지만 코드가 오히려 장황해지기 때문에 로 타입을 통해 instanceof 연산자를 사용하는 것을 권장합니다.
비고
1. List vs List<Object>
로 타입인 List는 Object 타입을 다 수용할 수 있으므로 둘 다 동일한 것처럼 보이지만 매개변수로 로 타입을 선언하는 것과 실제 타입 매개변수가 Object인 클래스를 선언하는 것은 큰 차이가 있습니다.
아래 예제 코드를 보면 로 타입으로 선언할 경우 String 타입 리스트에 String 타입이 아닌 요소를 넣을 수 있고 리스트에서 꺼내는 과정에서 String 타입이 아니기 때문에 ClassCastException이 발생하는 것을 확인할 수 있습니다.
즉, 안전성이 깨집니다.
반면 List<Object>를 파라미터로 선언할 경우 List<String>과 다른 제네릭 타입이기 때문에 fail-fast하게 컴파일 타임에서 오류를 발견할 수 있습니다.
2. Set vs Set<?>
마찬가지로 로 타입으로 파라미터를 선언할 경우 아무 컬렉션이나 파라미터로 넘길 수 있고 아무 요소나 추가할 수 있기 때문에 안전성이 깨집니다.
Set<?>은 어떤 제네릭 타입도 받아들일 수 있지만, 주요 차이점은 null을 제외한 요소를 추가할 수 없어서 어느 정도 안정성을 보장합니다.
정리
제네릭 타입은 자바가 추구하는 하위 버전 호환성 유지를 위해 제공되었을 뿐 직접 사용할 경우 안전성이 깨지고 표현력이 저하되기 때문에 사용하는 것을 지양해야 합니다.
참고
이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 28] 배열보다는 리스트를 사용하라 (1) | 2024.02.18 |
---|---|
[아이템 27] 비검사 경고를 제거하라 (1) | 2024.02.16 |
[아이템 25] 톱레벨 클래스는 한 파일에 하나만 담으라 (1) | 2024.02.09 |
[아이템 24] 멤버 클래스는 되도록 static으로 만들라 (0) | 2024.02.08 |
[아이템 23] 태그 달린 클래스보다는 클래스 계층 구조를 활용하라 (0) | 2024.02.07 |