JAVA Generic

김하영·2021년 1월 11일
0
post-thumbnail

토픽 : 자바 Generic
자바 Generic 의 개념
자바 Generic 의 사용 사례
자바 Generic 에 대한 사용 경험

자바 Generic 의 개념

- 제네릭(Generic) 이란?

제네릭(Generic)은 JDK1.5에 도입된 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

- 제네릭을 사용해야하는 이유

제네릭을 사용하면 잘못된 타입이 사용될 수 있는 문제를 컴파일 과정에서 제거할 수 있다.
자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 대해 강한 타입 체크를 한다. 실행 시 타입 에러가 나는것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다. 또 제네릭 코드를 사용하면 타입을 국한하기 떄문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상되는 효과를 얻을 수 있다.

ArrayList list = new ArrayList(); //제네릭을 사용하지 않을경우
list.add("test");
String temp = (String) list.get(0); //타입변환이 필요함
        
ArrayList<String> list2 = new ArrayList(); //제네릭을 사용할 경우
list2.add("test");
temp = list2.get(0); //타입변환이 필요없음

자바 Generic 의 사용 방법

- 제네릭 사용법

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.
제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 < > 부호가 붙고 사이에 타입 파라미터가 위치한다.

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}
  • 타입 파라미터는 정해진 규칙은 없지만 일반적으로 대문자 알파벳 한글자로 표현한다.

- 제네릭 클래스

class ExClassGeneric<T> {
    private T t;

    public void setT(T t) {
        this.t = t;
    }
			
    public T getT() {
        return t;
    }
}

위와 같이 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 넣어두었다가 실제 설계한 클래스가 사용되어질 때 ExClassGeneric<String> exGeneric = new ExClassGeneric<>(); 이런식으로 구체적인 타입을 지정하면서 사용하면 타입 변환을 최소화 시킬 수 있다.

- 제네릭 인터페이스

interface ExInterfaceGeneric<T> {
    T example();
}

class ExGeneric implements ExInterfaceGeneric<String> {

    @Override
    public String example() {
        return null;
    }
}

인터페이스도 위와 같이 클래스처럼 제네릭으로 설정해두고 활용할 수 있다.

- 멀티 타입 파라미터 사용

class ExMultiTypeGeneric<K, V> implements Map.Entry<K,V>{

    private K key;
    private V value;

    @Override
    public K getKey() {
        return this.key;
    }

    @Override
    public V getValue() {
        return this.value;
    }

    @Override
    public V setValue(V value) {
        this.value = value;
        return value;
    }
}

타입은 두개 이상의 멀티 타입 파라미터를 사용할수 있고 이 경우 각 타입 파라미터를 콤마로 구분한다.

- 제네릭 메소드

class People<T,M>{
    private T name;
    private M age;
	
    People(T name, M age){
        this.name = name;
        this.age = age;
    }

    public T getName() {
        return name;
    }
    public void setName(T name) {
        this.name = name;
    }
    public M getAge() {
        return age;
    }
    public void setAge(M age) {
        this.age = age;
    }
	
    //Generic Mothod
    public static<T,V> boolean compare(People<T,V>p1, People<T,V>p2) {
        boolean nameCompare = p1.getName().equals(p2.getName());
        boolean ageCompare =p1.getAge().equals(p2.getAge());
        return nameCompare && ageCompare;
    }
}

public class ExGeneric {
    public static void main(String []args){
        //타입 파라미터 지정
        People<String,Integer> p1 = new People<String,Integer>("Jack",20);
        //타입 파라미터 추정
        People<String,Integer> p2 = new People("Steve",30);
        //GenericMothod 호출
        boolean result = p1.compare(p1,p2);
        System.out.println(result);
    }
}

Class에 제너릭 타입을 선언하지 않고, 각 메소드마다 제너릭 타입을 선언해 사용할수 있다.
메소드의 파라미터의 <T> 이 선언되어 있다면, 리턴타입 바로앞에 <T> 제너릭 타입을 선언해주어야한다.

※ 메소드의 파라미터에 <T> 가 선언되어 있다면, ReturnType 앞에 <T> 를 선언하자.

제네릭 메서드를 정의할때는 리턴타입이 무엇인지와는 상관없이 내가 제네릭 메서드라는 것을 컴파일러에게 알려줘야하기 때문이다.

그리고 중요한 점이 제네릭 클래스가 아닌 일반 클래스 내부에도 제네릭 메서드를 정의할 수 있다.
그 말은, 클래스에 지정된 타입 파라미터와 제네릭 메서드에 정의된 타입 파라미터는 상관이 없다는 것이다.
즉, 제네릭 클래스에 를 사용하고 같은 클래스의 제네릭 메서드에도 로 같은 이름을 가진 타입파라미터를 사용하더라도 둘은 전혀 상관이 없다.

- 제네릭 와일드 카드

public class Calcu {
    public void printList(List<?> list) {
       for (Object obj : list) {
    	   System.out.println(obj + " ");  
       }
    }

    public int sum(List<? extends Number> list) {
      int sum = 0;
      for (Number i : list) {
    	  sum += i.doubleValue();  
      }
      return sum;
    }

   public List<? super Integer> addList(List<? super Integer> list) {
      for (int i = 1; i < 5; i++) {
    	 list.add(i); 
      }
      return list;
    }
}

와일드카드 타입에는 총 세가지의 형태가 있으며 물음표(?)라는 키워드로 표현된다.

  1. 제네릭타입<?> : 타입 파라미터를 대치하는 것으로 모든 클래스나 인터페이스타입이 올 수 있습니다.

  2. 제네릭타입<? extends 상위타입> : 와일드카드의 범위를 특정 객체의 하위 클래스만 올 수 있습니다.

  3. 제네릭타입<? super 하위타입> : 와일드카드의 범위를 특정 객체의 상위 클래스만 올 수 있습니다.

제네릭으로 사용될 타입 파라미터의 범위를 제한할 수 있는 방법이 있다.

- 한정적 타입 매개변수 (Bounded Type Parameter)

만약 GenericArrayList 가 Number 의 서브클래스만 타입으로 가지도록 하고 싶은 경우 아래와 같이 제네릭의 타입을 제한할 수 있다.

public class GenericArrayList<T extends Number>

위와 같이 정의했다면 GenericArrayList 에는 String 을 담을 수 없다.

Number 의 상위클래스만 타입으로 가지도록 하고 싶은 경우 아래와 같이 제네릭의 타입을 제한할 수 있다.

public class GenericArrayList<T super Number>

바운디드 타입 파라미터가 사용되는 가장 흔한 예시는 Comparable 을 적용하는 경우다.
T extends Comparable 와 같이 정의하면 Comparable 인터페이스의 서브클래스들만 타입으로 사용하겠다는 것이다.
Comparable 인터페이스를 구현하기 위해서는 compareTo() 메소드를 반드시 정의해야하기 때문에 Comparable 인터페이스를 구현한 클래스들은 비교가 가능한 타입이 된다.

비교하는 로직이 들어간 클래스에는 비교가 가능한 타입들을 다루는 것이 맞을 것이다.
이를 강제하도록 할 수 있는게 바운디드 타입 파라미터이다.

- 제네릭의 공변성 / 반공변성

[ +T ] 공변성 : 타입 T를 확장한 타입에 대해서 허용

<T extends Number>

[ T ] 무공변성 : 타입 T만 허용

[ -T ] 반공변성: 타입 T의 상위(부모) 타입에 대해서 허용

<T super Number>

자바 Generic 의 사용 사례 / 경험

현재 검색 API에서 다양한 검색 타입에 맞춰, 제네릭 메소드를 사용하고 있다.

  • 제네릭 메소드 호출
return this.search(requestVo, new TypeReference<AISearchResponse<AISearchResultAllVo>>(){});
  • 제네릭 메소드
private <T> T search(AISearchRequestVo requestVo, TypeReference typeReference) {

	T response;

...

response = mapper.readValue(commonApiResponse.getData(), typeReference);

...

return response;

}

매개변수로 넘어온 typeReference에 따라 재네릭 타입으로 선언된 response가 컴파일 시점에 타입이 정의된다.

실제로 검색 타입이 통합검색,곡,앨범,아티스트 등 6개 정도가 정의되어 search라는 제네릭 메소드를 호출하는데 만약 제네릭 메소드를 사용하지 않았더라면 각 검색 타입에 따른 메소드를 하나씩 생성하거나 공통 타입으로 search의 공통 리턴 타입을 정의하고 하나씩 타입변환을 하는 번거로운 작업이 필요했다.

제네릭이 JAVA1.5의 핵심이라는 이유를 알겠다!

  • 질문
    제네릭을 실무에서도 쓰고 기본적인 개념들은 이해가 가는 정도라 따로 질문이 없습니다~
    혹시 제가 정리한 내용외에 제네릭 관련하여 더 알아봤으면 하는게 있으실까요??
profile
Back-end Developer

1개의 댓글

comment-user-thumbnail
2021년 6월 16일

자바 Generic 의 사용 사례 / 경험이 이해가잘안되유 ㅠㅠㅠ

답글 달기