일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- GIT
- SQL
- 코테
- Kafka
- 포트폴리오
- LV03
- 프로그래머스
- LV0
- Redis
- 일정관리프로젝트
- CI/CD
- 일정관리 프로젝트
- Lv.0
- 디자인 패턴
- docker
- 이것이 자바다
- CoffiesVol.02
- LV.02
- mysql
- JPA
- 연습문제
- LV02
- Join
- S3
- LV01
- Java
- LV1
- 데이터 베이스
- 알고리즘
- spring boot
- Today
- Total
코드 저장소.
Generic 본문
목차
1.도입
2제네릭이란 ?
3.와일드 카드
4.제네릭 소거
5.정리
1.도입
평소에 자바로 코드를 작성을 하다보면 제네릭을 쓰는 경우가 많다. 하지만 막상 떠올리는 것은 "컴파일시 타입을 체크해준다","API의 표현력을 높여준다" 정도로 알고 있었다. 이번 글에서는 그 동안 애매하게 알고 있었던 제네릭에 대한 내용을 다시 정리해 보려고 한다. 기본 문법부터 와일드카드, 그리고 제네릭 소거까지 살펴보면서 실제 코드 예제도 같이 다뤄보려고 한다.
2.제네릭이란 ?
2-1. 제네릭이 필요한 이유
자바 1.4 시절에는 컬렉션이 전부 Object 타입으로 동작했다. 이 경우 타입 체크가 컴파일 시점에 이뤄지지 않아서 런타임 오류로 이어질 수 있었다.
List list = new ArrayList();
list.add("hello");
list.add(123); // 다른 타입도 들어가 버림
String str = (String) list.get(1); // 런타임에서 ClassCastException 발생
이 문제를 해결하기 위해 제네릭(Generic) 개념이 도입되었다. 제네릭은 컴파일 시점에 타입을 지정해 안전하게 사용할 수 있도록 하는 기능이다.
2-2. 제네릭 기본 문법
제네릭은 클래스/인터페이스에 타입 파라미터(<T>,<K,V>)를 선언을 하고 구체적인 타입으로 사용 지점에서 치환을 합니다.
// 제네릭 클래스
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// 사용
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String data = stringBox.get(); // 캐스팅 불필요
2-3. 메서드 제네릭
클래스 전체가 제네릭이 아니어도, 메서드 단위에서 제네릭을 선언해 사용할 수 있다.
public class Util {
public static <T> T pickFirst(T a, T b) {
return a != null ? a : b;
}
}
// 사용
String result = Util.pickFirst("A", "B"); // T는 String
Integer num = Util.pickFirst(1, 2); // T는 Integer
3.와일드 카드
3-1. 기본 개념
제네릭을 선언할 때 정확한 타입 대신 ?를 사용하면 와일드카드라고 부른다.
- <?> : 어떤 타입이든 올 수 있다.
- <? extends T> : T와 그 하위 타입만 허용.
- <? super T> : T와 그 상위 타입만 허용.
3-2. 단순 와일드카드 (<?>)
List<?> list = new ArrayList<String>();
list.add(null); // null만 추가 가능
Object obj = list.get(0); // 꺼낼 때는 Object
->타입은 모르겠지만, 뭔가 담긴 리스트라는 뜻.
3-3. 상한 제한 와일드카드 (<? extends T>)
읽기 전용처럼 동작한다.
List<? extends Number> numbers = new ArrayList<Integer>();
Number n = numbers.get(0); // 읽기는 OK
// numbers.add(10); // 컴파일 에러 (추가 불가)
->Producer Extends: 값을 꺼낼 때만 안전하다.
3-4. 하한 제한 와일드카드 (<? super T>)
쓰기 전용처럼 동작한다.
List<? super Integer> integers = new ArrayList<Number>();
integers.add(10); // 추가 가능
Object obj = integers.get(0); // 꺼낼 때는 Object
-> Consumer Super: 값을 넣을 때만 안전하다.
4.제네릭 소거
4-1. 제네릭 소거란?
자바에서 제네릭은 런타임 기능이 아니다. 컴파일 단계에서만 타입을 검사하고, 실제 실행 시점에는 타입 정보가 지워지는 방식을 쓴다. 이걸 제네릭 소거(Type Erasure)라고 한다. 덕분에 하위 호환성이 유지되지만, 몇 가지 제약이 생긴다.
4-1. 같은 클래스 취급
List<String>과 List<Integer>는 다른 타입처럼 보이지만, 컴파일이 끝나면 둘 다 단순히 ArrayList일 뿐이다.
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass()); // class java.util.ArrayList
System.out.println(integers.getClass()); // class java.util.ArrayList
System.out.println(strings.getClass() == integers.getClass()); // true
4-2. 상한(bound) 소거
타입 파라미터에 제한을 걸면, 결국 그 제한 타입으로 치환된다.
class NumberBox<T extends Number> {
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
NumberBox<Integer> intBox = new NumberBox<>();
intBox.set(100);
Number result = intBox.get(); // 반환 타입은 Number로 소거됨
System.out.println(result.getClass()); // class java.lang.Integer
4-3. 배열과의 충돌
제네릭 타입 배열은 만들 수 없다. 배열은 런타임에 타입 체크를 하지만, 제네릭은 타입 정보가 런타임에 없기 때문이다.
// List<String>[] array = new List<String>[10]; // 컴파일 에러
List<?>[] array = new List<?>[10]; // 이렇게만 가능
array[0] = Arrays.asList("hello");
System.out.println(array[0].get(0)); // hello
4-4. 오버로딩 불가
제네릭 소거 때문에, 타입 파라미터만 다른 메서드는 오버로딩이 불가능하다.
// 컴파일 시 둘 다 print(List list)로 소거됨 → 에러 발생
public void print(List<String> list) {}
public void print(List<Integer> list) {} // 컴파일 에러
5.정리
마지막으로 제네릭을 공부한것을 다시 정리해보면 아래와 같습니다.
- 제네릭이란? → 컴파일 시점에 타입을 강제해 캐스팅 문제를 줄이고, 코드 재사용성과 API 표현력을 높인다.
- 와일드카드 → ?, extends, super를 통해 읽기 전용/쓰기 전용 제약을 표현할 수 있다.
- 제네릭 소거(Type Erasure) → 런타임에는 타입 정보가 사라져 같은 클래스 취급을 받으며, 배열/오버로딩/리플렉션에서 제약이 생긴다.
실제로 코드를 돌려보니, List<String>과 List<Integer>가 같은 클래스라는 점이나, 제네릭 배열이 안 되는 이유가 확실히 이해됐다. 그동안 막연히 “타입을 검사해준다” 정도로만 알고 있었는데, 이번에 정리하면서 제네릭의 원리와 한계까지 알 수 있었다.
제네릭은 단순히 문법적인 장식이 아니라, 안전하고 표현력 있는 API를 만드는 도구라는 점이 인상 깊다. 앞으로는 JPA의 JpaRepository<T, ID> 같은 실무 코드에서도 “왜 제네릭이 필요했는지”를 더 의식하면서 볼 수 있을 것 같다.
'Java' 카테고리의 다른 글
GC(Garbage Collector) (0) | 2025.09.06 |
---|---|
== vs equals(), hashCode() (0) | 2025.09.06 |
Exception 처리 (Checked vs Unchecked) (0) | 2025.09.06 |
String vs StringBuilder vs StringBuffer (0) | 2025.09.05 |
GraalVM? (0) | 2025.08.05 |