제네릭은 Java에서 데이터 타입을 일반화하여 다양한 타입을 지원하는 클래스나 메서드를 정의할 수 있게 하는 기능
이를 통해 타입 안정성 보장, 코드의 재사용성 향상
타입 안정성(Type Safety) : 컴파일 시점에 타입을 확인하며, 런타임 에러를 줄이고 안정성 향상, 즉 제네릭을 사용하면 타입 변환(Casting)과정에서 발생할 수 있는 오류를 방지 가능
코드 재사용성(Code Resusability) : 다양한 데이터 타입에 대해 동일한 로직을 사용할 수 있는 코드 작성이 가능
가독성과 유지보수성 향상 : 명확한 타입 지정으로 코드의 가독성이 좋아지고, 유지보수가 쉬워짐.
-> 이게 즉 사용 이유
컴파일 시 타입 에러 방지하여 잘못된 사용 방지. 제네릭을 사용하지 않으면 사용할 때마다 명시적으로 타입 캐스팅을 해야하는데 이러한 과정이 불필요 해짐. + 동일한 코드를 여러 타입에 대해 재사용할 수 있어 중복 코드를 줄이고 유지보수를 쉽게할 수 있다.
import java.util.ArrayList;
public class WithoutGenerics {
public static void main(String[] args) {
ArrayList list = new ArrayList(); // Raw 타입 사용
list.add("Hello");
list.add(123); // 잘못된 타입도 추가 가능
String str = (String) list.get(0); // 명시적 캐스팅 필요
System.out.println(str);
// 잘못된 타입 캐스팅 시 런타임 에러 발생
String num = (String) list.get(1); // ClassCastException
}
}
import java.util.ArrayList;
public class WithGenerics {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>(); // 제네릭 타입 지정
list.add("Hello");
// list.add(123); // 컴파일 에러로 방지됨
String str = list.get(0); // 명시적 캐스팅 불필요
System.out.println(str);
}
}
대부분의 컬렉션 타입, 제네릭 클래스, 제네릭 메서드 등
이 때 주의해야 할 점은 제네릭은 기본 타입이 안되서 오토박싱으로 인한 레퍼 클래스를 사용해야 한다.
또한 컴파일러 수준에서 동작하기 때문에 런타임에는 타입 정보가 소거되어 Object로 처리된다.
목표: 타입에 구애받지 않는 데이터를 저장하고 관리하는 제네릭 클래스를 작성합니다.
실습:
Box 클래스를 만들어 하나의 데이터를 저장하고 가져올 수 있게 구현.Pair 클래스를 만들어 두 개의 데이터를 관리하도록 구현.// Box 클래스
public class Box<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());
Box<Integer> intBox = new Box<>();
intBox.setItem(123);
System.out.println(intBox.getItem());
}
}
목표: 타입에 따라 동작하는 범용적인 메서드를 작성합니다.
실습:
printArray 메서드 작성.compare 메서드 작성.public class GenericMethods {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static <T extends Comparable<T>> T getMax(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
public static void main(String[] args) {
String[] names = {"Alice", "Bob", "Charlie"};
Integer[] numbers = {1, 2, 3};
printArray(names);
printArray(numbers);
System.out.println("Max: " + getMax(10, 20));
System.out.println("Max: " + getMax("Apple", "Orange"));
}
}
목표: Java 컬렉션에 제네릭을 적용해 데이터를 관리하고 처리하는 방법을 익힙니다.
실습:
List<T>를 사용해 다양한 타입의 데이터를 관리.Map<K, V>를 활용해 키-값 쌍을 저장하고 검색.import java.util.*;
public class CollectionExample {
public static void main(String[] args) {
// List 예제
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
for (String name : names) {
System.out.println(name);
}
// Map 예제
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 85);
System.out.println(scores.get("Alice"));
// 정렬 예제
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
Collections.sort(numbers);
System.out.println(numbers);
}
}
목표: 제네릭을 활용한 실무에서의 사용법을 익힙니다.
실습:
1. API 요청/응답 처리
Response<T> 클래스를 만들어 다양한 API 응답 데이터를 처리.
public class Response<T> {
private String status;
private T data;
public Response(String status, T data) {
this.status = status;
this.data = data;
}
public String getStatus() {
return status;
}
public T getData() {
return data;
}
}
public class Main {
public static void main(String[] args) {
Response<String> response = new Response<>("success", "Data loaded");
System.out.println(response.getStatus() + ": " + response.getData());
}
}
Repository 클래스 구현
public class Repository<T> {
private List<T> data = new ArrayList<>();
public void add(T item) {
data.add(item);
}
public T get(int index) {
return data.get(index);
}
}
public class Main {
public static void main(String[] args) {
Repository<String> repo = new Repository<>();
repo.add("Item 1");
repo.add("Item 2");
System.out.println(repo.get(0));
}
}
제네릭은 Java의 강력한 기능 중 하나로, 데이터 타입의 안정성을 보장하고 코드의 재사용성을 높입니다. 신입 백엔드 개발자는 제네릭을 활용하여 다양한 데이터 구조와 실무 환경에 익숙해질 수 있습니다. 위의 실습 과제를 진행하면서 실무에서 어떻게 활용할지 감을 잡을 수 있을 것입니다.