자바 백엔드 입문 6편. List
이 글은 자바 백엔드 입문 시리즈 59편 중 6편이에요. 5편 자바 컬렉션 에서 매번 등장한 List<String> 의 <> 부분 — 자바 제네릭 의 정체를 풀어 가요.
제네릭이 헷갈리는 이유
자바 코드 거의 어디에나 <T>·<E>·<K, V> 같은 표현이 박혀 있어요. 처음 보면 "이게 뭘 의미하지?" 가 안 잡혀요. 또 <? extends Number> 같은 와일드카드도 등장해 복잡.
이 글에서는 도시락 통 비유로 풀어요. List<String> = "문자열 전용 도시락 통", Map<Long, User> = "Long 키 → User 값 도시락 통". 도시락 통 자체는 같은데, "무엇을 담는지" 가 타입 매개변수로 명시.
제네릭이 등장한 배경
자바 5(2004) 이전엔 컬렉션이 "아무거나 담을 수 있는 도시락 통" 이었어요.
// 자바 1.4 시절 — 타입 명시 X
List names = new ArrayList();
names.add("Alice");
names.add(123); // 정수도 박힘 (의도와 다름)
String name = (String) names.get(0); // 매번 캐스팅 필요
String wrong = (String) names.get(1); // ClassCastException! (123은 String 아님)
문제: - 타입 안전 X — 의도와 다른 객체 박혀도 컴파일 통과 - 매번 캐스팅 — 코드 더러움 - 런타임 에러 — 캐스팅 실패가 실행 중 폭발
자바 5에서 제네릭 도입.
// 자바 5+ — 타입 명시
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(123); // 컴파일 에러! Integer는 String 아님
String name = names.get(0); // 캐스팅 불필요
컴파일 시점에 타입 안전 보장 + 캐스팅 자동. 자바 코드의 신뢰성·가독성 한 단계 상승.
타입 매개변수 — T·E·K·V
제네릭 정의 시 사용하는 "타입 변수". 컨벤션이 있어요.
| 글자 | 의미 |
|---|---|
| T | Type (일반) |
| E | Element (컬렉션 요소) |
| K | Key (Map 키) |
| V | Value (Map 값) |
| N | Number |
| R | Return type |
public interface List<E> { // E = Element
void add(E element);
E get(int index);
}
public interface Map<K, V> { // K = Key, V = Value
V put(K key, V value);
V get(K key);
}
<E>·<K, V> 가 "이 클래스를 쓸 때 타입을 채워 넣을 자리". 이걸 타입 매개변수(Type Parameter).
제네릭 클래스 직접 만들기
타입 안전한 "한 개 값 박스" 만들기.
public class Box<T> { // 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 s = stringBox.get(); // 캐스팅 불필요
Box<Integer> intBox = new Box<>();
intBox.set(42);
intBox.set("문자열"); // ❌ 컴파일 에러
같은 Box 클래스인데 — <String> 박으면 문자열 전용, <Integer> 박으면 정수 전용. 도시락 통 비유.
제네릭 메서드
클래스 자체는 제네릭이 아닌데 — 특정 메서드만 제네릭으로.
public class Utils {
public static <T> T pickFirst(List<T> list) { // <T> 메서드 레벨 선언
return list.isEmpty() ? null : list.get(0);
}
}
// 사용
String s = Utils.pickFirst(List.of("Alice", "Bob")); // T = String 자동 추론
Integer n = Utils.pickFirst(List.of(1, 2, 3)); // T = Integer 자동
메서드 시그니처 앞에 <T> 박으면 — 그 메서드 안에서 T 사용 가능. 호출 시 자동 추론.
Bounded — <T extends Number>
타입 매개변수에 "제약" 박기.
public class NumberBox<T extends Number> { // T는 Number 또는 그 자식
private T value;
public double doubled() {
return value.doubleValue() * 2; // Number 메서드 호출 가능
}
}
NumberBox<Integer> intBox = new NumberBox<>(); // OK — Integer는 Number 자식
NumberBox<Double> doubleBox = new NumberBox<>(); // OK
NumberBox<String> wrong = new NumberBox<>(); // ❌ String은 Number 자식 X
<T extends Number> = "T는 Number 또는 그 하위 클래스만". 그 안에서 Number의 메서드(doubleValue·intValue 등) 호출 가능.
와일드카드 <?> — 읽기 전용
이건 함수 매개변수에 자주.
// 어떤 타입이든 받지만 — 안의 요소를 정확한 타입으로 다룰 수 없음
public void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
printAll(List.of("a", "b")); // OK
printAll(List.of(1, 2, 3)); // OK
<?> = "무엇이든 OK, 다만 안에 박을 수는 없음". "읽기 전용 매개변수" 같은 용도.
상·하한 와일드카드
List<? extends Number> readOnly; // Number 또는 자식의 List만 받음 (읽기)
List<? super Integer> writeOnly; // Integer 또는 부모의 List 받음 (쓰기 가능)
이건 입문 단계에서 깊이 X. "PECS — Producer Extends, Consumer Super" 라는 룰이 있는데, 처음엔 신경 안 써도 OK.
타입 소거 — JVM의 비밀
자바 제네릭의 흥미로운 사실 — 컴파일 후엔 타입 정보가 사라져요. 이걸 타입 소거(Type Erasure).
// 우리가 쓰는 코드
List<String> names = new ArrayList<>();
// 컴파일 후 .class 파일 안 (개념)
List names = new ArrayList(); // <String> 사라짐
JVM은 "리스트" 만 알지 "문자열 리스트" 라는 정보는 런타임에 없어요. 그래서:
// 리플렉션으로 타입 확인
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true (둘 다 ArrayList.class)
이 "타입 소거" 가 자바 제네릭의 한계 — 런타임에 T.class 같은 표현을 직접 못 씀. JPA·Jackson 같은 라이브러리가 "TypeReference<List<User>>" 같은 우회 패턴을 쓰는 이유.
Spring·JPA에서 자주 보는 제네릭
이 시리즈의 거의 모든 글에서 등장.
// JPA Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
Optional<Order> findById(Long id);
List<Order> findByStatus(String status);
}
// Service
public Optional<User> findByEmail(String email) { ... }
// Controller
public ResponseEntity<List<Order>> list() { ... }
<Order, Long>·<Order>·<List<Order>> — 다 제네릭. 자바 백엔드의 일상.
제네릭은 "이 컨테이너에 무엇이 들어가는지 명시" 도구. List<String> 한 줄로 컴파일러가 타입 검증 자동. <T extends ...> 와 와일드카드는 깊이 가면 복잡하지만 — 입문에선 "클래스 이름 옆 <타입>" 만 알면 80% 끝.
한 줄 정리 — 자바 제네릭 = 컴파일 시점 타입 안전 도구. <T> 타입 매개변수로 "이 컨테이너에 무엇이 들어가나" 명시. ArrayList·HashMap 같은 컬렉션이 핵심 사용처. 런타임엔 타입 소거로 정보 사라짐.
시험 직전 한 번 더 — 자바 제네릭 입문자가 매번 헷갈리는 것
- 제네릭 = 컴파일 시점 타입 안전 도구 (자바 5+)
- 도입 전 =
List names·매번 캐스팅·런타임 에러 - 도입 후 =
List<String>·캐스팅 불필요·컴파일 시점 검증 - 타입 매개변수 컨벤션 = T(Type)·E(Element)·K(Key)·V(Value)·N(Number)·R(Return)
- 제네릭 클래스 =
class Box<T> { ... } - 제네릭 메서드 =
<T> T pickFirst(List<T> list) - Bounded =
<T extends Number>— Number 자식만 - 자바엔
<T super ...>같은 클래스 레벨 하한 X (메서드 매개변수 와일드카드에만) - 와일드카드
<?>= 무엇이든, 읽기만 <? extends Number>= 읽기 (Number 자식들)<? super Integer>= 쓰기 (Integer 부모들)- PECS = Producer Extends, Consumer Super (입문 단계 X)
- 타입 소거 = 컴파일 후 타입 정보 사라짐
- 런타임에
T.class직접 사용 X TypeReference<List<User>>= 우회 패턴 (Jackson 등)<>다이아몬드 연산자 = 자바 7+ — 우변 타입 자동 추론new ArrayList<String>()→new ArrayList<>()(자바 7+)- 제네릭 배열 생성 X —
new T[10]불가 - 자바 백엔드 = 매일 제네릭 다룸
- Spring·JPA·Jackson 모두 제네릭 기반
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: