자바 공부#3_01. 제네릭(Generics)과 PECS 원칙
제네릭(Generics) 이란?
자바에서 제네릭은 클래스 내부에서 사용할 데이터 타입을 나중에 확정하는 기법을 의미한다. 이는 클래스나 메서드를 선언할 때가 아니라 사용할 때, 즉 인스턴스를 생성할 때나 메서드를 호출할 때 정한다는 말이다.
List<String> list = new ArrayList<>();
위의 예시에서 <> 이 부분이 제네릭이다. 괄호 안에 타입을 명시하면 list의 데이터 타입을 String으로 지정할 수 있다.
만약 우리가 어떤 클래스 또는 메서드에 대해 다양한 타입을 지원하고 싶을 때, 제네릭을 사용하지 않는다면 두 가지 방법이 있다. Object 타입을 사용하는 방법과, 동일한 메커니즘을 가지는 클래스 및 메서드를 여러 개 정의하는 방법이다.
Object를 사용한다면 코드 중복은 막을 수 있지만 객체의 타입을 컴파일 타임에 체크할 수 없다는 제약이 생긴다. 이로 인해 런타임에 타입으로 인한 오류가 생길 수 있다.
동일한 메커니즘을 갖는 클래스 및 메서드를 여러 개 정의하는 건 너무 비효율적인 방법이다.
이러한 문제를 해결하기 위해 제네릭을 사용한다.
제네릭의 장점
- 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다. → 타입 안정성 ↑, 형변환의 번거로움 ↓
- 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
타입 안정성(type safety)
- 의도하지 않은 타입의 객체가 저장되는 것을 막는다.
- 저장된 객체를 꺼내올 때 다른 타입으로 잘못 형변환하여 발생할 수 있는 오류를 줄인다.
제네릭 타입 매개변수
제네릭의 <> 꺾쇠 괄호 안에 들어가는 타입 매개변수(타입 변수)는 제네릭을 이용한 클래스나 메서드를 설계할 때 사용된다. 주로 사용되는 타입들은 다음과 같다.
| 타입 매개변수 | 설명 |
| <T> | Type |
| <E> | Element |
| <K> | Key |
| <V> | Value |
| <N> | Number |
위의 타입들은 암묵적인 규칙이라고 보면 된다.
제네릭 클래스
제네릭은 타입을 선언한 클래스를 제네릭 클래스라 한다.
// 제네릭 타입 한 개.
public class ClassName <T> {
private T type;
public T getType() {
return type;
}
public void setType(T type) {
this.type = type;
}
}
// 제네릭 타입 두 개
public class ClassName <T, K> {
private T type;
private K key;
public T getType() {
return type;
}
public void setType(T type) {
this.type = type;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
}
// 제네릭 타입 두 개 예시: HashMap
public class HashMap <K, V> { ... }
이렇게 생성된 제네릭 클래스를 사용할 때에는, 즉 객체를 생성할 때에 구체적인 타입을 명시해주면 된다.
public class Main {
public static void main(String[] args) {
// 1번.
ClassName<String, Integer> a = new ClassName<String, Integer>();
// 2번.
ClassName<String, Integer> a = new ClassName<>();
// 3번.
ClassName a = new ClassName();
}
}
JDK 1.7부터 생성자의 <>에 타입을 생략할 수 있다. 따라서 2번과 같이 객체를 생성해도 문제가 없다.
또한 3번과 같이 타입을 지정하지 않고도 객체를 생성할 수 있는데, 이 때에는 타입을 Object로 간주한다.
제네릭 메서드
제네릭 타입을 선언한 메서드를 제너릭 메서드라고 한다.
클래스의 제네릭 타입이 전역 변수처럼 사용된다면 메서드의 제네릭 타입은 해당 메서드 안에서만 사용할 수 있는 지역성을 갖는다.
public class ClassName {
public <T> T method(T type) {
...
}
}
제네릭 타입을 메서드에 선언할 때는 접근제한자와 반환타입 사이에 선언한다.
ClassName a = new ClassName();
// 1번.
a.<String>method("string");
// 2번.
a.method("string");
제네릭 메서드 사용 시에는 위와 같이 메서드 명 앞에 타입을 지정해줘야 하짐나 컴파일러가 타입을 추정할 수 있는 경우에는 생략할 수 있다. 대부분의 경우 컴파일러가 타입을 추정할 수 있다.
하지만 제네릭을 사용할 때 주의할 점이 있다. 바로 정적 메서드로 선언할 때이다.
static 메서드는 객체가 생성되기 전에 이미 메모리에 올라가있어야하는데 다음과 같이 메서드에 타입을 따로 지정해주지 않으면 오류가 생긴다.
class ClassName<E> {
/*
* 클래스와 같은 E 타입이더라도
* static 메소드는 객체가 생성되기 이전 시점에
* 메모리에 먼저 올라가기 때문에
* E 유형을 클래스로부터 얻어올 방법이 없다.
*/
static E genericMethod01(E o) { // error!
return o;
}
// -> 아래와 같이 타입을 따로 명시해줘야 함
// 아래 메서드의 E 타입은 제네릭 클래스의 E 타입과 다른 독립적인 타입이다.
static <E> E genericMethod02(E o) { // error!
return o;
}
}
class Main {
public static void main(String[] args) {
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
ClassName.getnerMethod01(3);
// 클래스 객체는 String으로 생성
ClassName<String> a = new ClassName<String>();
ClassName.genericMethod02(3).getClass().getName(); // Integer
ClassName.genericMethod02("string").getClass().getName(); //String
}
}
제네릭 제한
객체를 생성할 때 타입을 지정하는데, 어떤 타입이 올 수 있는지를 제한하지 않으면 정말 모든 타입이 올 수 있다. 따라서 extends, super 키워드를 통해 이를 문법적으로 제한할 수 있는 기능을 제공한다.
extends - 제네릭 상한 제한
extends는 해당 타입 또는 그 하위 타입만 허용하도록 제한하는 기능이다.
class BoxMaterial { }
class Paper extends BoxMaterial { }
class Plastic extends BoxMaterial { }
public class Box<M extends BoxMaterial> {
private M material;
public static void main(String[] args) {
Box<Water> waterBox = new Box<>(); // 불가. BoxMaterial과 자식 타입만 가능
Box<Paper> paperBox = new Box<>();
Box<Plastic> plasticBox = new Box<>();
}
}
super- 제네릭 하한제한
super 키워드는 해당 타입과 그 상위 타입만 사용 가능하도록 제한할 때 사용한다.
class BoxMaterial { }
class Paper extends BoxMaterial { }
class Plastic extends BoxMaterial { }
class Waste extends Plastic { }
public class Box<T> {
private T material;
public Box(T material) {
this.material = material;
}
public T getMaterial() {
return material;
}
}
와일드 카드
와일드카드는 제네릭 타입에서 정확한 타입을 지정하지 않고 유연하게 사용하고 싶을 때 사용하는 특수한 문법이다. 쉽게 말해 '알 수 없는 타입'이라는 의미이다. 타입을 제한하거나 확장하면서도 다양한 타입을 받을 수 있는 장점이 있다. 사용 방법은 다음과 같다.
1. <?> : 모든 타입 허용 (Unbounded Wildcard)
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
- <?>는 어떤 타입이든 올 수 있다. → 한마디로 Object (최상위 타입) 라고 생각하면 됨.
- 단, 요소를 읽는 것만 가능하고 추가(insert)는 불가능하다 (컴파일 에러 발생).
2. <? extends T> : T와 T의 하위 타입만 허용 (Upper Bounded Wildcard)
public void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
- 예: List<Integer>, List<Double> 모두 허용
3. <? super T> : T와 T의 상위 타입만 허용 (Lower Bounded Wildcard)
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
- 예: List<Object>, List<Number> 허용
- 값을 추가(insert) 할 수 있지만, 꺼낼 때는 Object 타입으로만 꺼낼 수 있음
와일드카드 vs 제네릭 타입 매개변수
와일드카드는 메서드 파라미터에만 사용하고,
제네릭 타입 매개변수는 클래스나 메서드 전체에서 타입을 직접 다룰 때 사용
PECS (Producer Extends, Consumer Super) 원칙
PECS는 제네릭을 사용할 때 변성(variance)를 적용하는 규칙이다.
1. Producer Extends (PE)
데이터를 읽을 때 extends를 사용해야 한다는 원칙이다. 즉, "생산자"는 extends를 사용한다는 것이다.
이렇게 하면 하위 타입의 객체를 읽을 때 유연성이 증가한다.
public <T> void processProducer(List<? extends T> producer) {
for (T item : producer) {
// Do something with item
}
}
(받아 온 데이터를 이용해 뭔가를 produce 함)
예시)
List<? extends Fruit> basket = new ArrayList<Apple>();
Fruit f = basket.get(0); // ✅ OK, 안전하게 읽을 수 있음
basket.add(new Apple()); // ❌ 안 됨 (어떤 구체적 타입인지 몰라서)
여기서 list는 List<Animal>이든 List<Dog>이든 List<Cat>이든 받아서
Animal 타입으로 안전하게 읽을 수 있다.
2. Consumer Super (CS)
데이터를 쓸 때 super을 사용해야 한다는 원칙이다. 즉, "소비자"는 super을 사용한다는 것이다.
이렇게 하면 타입의 객체를 안전하게 쓸 수 있다.
public <T> void processConsumer(List<? super T> consumer, T item) {
consumer.add(item);
}
(받아 온 데이터에 나의 것을 consume 해서 무언가를 함)
예시)
List<? super Apple> basket = new ArrayList<Fruit>();
basket.add(new Apple()); // ✅ OK
basket.add(new FujiApple()); // ✅ OK (Apple의 하위 타입이니까)
Apple a = basket.get(0); // ❌ 안 됨 (Object로만 나옴)
여기서 list는 List<Dog>, List<Animal>, List<Object> 전부 가능.
Dog를 안전하게 넣을 수 있는 list를 받고 싶은 경우 이렇게 선언한다.
PECS는 왜 필요한가?
타입 안정성을 보장하면서 유연하게 설계하기 위해 필요하다. 잘못된 타입이 들어가는 걸 컴파일 타임에 방지할 수 있다는 장점이 있다.
참고:
- https://st-lab.tistory.com/153
- https://velog.io/@user1/PECS
- https://sunba30.tistory.com/50
- https://dy-coding.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-PECS%EB%9E%80