티스토리 뷰

Language/Java

[JAVA] Generics에 대하여

토랑 2025. 1. 14. 18:04

Generics란?

  • Generics래스, 인터페이스, 메서드에 사용할 타입을 파라미터화할 수 있도록 해주는 기능
  • 주로 컴파일 타임타입 안정성을 보장하고, 불필요한 타입 캐스팅을 줄이는 데 사용

Generics의 주요 목적

  • 타입 안정성
    • 잘못된 타입 사용으로 인한 오류 방지
  • 코드 재사용성
    • 타입에 구애받지 않는 유연한 코드 작성
  • 가독성 향상
    • 명시적으로 타입을 정의하여 코드 이해도 증가

Generics의 기본 사용법

  • Generics는 주로 <> (다이아몬드 연산자)로 표현

1. 클래스와 인터페이스

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

위 코드에서 T는 Type Parameter로, Box 클래스가 어떤 타입이든 받을 수 있도록 한다

2. 메서드

public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

Generics 메서드는 클래스에 정의된 타입 매개변수와 독립적으로 동작하며, 메서드 수준에서 타입을 파라미터화할 수 있다

3. 컬렉션과 함께 사용

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");

String name = names.get(0); // 타입 캐스팅 불필요

Generics를 사용하면 List가 특정 타입(String)만 받을 수 있도록 제한


Generics의 주요 원칙

1. 제네릭 타입은 컴파일 타임에만 존재한다

  • Generics는 Type Erasure(타입 소거)를 사용하여 컴파일 시 타입 정보를 제거하고, 런타임에는 일반 클래스 또는 인터페이스로 동작
  • 예를 들어, List<String>와 List<Integer>런타임에 동일한 List로 간주됩니다.
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true

2. 제한된 타입 매개변수

Generics에서 특정 타입만 허용하고 싶을 때 bounded type parameter를 사용할 수 있습니다.

상위 타입 제한

public <T extends Number> void process(T number) {
    System.out.println(number.doubleValue());
}
  • T는 Number의 하위 클래스만 가능

다중 제한

public <T extends Number & Comparable<T>> void process(T value) {
    System.out.println(value.compareTo(value));
}
  • T는 Number를 상속
  • Comparable 인터페이스를 구현해야 함

3. 와일드카드

와일드카드는 Generics의 유연성을 높여줌

와일드카드 사용 예시

public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
  • ?
    • 모든 타입을 허용.
  • ? extends T
    • T와 그 하위 타입만 허용.
  • ? super T
    • T와 그 상위 타입만 허용.

실무에서 Generics 사용 사례

1. 데이터 매핑

  • 데이터베이스에서 가져온 결과를 특정 객체로 매핑하는 DAO(Data Access Object) 계층에서 Generics를 사용할 수 있음

예시: 데이터 매핑

public interface GenericDao<T, ID> {
    void save(T entity);
    T findById(ID id);
    List<T> findAll();
}

public class UserDao implements GenericDao<User, Long> {
    private final Map<Long, User> database = new HashMap<>();

    @Override
    public void save(User entity) {
        database.put(entity.getId(), entity);
    }

    @Override
    public User findById(Long id) {
        return database.get(id);
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(database.values());
    }
}

// 사용 예시
GenericDao<User, Long> userDao = new UserDao();
User user = new User(1L, "Alice");
userDao.save(user);
User retrievedUser = userDao.findById(1L);

2. 서비스 계층에서의 응답 처리

  • API 설계 시 서비스 계층에서 공통 응답 구조를 정의할 때 Generics를 활용하면 효율적입니다.

예시: 공통 응답 구조

public class ApiResponse<T> {
    private String status;
    private T data;

    public ApiResponse(String status, T data) {
        this.status = status;
        this.data = data;
    }

    // Getter and Setter
    public String getStatus() {
        return status;
    }

    public T getData() {
        return data;
    }
}

// 사용 예시
ApiResponse<String> successResponse = new ApiResponse<>("success", "Operation completed.");
ApiResponse<User> userResponse = new ApiResponse<>("success", new User(1L, "Alice"));

3. 유효성 검사 도구

  • Generics를 활용해 다양한 객체의 유효성을 검사하는 공통 유틸리티를 작성할 수 있습니다.

예시: Validator 유틸리티

public interface Validator<T> {
    boolean validate(T entity);
}

public class UserValidator implements Validator<User> {
    @Override
    public boolean validate(User user) {
        return user.getName() != null && user.getId() != null;
    }
}

// 사용 예시
Validator<User> validator = new UserValidator();
User user = new User(1L, "Alice");
boolean isValid = validator.validate(user);

Generics의 장단점

장점

  • 컴파일 타임 타입 체크
    • 잘못된 타입 사용을 컴파일 타임에 방지
  • 타입 캐스팅 제거
    • 명시적 타입 캐스팅 불필요
  • 코드 재사용성
    • 다양한 타입을 처리하는 코드 작성 가능

단점

  • 타입 소거
    • 런타임에는 Generics 타입 정보가 소거되어 리플렉션 사용 시 제한이 있음
  • 원시 타입 호환성 문제
    • Generics는 기본 타입(int, double 등)을 직접 처리하지 못하고 Wrapper Class를 사용해야 함

면접 대비 질문

더보기

1. 제네릭이란 무엇인가요?

답변: 제네릭(Generics)은 자바에서 클래스, 인터페이스, 메서드에 사용할 데이터 타입을 일반화하여 작성할 수 있도록 도와주는 기능입니다. 제네릭을 사용하면 컴파일 타임에 타입을 검사하고, 잘못된 타입이 사용되는 것을 방지할 수 있습니다.


2. 제네릭을 사용하는 이유는 무엇인가요?

답변:

  • 타입 안정성 제공: 잘못된 데이터 타입이 사용되는 것을 방지합니다.
  • 캐스팅 제거: 명시적인 타입 변환이 필요 없습니다.
  • 코드 재사용성 증가: 여러 타입에 대해 동일한 코드 작성이 가능합니다.

3. 제네릭을 사용하는 기본적인 코드 예시는 무엇인가요?

답변:

a. 제네릭 없이 사용하는 경우:

import java.util.ArrayList;

public class NoGenericsExample {
    public static void main(String[] args) {
        ArrayList list = new ArrayList(); // 타입 지정 X
        list.add("Hello");
        list.add(123); // 잘못된 타입 추가 가능

        String str = (String) list.get(1); // ClassCastException 발생 가능
    }
}

b. 제네릭을 사용하는 경우:

import java.util.ArrayList;

public class GenericsExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>(); // String 타입으로 제한
        list.add("Hello");
        // list.add(123); // 컴파일 에러 발생

        String str = list.get(0); // 타입 캐스팅 불필요
        System.out.println(str);
    }
}

 


4. 제네릭 클래스란 무엇인가요?

답변: 제네릭 클래스는 클래스 정의 시 타입 매개변수를 사용하는 클래스입니다. 다양한 데이터 타입에 대해 하나의 클래스를 사용할 수 있습니다.

예제 코드:

// 제네릭 클래스 정의
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()); // 출력: Hello

        Box<Integer> intBox = new Box<>();
        intBox.setItem(123);
        System.out.println(intBox.getItem()); // 출력: 123
    }
}

 


5. 제네릭 메서드란 무엇인가요?

답변: 제네릭 메서드는 메서드 선언부에 타입 매개변수를 선언하여, 메서드 호출 시 타입을 지정할 수 있게 합니다.

예제 코드:

public class GenericMethodExample {
    // 제네릭 메서드 정의
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        String[] stringArray = {"A", "B", "C"};
        Integer[] intArray = {1, 2, 3};

        printArray(stringArray); // T는 String으로 결정
        printArray(intArray);    // T는 Integer로 결정
    }
}

 


6. 와일드카드란 무엇인가요?

답변: 와일드카드(?)는 제네릭에서 특정 타입이 아닌 어떤 타입이라도 허용하기 위해 사용됩니다.

주요 유형:

  1. <?> (Unbounded Wildcard): 모든 타입 허용.
  2. <? extends Type> (Upper Bounded Wildcard): 특정 타입과 그 하위 클래스만 허용.
  3. <? super Type> (Lower Bounded Wildcard): 특정 타입과 그 상위 클래스만 허용.

예제 코드:

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

public class WildcardExample {
    // Unbounded Wildcard
    public static void printList(List<?> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }

    // Upper Bounded Wildcard
    public static void printNumbers(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        printList(stringList); // 모든 타입 가능

        List<Integer> intList = new ArrayList<>();
        intList.add(123);
        printNumbers(intList); // Number와 하위 클래스만 가능
    }
}

 


7. 제네릭에서 T, E, K, V는 무엇인가요?

답변: 제네릭 타입 매개변수 이름은 관례에 따라 다음과 같이 사용됩니다:

  • T (Type): 임의의 데이터 타입.
  • E (Element): 컬렉션에서 요소를 나타낼 때 사용.
  • K, V: 키(Key)와 값(Value)을 나타낼 때 사용 (예: Map).
  • ?: 와일드카드 (특정 타입이 아닌 모든 타입).

예제 코드:

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("Age", 25);
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
    }
}

 


8. 제네릭의 장점과 제한 사항은 무엇인가요?

장점:

  1. 타입 안정성: 컴파일 단계에서 타입 검사로 오류 방지.
  2. 코드 재사용성: 다양한 타입을 처리하는 하나의 클래스/메서드 작성 가능.
  3. 가독성 향상: 코드 명확성과 유지보수성 증가.

제한 사항:

  1. 기본 타입 사용 불가: 제네릭은 참조 타입만 지원합니다. (예: int 대신 Integer 사용)
  2. 런타임에 타입 정보 소거(Type Erasure): 실행 시 타입 정보는 제거되어 특정 타입 확인 불가.
  3. 정적 컨텍스트에서 타입 매개변수 사용 불가: 정적 메서드나 변수에서는 제네릭 타입 사용 불가.

9. 초보자가 많이 하는 실수는 무엇인가요?

  1. 타입 매개변수와 일반 타입 혼동:대신:
  2. Box<Integer> box = new Box<>();
  3. Box<int> box = new Box<>(); // 잘못된 코드 (기본 타입 불가)
  4. 와일드카드 잘못 사용:
  5. List<?> list = new ArrayList<>(); list.add("Hello"); // 컴파일 에러 (쓰기 불가)

10. Type Erasure의 한계점은 무엇인가요?

  • 타입 소거로 인해 런타임에 타입 정보가 사라져 리플렉션으로 구체적인 타입을 알 수 없습니다. 이를 해결하려면 Class<T>와 같은 방식을 사용해야 합니다.

11. 왜 Java는 공변성(covariance)와 반공변성(contravariance)을 지원하지 않나요?

  • Java는 명확성과 안전성을 위해 배열에서는 공변성을 지원하지만, Generics에서는 런타임 오류를 방지하기 위해 불변성(invariance)을 유지합니다.

12. 와일드카드와 타입 파라미터의 차이는 무엇인가요?

  • 와일드카드는 불특정 타입을 표현하며 읽기 전용으로 주로 사용됩니다. 반면 타입 파라미터는 더 구체적으로 제네릭 메서드나 클래스에서 특정 타입으로 제한됩니다.

13. Generics와 리플렉션을 함께 사용할 수 있나요?

  • Generics는 타입 소거를 사용하므로 리플렉션에서 원시 타입만 확인할 수 있습니다. 이를 해결하려면 TypeToken 또는 ParameterizedType을 활용해야 합니다.

14. Generics는 성능에 영향을 미치나요?

  • Generics는 컴파일 타임에 타입을 체크하고 런타임에는 타입 소거로 동작하기 때문에 성능에 큰 영향을 미치지 않습니다. 다만, 기본 타입 대신 Wrapper Class를 사용할 때 박싱/언박싱으로 성능 저하가 발생할 수 있습니다.

15. Java의 Collections Framework에서 Generics는 어떻게 활용되나요?

  • Java의 List, Set, Map과 같은 컬렉션 인터페이스는 모두 Generics를 활용하여 타입 안정성을 제공합니다. 예를 들어, List<String>은 문자열만 저장할 수 있도록 보장합니다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함