CodeGym /행동 /JAVA 25 SELF /제네릭: 왜 필요한가, 기본 문법

제네릭: 왜 필요한가, 기본 문법

JAVA 25 SELF
레벨 26, 레슨 4
사용 가능

1. “raw” 컬렉션의 문제(raw types)

잠깐 역사로 돌아가 봅시다. Java 5 이전에는 모든 컬렉션이 ‘무엇이든 담는’ 구조였습니다. 요소를 Object 타입으로 저장했고, 컴파일러는 거기에 무엇을 넣는지 전혀 검사하지 않았습니다. 문자열을 넣고 싶나요? 가능합니다. 숫자? 물론이죠. 고양이? 그것도 됩니다.

// 'raw' 컬렉션(raw types) 예시, Java 5 이전
List list = new ArrayList();
list.add("안녕");
list.add(42);
list.add(new Object());

값을 꺼내 사용하려 할 때 문제가 드러납니다:

String s = (String) list.get(0); // OK, 문자열입니다
String s2 = (String) list.get(1); // 붐! ClassCastException

컴파일러는 조용하지만, 런타임에 ClassCastException을 맞게 됩니다. ‘사과’라고 적힌 상자 안에 실제로는 컵, 바나나, 고슴도치가 들어 있는 것과 같습니다.

왜 문제가 될까요?

  • 오류가 실행 중에야 드러납니다.
  • 타입이 뒤섞입니다: 필요한 타입으로 직접 캐스팅(cast)해야 합니다.
  • 코드 가독성이 떨어지고 더 위험합니다.

해결책 — 제네릭(Generics)

제네릭(Generics)은 타입 매개변수를 갖는 클래스, 인터페이스, 메서드를 만들 수 있게 해 주는 메커니즘입니다. 즉, 컬렉션에 “문자열만 저장해”라고 말하면, 컴파일러가 이를 엄격히 지켜 줍니다.

List<String> words = new ArrayList<>();
words.add("안녕");
words.add("세상");
// words.add(42); // 컴파일 오류! List<String>에는 int를 추가할 수 없습니다.

이제 컴파일러가 문자열 이외에는 리스트에 넣지 못하게 막습니다. 오류는 프로그램을 실행하기 전에 잡힙니다.

제네릭의 핵심 아이디어:
컬렉션(뿐만 아니라 전반적인 코드)의 타입 안전성을 보장하여, 오류를 런타임이 아닌 컴파일 타임에 잡는다.

2. 제네릭 문법: 코드에서 어떻게 보이나요

꺾쇠 괄호에 타입 지정

컬렉션을 만들 때, 요소의 타입을 <> 안에 명시하세요:

List<String> names = new ArrayList<>();
names.add("앨리스");
names.add("밥");
// names.add(123); // 오류: 문자열 리스트에 숫자를 추가할 수 없습니다.

String first = names.get(0); // 형 변환이 필요 없다!

클래식 예:

  • List<String> — 문자열 리스트
  • List<Integer> — 정수 리스트
  • Set<Double> — 실수 집합
  • Map<String, Integer> — 키는 String, 값은 Integer

왜 그냥 List라고만 쓰면 안 될까요?

가능하지만, 제네릭의 장점을 모두 잃게 되며 컴파일러가 경고할 것입니다:

List list = new ArrayList(); // raw type — 권장되지 않습니다!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // 안녕, ClassCastException!

현대적인 Java 코드는 항상 제네릭을 사용합니다.

다이아몬드 연산자 <>

Java 7부터는 문맥상 타입이 명확하다면 우변의 타입을 생략할 수 있습니다:

List<String> list = new ArrayList<>(); // 컴파일러가 여기서 <String>임을 알아냅니다.

3. 다양한 컬렉션에서의 제네릭

List, Set, Map 예시

List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);

Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("앨리스");
uniqueNames.add("밥");

Map<String, Integer> ages = new HashMap<>();
ages.put("앨리스", 23);
ages.put("밥", 31);

사용자 정의 클래스 예시

class Student {
    String name;
    int age;
    // ...
}

List<Student> students = new ArrayList<>();
students.add(new Student());

4. 유용한 포인트

제네릭의 장점

타입 안전성. 컴파일러가 컬렉션에 올바른 타입의 요소만 들어가도록 보장합니다.

형 변환이 필요 없다. 예전: String s = (String) list.get(0);. 이제: String s = list.get(0);.

코드가 더 읽기 쉽고 더 안전하다. 런타임의 ‘깜짝 사건’이 줄어듭니다.

제네릭의 제한

기본형(primitives)을 사용할 수 없다. 제네릭은 기본형(int, double, boolean)이 아니라 객체에 대해서만 동작합니다. 박싱 클래스(래퍼)를 사용하세요: Integer, Double, Boolean.

List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int가 자동으로 Integer로 박싱됩니다(autoboxing)

타입 소거(type erasure) 간단 설명

Java에서 제네릭은 타입 소거 메커니즘으로 구현됩니다: 컴파일 후 타입 매개변수 정보가 지워지며, 런타임의 JVM은 이것이 List<String>이었는지, 그냥 List였는지 알지 못합니다. 이는 하위 호환성을 위한 설계입니다.

결과: 특정 타입 인자를 붙여 instanceof로 검사할 수 없습니다.

List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // 컴파일 오류!

다른 타입의 요소를 추가하려 하면 — 컴파일 오류

List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // 컴파일 오류: incompatible types: int cannot be converted to String

Map<String, Integer> map = new HashMap<>();
map.put("고양이", 5);
// map.put(3, "코끼리"); // 오류: 키는 String, 값은 Integer여야 합니다

이게 훌륭한 점입니다: 오류가 컴파일 타임에 잡힙니다.

컬렉션에만 국한되지 않음

제네릭은 사용자 정의 클래스와 메서드에서도 사용할 수 있습니다. 예를 들어 범용 “Box”:

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("안녕");
System.out.println(stringBox.get());

Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());

컬렉션에서 제네릭은 표준이며, Stream API나 Optional 같은 곳에서도 만나게 됩니다.

5. 제네릭을 사용할 때 흔한 실수

오류 1: “raw” 컬렉션 사용. List list = new ArrayList(); 같은 방식은 타입 안전성을 잃게 합니다. 항상 타입 매개변수를 지정하세요. 예: List<String>.

오류 2: 기본형을 사용하려는 시도. List<int>라고 쓸 수 없습니다. List<Integer>를 사용하세요.

오류 3: 컬렉션에서 읽을 때 수동 형 변환. 제네릭을 사용한다면 (String) list.get(i) 같은 캐스팅은 필요 없습니다. 그게 필요하다면 어딘가에서 타입 규약을 깼다는 뜻입니다.

오류 4: 타입 매개변수가 런타임에도 사용 가능하다고 기대함. 타입 소거 때문에 instanceof로 List<String> 같은 형태를 검사할 수 없습니다.

오류 5: 한 컬렉션에 서로 다른 타입을 섞기. List<String>으로 선언했다면 Integer를 추가하려 하지 마세요 — 컴파일러가 막아 줄 것이고, 그것이 바람직합니다.

1
과제
JAVA 25 SELF, 레벨 26, 레슨 4
잠금
나의 디지털 도서관 카탈로그 📚✍️
나의 디지털 도서관 카탈로그 📚✍️
1
과제
JAVA 25 SELF, 레벨 26, 레슨 4
잠금
마법의 범용 상자 📦✨
마법의 범용 상자 📦✨
1
설문조사/퀴즈
컬렉션과 generics, 레벨 26, 레슨 4
사용 불가능
컬렉션과 generics
컬렉션과 generics
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION