제네릭(Generics)이란 무엇이며, 왜 사용하나요?

김상욱·2024년 11월 14일

제네릭(Generics)이란 무엇이며, 왜 사용하나요?

제네릭은 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로 처리된다.

  • 정적 컨텍스트에서 사용이 제한된다. 즉, static 필드나 메서드에서 사용할 수 없다. -> 정적 컨텍스트는 컴파일러 실행시가 그 전인 클래스 로드 시에 메모리에 로드되어 실행되기 때문.

신입 백엔드 개발자를 위한 실습 아이디어

1. 제네릭 클래스 구현

목표: 타입에 구애받지 않는 데이터를 저장하고 관리하는 제네릭 클래스를 작성합니다.

실습:

  • 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());
    }
}

2. 제네릭 메서드 작성

목표: 타입에 따라 동작하는 범용적인 메서드를 작성합니다.

실습:

  • 배열 요소를 출력하는 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"));
    }
}

3. 제네릭과 컬렉션 연습

목표: 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);
    }
}

4. 제네릭과 실무 시나리오

목표: 제네릭을 활용한 실무에서의 사용법을 익힙니다.

실습:
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());
        }
    }
  1. Repository 클래스 구현

    • 간단한 제네릭 DAO(데이터 접근 객체) 작성.
    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));
        }
    }

5. 실습 결과 정리

  • 블로그나 GitHub에 실습 결과를 정리.
  • 제네릭의 개념, 사용 이유, 코드 예제와 결과를 함께 정리하여 기록.

결론

제네릭은 Java의 강력한 기능 중 하나로, 데이터 타입의 안정성을 보장하고 코드의 재사용성을 높입니다. 신입 백엔드 개발자는 제네릭을 활용하여 다양한 데이터 구조와 실무 환경에 익숙해질 수 있습니다. 위의 실습 과제를 진행하면서 실무에서 어떻게 활용할지 감을 잡을 수 있을 것입니다.

0개의 댓글