개발을 할 때 코드의 재사용성을 높이고, 안정성을 확보하는 것은 매우 중요하다.
특히 Java에서는 다양한 타입의 데이터를 다루다 보면, 타입 안전성을 유지하면서도 유연한 코드가 필요하다.
이를 효과적으로 해결하는 방법 중 하나가 제네릭(Generic) 이다.
제네릭(Generic)이란 클래스나 메서드를 정의할 때, 특정 타입을 미리 지정하지 않고 사용할 때 결정할 수 있도록 하는 기능이다.
즉, 코드를 작성할 때 타입을 고정하지 않고, 실제 사용할 때 원하는 타입을 지정할 수 있도록 유연성을 제공한다.
예를 들어, List<String>
과 List<Integer>
는 각각 문자열과 정수를 저장하는 리스트지만, 내부적으로는 같은 List<T>
클래스를 사용한다.
여기서 T
는 제네릭 타입 매개변수(Type Parameter)이며, 다양한 타입을 처리할 수 있도록 도와준다.
class Box<T> { // T는 임의의 타입을 의미
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println(stringBox.getItem()); // Hello
Box<Integer> intBox = new Box<>();
intBox.setItem(100);
System.out.println(intBox.getItem()); // 100
}
}
위 코드에서 Box<T>
클래스는 T
라는 제네릭 타입을 사용하므로, String
이나 Integer
등 다양한 타입을 저장할 수 있다.
제네릭을 사용하면 컴파일 시점에 타입을 체크할 수 있어, 잘못된 타입이 들어가는 것을 방지할 수 있다.
제네릭이 없던 시절에는 Object
를 사용해야 했는데, 이 경우 타입 변환 과정에서 런타임 오류가 발생할 가능성이 컸다.
제네릭을 사용하지 않은 경우 (문제 발생 가능)
import java.util.ArrayList;
public class NonGenericExample {
public static void main(String[] args) {
ArrayList list = new ArrayList(); // 타입을 지정하지 않음
list.add("Hello");
list.add(100); // 다른 타입도 추가 가능
String str = (String) list.get(1); // ClassCastException 발생 가능
System.out.println(str);
}
}
위 코드에서는 ArrayList
에 String
과 Integer
를 함께 넣을 수 있다.
하지만 String
으로 캐스팅할 때 ClassCastException
이 발생할 위험이 있다.
제네릭을 사용한 경우 (안전한 코드)
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(); // 타입을 String으로 제한
list.add("Hello");
// list.add(100); // 컴파일 오류 발생
String str = list.get(0); // 캐스팅 불필요
System.out.println(str);
}
}
제네릭을 사용하면 잘못된 타입의 값이 들어가는 것을 컴파일 시점에 방지할 수 있다.
제네릭을 사용하면 하나의 클래스나 메서드를 다양한 타입에 대해 재사용할 수 있다.
예를 들어, 특정 타입의 데이터를 저장하는 Box
클래스를 만들고 싶다고 가정하자.
제네릭이 없다면, 각 타입마다 별도의 클래스를 만들어야 한다.
class StringBox {
private String item;
public void setItem(String item) { this.item = item; }
public String getItem() { return item; }
}
class IntegerBox {
private Integer item;
public void setItem(Integer item) { this.item = item; }
public Integer getItem() { return item; }
}
하지만 제네릭을 사용하면 하나의 클래스만으로 다양한 타입을 처리할 수 있다.
class Box<T> {
private T item;
public void setItem(T item) { this.item = item; }
public T getItem() { return item; }
}
이처럼 제네릭을 사용하면 중복 코드를 줄이고, 유지보수성을 높일 수 있다.
제네릭은 클래스뿐만 아니라 메서드에서도 활용 가능하다.
class Util {
public static <T> void printItem(T item) {
System.out.println(item);
}
}
public class Main {
public static void main(String[] args) {
Util.printItem("Hello"); // 문자열 출력
Util.printItem(123); // 정수 출력
Util.printItem(3.14); // 실수 출력
}
}
위 코드에서 printItem
메서드는 입력받는 타입을 제한하지 않는다.
따라서 문자열, 정수, 실수 등 어떤 타입의 데이터도 출력할 수 있다.
좀 더 와닿는 예제를 생각해보자.
예를 들어, 온라인 쇼핑몰에서 다양한 유형의 상품을 관리하는 시스템을 설계한다고 가정하자.
제네릭을 사용하면 상품의 타입을 제한하지 않고 유연하게 관리할 수 있다.
class Product<T> {
private String name;
private T value; // 가격, 크기, 무게 등 다양한 속성을 저장 가능
public Product(String name, T value) {
this.name = name;
this.value = value;
}
public void displayInfo() {
System.out.println("상품명: " + name + ", 속성: " + value);
}
}
public class ShoppingMall {
public static void main(String[] args) {
Product<Integer> laptop = new Product<>("노트북", 1500000); // 가격 (정수)
Product<Double> apple = new Product<>("사과", 1.2); // 무게 (실수)
Product<String> tshirt = new Product<>("티셔츠", "Large"); // 사이즈 (문자열)
laptop.displayInfo();
apple.displayInfo();
tshirt.displayInfo();
}
}
실행 결과
상품명: 노트북, 속성: 1500000
상품명: 사과, 속성: 1.2
상품명: 티셔츠, 속성: Large
제네릭은 자바의 컬렉션 프레임워크에서도 필수적으로 사용된다.
import java.util.ArrayList;
import java.util.List;
public class GenericListExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
for (String name : names) {
System.out.println(name);
}
}
}
위 코드에서 List<String>
을 사용하면 String
타입의 데이터만 저장할 수 있다.
덕분에 타입 안정성이 보장되고, 불필요한 형 변환을 피할 수 있다.
제네릭은 타입 안정성을 높이고, 코드의 재사용성을 증가시키며, 유지보수성을 개선하는 강력한 기능이다.
컬렉션, 유틸리티 클래스 등 다양한 곳에서 제네릭이 활용될 수 있다.
제네릭을 잘 활용하면 유연하면서도 안정적인 코드를 작성할 수 있다.
제네릭을 적극적으로 활용해 불필요한 형 변환을 줄이고, 더 안전한 코드를 작성해보자!