Search
Duplicate
📒

[Java Study] 08. 제네릭

상태
완료
수업
Java Study
주제
4 more properties
참고

제네릭

NOTE
자바에서 제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다.
제네릭은 자바5와 함께 도입되었으며 제네릭의 등장으로 코드의 안전성을 높이고 재활용성을 높일 수 있습니다. 단계별로 제네릭이 왜 필요한지 코드로 알아봅시다.
먼저 문자와 숫자에 대해서 값을 보관하고, 꺼내주는 기능을 제공하는 클래스를 만들어봅시다.
public class IntegerBox { // 정수 변수 private Integer value; // 값을 수정한다. public void set(Integer value){ this.value = value; } // 값을 꺼낸다. public Integer get() { return value; } }
Java
복사
public class StringBox { // 문자열 변수 private String value; // 값을 수정한다. public void set(String value){ this.value = value; } // 값을 꺼낸다. public String get() { return value; } }
Java
복사
위의 코드를 보면 타입을 제외하고는 로직이 완전히 동일한것을 확인할 수 있습니다.
중복되는 코드를 제거하기 위해서 1차적으로 다형성을 도입해볼 수 있습니다.
public class ObjectBox { // 모든 타입을 받는 변수 private Object value; // 값을 수정한다. public void set(Object object) { this.value = object; } // 값을 꺼낸다. public Object get(){ return value; } }
Java
복사
Object(Integer, String 둘다 허용)
public static void main(String[] args) { ObjectBox integerBox = new ObjectBox(); integerBox.set(10); Integer integer = (Integer) integerBox.get(); // Object -> Integer 캐스팅 System.out.println("integer = " + integer); ObjectBox stringBox = new ObjectBox(); stringBox.set("hello"); String str = (String) stringBox.get(); // Object -> String 캐스팅 System.out.println("str = " + str); // 잘못된 타입의 인수 전달시 integerBox.set("문자100"); Integer result = (Integer) integerBox.get(); // String -> Integer 캐스팅 예외 System.out.println("result = " + result); }
Java
복사
Object로 중복되는 코드를 줄일 수 있었지만, 값을 꺼낼 때 타입 캐스팅이 필요해졌습니다.
그리고 Object로 사용한 만큼 String, Integer이외의 타입이 들어갈 수 있게 되었습니다. 이 문제는 특정 인터페이스를 구현하도록 만들면 해결할 수는 있지만 본질적인 해결책은 아닙니다.
public class GenericBox<T> { // T 타입의 변수 private T value; // 값을 수정한다. public void setValue(T value) { this.value = value; } // 값을 꺼낸다. public T getValue() { return value; } }
Java
복사
Object → T 타입으로 변경
public static void main(String[] args) { // T -> Integer GenericBox<Integer> integerBox = new GenericBox<>(); integerBox.setValue(10); Integer integer = integerBox.getValue(); System.out.println("integer = " + integer); // T -> String GenericBox<String> stringBox = new GenericBox<>(); stringBox.setValue("Hello"); String str = stringBox.getValue(); System.out.println("str = " + str); }
Java
복사
코드 명확성과 재사용성 증가

제네릭 명명 관례

NOTE
제네릭 타입 파라미터에는 일반적으로 대문자 한 글자를 사용합니다. 가장 흔히 사용되는 타입 파라미터 이름은 다음과 같습니다.
E : Element의 약자로, 컬렉션에 저장되는 요소의 타입을 지정합니다.
K : Key의 약자로, 맵의 키로 사용되는 타입을 지칭합니다.
V : Value의 약자로, 맵의 값으로 사용되는 타입을 지칭합니다.
N : Number
T : Type
S, U, V 등 : 두 번째, 세 번째, 네 번째 타입

로(row) 타입

NOTE
제네릭이 도입되기 전의 코드와의 호환성을 위해 자바에서는 제네릭 타입을 사용하지 않고 클래스나 인터페이스를 사용할 수 있으며 이를 로 타입이라 합니다.
로 타입의 대표적인 경우는 List 인터페이스를 제네릭 없이 사용하는 경우이며 이 경우, 모든 타입의 데이터가 들어갈 수 있지만, 데이터 반환시 형변환이 필요하게 되고, 예외가 발생할 수 있습니다.
public static void main(String[] args) { GenericBox integerBox = new GenericBox(); integerBox.setValue(10); // // 값 추출시 형 변환 필요 Integer result = (Integer) integerBox.getValue(); System.out.println("result = " + result); }
Java
복사
위의 코드를 보면 List 로 타입이 List<Object>와 동일하게 기능하는 것 처럼 느껴질 수 있습니다. 하지만 List<Object>Object를 사용하는것을 명시한 코드이고, List는 제네릭의 이점을 완전히 버리는 코드입니다.
List<Object>: Object로 타입이 저장되며, 컴파일 시점에 타입 체크가 가능합니다.
List: 어떤 객체도 저장할 수 있지만, 타입 정보가 없어 타입 불일치로 인한 런타임 오류 발생 가능성이 커집니다.

한정적 와일드카드(extends, super)

NOTE
한정적 와일드 카드는 제네릭 타입 인자의 범위를 특정 상위/하위 타입으로 제한하는 기능입니다.
// 상한 와일드 카드(T 하위타입 허용) <? extends T> // 하한 와일드 카드(T 상위타입 허용) <? super T>
Java
복사
한정적 와일드 카드 종류
한정적 와일드 카드를 사용하여 제네릭을 사용하는 방법을 알아보기 위해 동물 병원 시스템을 개발해봅시다. 시스템 개발에 초기코드는 아래와 같습니다.
public class Animal { private String name; private int size; // Constructor, Getter, Setter .. public void sound(){ System.out.println("동물 울음소리"); } // ... }
Java
복사
Animal (부모)
// 자식 클래스 1 public class Cat extends Animal { public Cat(String name, int size) { super(name, size); } @Override public void sound() { System.out.println("냐옹"); } } // 자식 클래스 2 public class Dog extends Animal{ public Dog(String name, int size) { super(name, size); } @Override public void sound() { System.out.println("멍멍"); } }
Java
복사
public class AnimalHospitalV1 { private Animal animal; // 동물 설정 public void setAnimal(Animal animal) { this.animal = animal; } // 동물의 이름과 크기 public void checkup(){ System.out.println("animal.getName() = " + animal.getName()); System.out.println("animal.getSize() = " + animal.getSize()); animal.sound(); } // 동물 크기 비교 public Animal getBigger(Animal target) { return animal.getSize() > target.getSize() ? animal : target; } }
Java
복사
위의 코드에서 제네릭읕 도입해서 Animal이 아닌 Dog, Cat의 구체타입을 사용할 수 있도록 해봅시다. 여기서 단순히 Animal타입을 T로 사용하면 문제가 생깁니다.
public class AnimalHospitalV2<T> { private T animal; public void setAnimal(T animal) { this.animal = animal; } public void checkup(){ // 컴파일 에러! // System.out.println("animal.getName() = " + animal.getName()); // System.out.println("animal.getSize() = " + animal.getSize()); // animal.sound(); } public T getBigger(T target) { // 컴파일 에러! // return animal.getSize() > target.getSize() ? animal : target; } }
Java
복사
T는 getName(), getSize()와 같은 메서드를 모른다.
T의 타입이 정해지지 않았기 때문에 Animal의 메서드를 사용할 수 없습니다.
이러한 문제를 한정적 와일드카드를 사용해서 해결할 수 있습니다.
public class AnimalHospitalV2<T extends Animal> { private T animal; public void setAnimal(T animal) { this.animal = animal; } public void checkup(){ System.out.println("animal.getName() = " + animal.getName()); System.out.println("animal.getSize() = " + animal.getSize()); animal.sound(); } public T getBigger(T target) { return animal.getSize() > target.getSize() ? animal : target; } }
Java
복사
public static void main(String[] args) { AnimalHospitalV2<Dog> dogHospital = new AnimalHospitalV2<>(); AnimalHospitalV2<Cat> catHospital = new AnimalHospitalV2<>(); Dog dog = new Dog("멍멍이1", 100); Cat cat = new Cat("냐옹이1", 300); // 개 병원 dogHospital.setAnimal(dog); dogHospital.checkup(); // 고양이 병원 catHospital.setAnimal(cat); catHospital.checkup(); dogHospital.setAnimal(dog); catHospital.setAnimal(cat); Dog biggerDog = dogHospital.getBigger(new Dog("멍멍이", 200)); System.out.println("biggerDog = " + biggerDog); }
Java
복사
T extends Animal을 사용하여 제네릭 타입을 Animal과 그 자식들로 제한해서 Animal의 메서드를 사용할 수 있게 되었습니다.

제네릭 메서드

NOTE
제네릭은 메서드 단위에서도 적용할 수 있으며, 클래스 전체가 아닌 특정 메서드에만 제네릭을 적용하고 싶을 때 유용합니다.
public class GenericMethod { // static 메서드 사용가능 public static <T> T genericMethod(T obj) { System.out.println("generic print = " + obj); return obj; } // 제한자 사용가능 public static <T extends Number> T numberMethod(T t) { System.out.println("bound print = " + t); return t; } }
Java
복사
public static void main(String[] args) { Integer i = 10; System.out.println("명시적 타입 인자 전달"); Integer result = GenericMethod.<Integer>genericMethod(i); Integer integerValue = GenericMethod.<Integer>numberMethod(10); Integer result1 = GenericMethod.genericMethod(i); Integer integerValue1 = GenericMethod.numberMethod(10); }
Java
복사
제네릭 메서드는 static과 일반 변수에 모두 적용이 가능하며, 메서드 호출 시 독립적으로 취급되기 때문입니다.
public class ComplexBox <T extends Animal> { // 클래스 제네릭 T private T animal; public void set(T animal) { this.animal = animal; } // 메서드 제네릭 Z (해당 메서드에서 T는 상한이 없는 Object로 취급된다.) public <Z> Z printAndReturn(Z z) { System.out.println("animal.className = " + animal.getClass().getName()); System.out.println("t.className = " + z.getClass().getName()); return z; } }
Java
복사
public static void main(String[] args) { Dog dog = new Dog("멍멍이", 100); Cat cat = new Cat("냐옹이", 50); ComplexBox<Dog> hospital = new ComplexBox<>(); hospital.set(dog); Cat returnCat = hospital.<Cat>printAndReturn(cat); System.out.println("returnCat = " + returnCat); }
Java
복사
제네릭 메서드는 클래스에서 선언한 제네릭보다 우선순위를 가집니다.
제네릭 메서드에서 클래스 제네릭은 상한이 없는 Object로 취급되며, 제한자가 있어도 해당 메서드를 호출할 수 없습니다.

와일드 카드(?)

NOTE
와일드 카드는 컴퓨터 프로그래밍에서 *, ?와 같이 하나 이상 문자들을 상징하는 특수 문자를 의미하며, 여러 타입이 들어올 수 있다는 뜻입니다.
public class Box<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Java
복사
// 제네릭 메서드 static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } // 와일드카드를 사용한 메서드 static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); } // 제네릭 메서드 값 사용 (T 타입) static <T extends Animal> T printAndReturnGeneric(Box<T> box) { T t = box.getValue(); System.out.println("이름 = " + t.getName()); return t; } // 와일드 카드 한정 값 사용 (Animal 타입) static Animal printAndReturnWildcard(Box<? extends Animal> box) { Animal animal = box.getValue(); System.out.println("이름 = " + animal.getName()); return animal; }
Java
복사
두 메서드는 비슷해 보이지만, 내부적인 동작에서 차이가 있습니다.
제네릭 메서드: 호출 시점에 T의 구체적인 타입이 결정된다.
와일드카드 메서드: 어떤 타입의 Box 객체도 받을 수 있다.
//1. 전달 printGenericV1(dogBox) //2. 제네릭 타입 결정 dogBox는 Box<Dog> 타입, 타입 추론 -> T의 타입은 Dog static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } //3. 타입 인자 결정 static <Dog> void printGenericV1(Box<Dog> box) { System.out.println("T = " + box.get()); } //4. 최종 실행 메서드 static void printGenericV1(Box<Dog> box) { System.out.println("T = " + box.get()); }
Java
복사
제네릭 메서드의 흐름
// 1. 전달 printWildcardV1(dogBox) // 2. 최종 실행 메서드, 와일드카드 ?는 모든 타입을 받을 수 있다. static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); }
Java
복사
와일드 카드 메서드의 흐름

타입 이레이저

NOTE
제네릭은 컴파일 단계에서만 사용되며, 컴파일 이후에는 제네릭 정보가 삭제됩니다. 즉, 제네릭에서 사용한 타입 매개변수가 모두 사라지는 것이며, .java 파일에는 타입 매개변수가 존재하지만 .class 파일에는 존재하지 않습니다.
public class Main { public static void main(String[] args) { GenericBox<Integer> box = new GenericBox<Integer>(); box.set(10); Integer result = box.get(); } }
Java
복사
제네릭 - 컴파일 전
public class Main { public static void main(String[] var0) { GenericBox var1 = new GenericBox(); var1.set(10); Integer var2 = (Integer)var1.get(); // 다운 캐스팅 적용! } }
Java
복사
제네릭 - 컴파일 후
자바의 제네릭은 단순하게 생각하면 개발자가 직접 캐스팅 하는 코드를 컴파일러가 대신 처리해주는 방식입니다.
자바는 컴파일 시점에 제네릭을 사용한 코드에 문제가 없는지 완벽하게 검증하기 때문에, 컴파일러의 다운 캐스팅에서는 문제가 발생하지 않는다.
자바의 제네릭은 컴파일 시점에만 존재하고, 런타임시에는 제네릭 정보가 지워진다.

타입 이레이저 방식의 한계

컴파일 이후에는 제네릭의 정보가 존재하지 않는다는건, 런타임에 타입을 활용한다는 코드를 작성할 수 없다는것입니다.
public class EraserBox<T> { public boolean instanceCheck(Object param) { // return param instanceof T; return false; } public void create() { // return new T(); } }
Java
복사
instancdof: 런타임에 객체 타입을 확인하는 연산자이므로, 제네릭에서 사용이 불가능합니다.
new T(): T는 컴파일 이후에 Object로 변환되기 때문에, new Object()와 동일해지며 원치 않는 결과가 초래하기 때문에 컴파일에러가 발생합니다.

제네릭 배열과 리스트

Java의 배열은 제네릭과 완전히 호환되지 않습니다.
Object[] objectArray = new Long[1]; objectArray[0] = "타입이 달라 넣을 수 없다"; // 런타임에서 예외발생 List<Object> ol = new ArrayList<Long>(); // 컴파일에서 예외발생 ol.add("타입이 달라 넣을 수 없다");
Java
복사
ex)제네릭 타입의 배열 new T[]와 같은 표현은 Java에서 효옹되지 않는다
ArrayList<T>와 같은 리스트는 제네릭을 사용하여 컴파일 시 타입 체크를 통해 타입 안전성을 높일수 있습니다.
핵심 정리를 하자면 배열과 제네릭은 매우 다른 타입 규칙이 적용됩니다.
배열: 공변이고 실체화됩니다. 런타임에는 안전하지만 컴파일에는 그렇지 않습니다.
제네릭: 불공변이고 타입 정보가 소거 됩니다. 런타임에 불안전하고 컴파일에 안전합니다.