타입을 안전하게, 코드는 유연하게

1. 왜 제네릭이 필요할까?

무엇이든 담을 수 있는 리스트를 만들면?

import java.util.ArrayList;
import java.util.List;

public class BeforeGenericExample {
    public static void main(String[] args) {
        List<Object> items = new ArrayList();

        items.add("사과");
        items.add(100);

        Object first = items.get(0);
        String fruit = (String) first;
        System.out.println(fruit.toUpperCase());

        // 여기서 문제가 생깁니다.
        // 두 번째 값은 Integer인데, 실수로 String으로 형변환하고 있습니다.
        // 컴파일은 되지만 실행 중 ClassCastException이 발생합니다.
        String secondFruit = (String) items.get(1);
        System.out.println(secondFruit.toUpperCase());
    }
}

문제는 여기서 시작된다

처음에는 "유연해서 편하다"라고 느껴질 수 있습니다.

하지만 실무에서는 이런 유연함이 오히려 오류를 발생시켜 불안함이 됩니다.

더 안전한 방법은 없을까?

여기서 Generic이 등장합니다.

"이 상자에는 어떤 타입만 담길지 미리 알려주자."


2. 제네릭은 무엇을 바꿨을까?

문자열만 담는 리스트라고 미리 선언하면?

import java.util.ArrayList;
import java.util.List;

public class AfterGenericExample {
    public static void main(String[] args) {
        // List<String>은 "문자열만 담는 리스트"라는 뜻입니다.
        List<String> fruits = new ArrayList<>();

        // 문자열은 정상적으로 저장됩니다.
        fruits.add("사과");

        // 아래 코드는 컴파일 단계에서 막힙니다.
        // 즉, 실행하기 전에 실수를 발견할 수 있습니다.
        // fruits.add(100);

        // 꺼낼 때도 이미 String 타입으로 보장되므로 형변환이 필요 없습니다.
        String fruit = fruits.get(0);
        System.out.println(fruit.toUpperCase());
    }
}

Generic : Type Safety

flowchart LR
    A["List"] --> B["String도 저장 가능"]
    A --> C["Integer도 저장 가능"]
    C --> D["꺼낼 때 직접 캐스팅"]
    D --> E["런타임 오류 가능"]

    F["List<String>"] --> G["String만 저장 가능"]
    G --> H["형변환 불필요"]
    H --> I["컴파일 단계에서 오류 차단"]