JAVA/BASIC

JAVA Generic 제네릭 (1) / 제네릭 타입, 멀티 타입 파라미터, 제네릭 메소드

유혁스쿨 2023. 3. 10. 11:07
728x90
반응형

Generic이란

Java 5 부터 추가된 문법으로, 잘못된 타입이 사용될 수 있는 문제를 컴파일러 과정에서 제거할 수 있게 한다.

컬렉션, 람다식, 스트림, NIO에서 널리 사용된다.

API Documents에는 제네릭 표현이 많기 때문에 제네릭을 정확히 이해해야만 API Docs를 정확히 이해할 수 있다.

클래스와, 인터페이스 그리고 메소드를 정의할 때 타입(Type)을 파라미터(Parameter)로 사용할 수 있도록 한다.

타입파라미터는 코드 작성시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해준다.

 

컴파일 시 강한 타입체크

자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제너릭 코드에 대해 강한 타입체크를 한다.

실행시 타입 에러가 나지 않도록 컴파일 시 미리 타입을 강하게 체크하여 에러를 사전에 방지한다.

 

타입 변환(Casting) 제거

비 제네릭 코드는 불필요한 타입 변환을 하므로 프로그램 성능에 악영향을 미친다.

List list = new ArrayList();
list.add("hello");
String str = list.get(0);

List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String 타입으로 변환해야 한다.

List list = new ArrayList();
list.add("hello");
String str = (String)list.get(0);

다음과 같이 제네릭 코드로 수정하면 

List<String> list = new ArrayList<String>(); // new 인스턴스<생략 가능> ex) new ArrayList<>();
list.add("hello");
String str = list.get(0);

제네릭 타입(Class<T>, Interface<T>)

제너릭 타입이란 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다.

클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다.

public class 클래스명 <T> {...}
public interface 인터페이스명 <T> {...}
/* 타입파라미터 이름은 T 이다. */

타입 파라미터는 일반적으로 대문자 알파벳 한글자로 표현한다.

제네릭 타입을 실제 코드에 사용하려면 타입 파라미터에 구체적인 타입을 지정해야한다.

public class Box {
    private Object object;
    public Object get() {
        return object;
    }
    public void set(Object object) {
        this.object = object;
    }
}

Box 클래스의 필드 타입을 Ojbect 타입으로 선언한 이유는 필드에 모든 종류의 객체를 저장하기 위함이다.

Object 클래스는 모든 자바의 최상위 조상(부모) 클래스이므로 모든 자바 객체는 Object타입으로 자동 타입변환되어 저장된다.

Object object = Java의 모든 Object;

set() 메소드를 통해 저장된 객체를 get() 메소드를 통해 Object 타입으로 반환받을 수 있다.

이때, 다음과 같은 강제 타입변환을 필요로 한다.

Box box = new Box();
box.set("hello"); // String -> Object 자동 타입 변환
String str = (String)box.get(); // Object -> String 강제 타입 변환

이와같이 Object 타입을 사용하면 모든 종류의 자바 객체를 저장할 수 있다는 장점이 있는 반면, 저장시에 자동으로 타입변환이 발생하고, 읽어올 때에는 강제로 타입을 변환을 필요로 한다.

타입 변환이 빈번해지면 전체 프로그램 성능에 좋지 못한 결과를 가져올 수 있다.

class Box <T> {
    private T t;
    public T get() {
        return t;
    }
    public void set(T t) {
        this.t = t;
    }
}

타입 파라미터 T 사용으로 Object 타입을 모두 T로 대체했다.

T는 Box클래스로 객체를 생성할 때 구체적인 타입으로 변경된다.

Box<String> box = new Box<>();

타입 파라미터 T는 String 타입으로 변경되어 Box 클래스의 내부는 다음과 같이 재구성된다.

public class Box<String> {
    private String t;
    public void set(String t) {
        this.t = t;
    }
    public String get() {
        return t;
    }
}

필드 타입이 T에서 String으로 변경되었고 set() 메소드도 String 타입만 매개값으로 받을수 있게 변경되었다.

get() 메소드 역시 String 타입으로 리턴하도록 변경되었다.

이는 저장할 때와 읽어올 때 타입 변환이 전혀 발생되지 않게 된다.

Box<String> box = new Box<>();
box.set("hello"); //<String> 으로 인해 자동 형변환 X
String str = box.get(); //<String> 으로 인해 강제 형변환 X

int에 대한 객체타입인 Integer 으로 Box객체를 생성해보자.

Box<Integer> box = new Box<>();

타입 파라미터 T는 Integer 타입으로 변경되어 Box 클래스는 내부적으로 다음과 같이 자동으로 재구성된다.

public class Box<Integer> {
    private Integer t;
    public void set(Integer t) {
        this.t = t;
    }
    public Integer get() {
        return t;
    }
}

다음 코드를 보면 저장할 때와 읽어올 때 Integer Wrapper클래스의 AutoBoxing, AutoUnboxing만 발생할 뿐 전혀 타입변환이 발생하지 않는다.

Box<Integer> box = new Box<>();
box.set(6);// int(6) -> Integer(6)자동 Boxing
int value = box.get();// Integer(6) -> int(6) 자동 Unboxing

이와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입으로 지정함으로써 타입 변환을 최소화 시킨다.


멀티 타입 파라미터(Class<K,V, ...> , Interface<K,V, ...>)

제네릭 타입은 두개 이상의 멀티 타입 파라미터를 사용할 수 있다.

각 타입 파라미터는 콤마로 구분한다.

 

Product<T, M> 제네릭 타입 클래스를 정의한다.

class Product <T, M> {
    private T kind; //종류
    private M model; //모델

    public T getKind() {return kind;}
    public void setKind(T kind) {this.kind = kind;}

    public M getModel() {return model;}
    public void setModel(M model) {this.model = model;}
}

 

public class Example {
    static class Tv {}
    static class Car {}
    public static void main(String[] args) {
    /* Logic - 1 */
    }
}

main메소드 Logic - 1

Product<Tv, String> 객체와 Product<Car, String> 객체를 생성한 뒤 set을 통해 값을 주입하고 get을 통해 값을 추출한다.

/* Logic - 1 */

Product<Tv, String> product1 = new Product<>();
product1.setKind(new Tv());
product1.setModel("스마트 TV");
Tv tv = product1.getKind();
String tvModel = product1.getModel();

Product<Car, String> product2 = new Product<>();
product2.setKind(new Car());
product2.setModel("과학5호기");
Car car = product2.getKind();
String carModel = product2.getModel();

객체 생성과 동시에 Product 클래스는 각각 다음과 같이 재구성 된다.

class Product <Car, String> {
    private Car kind; //종류
    private String model; //모델

    public Car getKind() {return kind;}
    public void setKind(Car kind) {this.kind = kind;}

    public String getModel() {return model;}
    public void setModel(String model) {this.model = model;}
}
class Product <Tv, String> {
    private Tv kind; //종류
    private String model; //모델

    public Tv getKind() {return kind;}
    public void setKind(Tv kind) {this.kind = kind;}

    public String getModel() {return model;}
    public void setModel(String model) {this.model = model;}
}

제너릭 메소드(<T, R> R method (T t))

제너릭 메소드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메소드를 말한다.

선언 방법은 리턴타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술한 다음, 리턴타입과 매개타입으로 타입파라미터를 사용한다.

public<타입파라미터, ...> 리턴타입 메소드명(매개변수, ...) {}
public<T> Box<T> boxing(T t){}

지정한 타입 파라미터는 매개변수의 타입과 리턴타입의 타입에서 사용할 수 있다.

 

제네릭 메소드는 두 가지 방식으로 호출한다.

코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정한다.

만약 구체적인 타입을 생략한다면, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하게 된다.

Box<Integer> box = <Integer>boxing(100); // 타입 파라미터를 명시적으로 Integer로 지정
Box<Integer> box = boxing(100); // 타입 파라미터를 Integer로 추정

 

아래와 같이 예제 코드를 구성해본다.

Util 클래스에 정적 제네릭 메소드로 Boxing() 을 정의하고 Example 클래스에서 호출한다.

 

Util.java

public class Util {
    public static <T> Box<T> boxing(T t){
        Box<T> box = new Box();
        box.set(t);
        return box;
    }
}

 

Example.java

public class Example {
    public static void main(String[] args) {
        Box<Integer> box1 = Util.<Integer>Boxing(100);
        int intValue = box1.get();
        Box<String> box2 = Util.Boxing("홍길동");
        String strValue = box2.get();
    }
}

box1 객체는 Integer를 명시적으로 타입파라미터로 지정하였지만, box2 객체는 타입 파라미터를 String으로 추정하게된다.

 

다음 예제는 Util클래스에 정적 제네릭 메소드로 compare()를 정의하고 Example클래스에서 호출하는 예제이다.

값이 동일한지 비교하고 boolean값을 리턴하도록 로직을 작성한다.

 

[Pair.java]

public class Pair<K, V> {
    private K key;
    private V value;
	// 생성자
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
	// setter getter
    public K getKey() {return key;}
    public void setKey(K key) {this.key = key;}
    public V getValue() {return value;}
    public void setValue(V value) {this.value = value;}
}

[Util.java]

public class Util {
    public static <K, V> boolean comapre(Pair<K, V> p1, Pair<K, V> p2) {
        boolean keyCompare = p1.getKey().equals(p2.getKey());
        boolean valueCompare = p1.getValue().equals(p2.getValue());
        return keyCompare && valueCompare;
    }
}

[Example.java]

public class Example {
    public static void main(String[] args) {
        Pair<Integer, String> p1 = new Pair(1, "사과");
        Pair<Integer, String> p2 = new Pair(1, "사과");
        boolean result1 = Util.<Integer, String>comapre(p1, p2); // 구체적인 타입 명시적 지정
        if(result1) {
            System.out.println("논리적으로 동등한 객체이다.");
        } else {
            System.out.println("논리적으로 동등하지 않는 객체이다.");
        }
        
        Pair<String, Integer> p3 = new Pair("hong", "홍길동");
        Pair<String, Integer> p4 = new Pair("hong", "홍길동");
        boolean result2 = Util.comapre(p3, p4); // 구체적인 타입 추정
        if(result2) {
            System.out.println("논리적으로 동등한 객체이다.");
        } else {
            System.out.println("논리적으로 동등하지 않는 객체이다.");
        }
    }
}

Util.java에서 compare() 메소드의 타입 파라미터는 K와 V로 선언되었는데, 리턴타입인 제네릭타입 Pair가 K와 V를 가지고 있기 때문이다.

이와같이 제네릭 메소드의 타입파라미터는 리턴타입이 제네릭 타입인경우 제네릭타입의 타입으로 지정할 수 있다.


[번외]

현업에서 다음과 같은 형태의 코드를 본 적이 있다.

@Service
public class Service {

    public <T, E> List<E> listJSON(final Map<String, Object> param) throws Exception {
        return mapper.list(param);
    }

}

 

해당 메소드를 호출하는 컨트롤러 코드이다.

@Controller
public class exampleController {

    @Autowired private Service service;
    
    @ResponseBody
    @RequestMapping(value = "listJSON")
    public List<Map<String, Object>> listJSON(final @RequestParam Map<String, Object> param) throws Exception {
        return service.listJSON(param);
    }
}

리턴타입인 List의 제네릭타입은 Map<String, Object> 이다.

Service 클래스의 listJson() 메소드에 파라미터로 Map을 넘겼기 때문에 다음과 같이 다시 작성할 수 있다.

@Service
public class Service {

    public <T, E> List<E> listJSON(final T param) throws Exception {
        return mapper.list(param);
    }

}

여기서 E는 리턴타입의 Map<String, Object>를 가르키기 때문에 멀티 타입 파라미터 E를 단일 T 1개로 공유할 수 있다.

@Service
public class Service {

    public <T> List<T> listJSON(final T param) throws Exception {
        return mapper.list(param);
    }

}

그렇다면 왜 굳이 T, E 2개의 멀티타입 파라미터를 사용했는가? 라는 의문을 가질 수 있는데, 그저 구분을 짓기 위함이 아닐까 생각이 든다.

728x90
반응형