웨러미닛.. 이게 모지..

오늘도 즐겁고 기쁜 마음으로 제네릭에 대해 포스팅 하고자 한다. 내용이 많으니까 2부에 걸쳐 올리려고 한다.

 

제네릭이 필요한 이유

매그네릭.. 아니 제네릭이 필요한 이유를 설명하기 전, 예시를 통해 이해해보자

예를들어, 아래와 같이 단순한 기능을 제공하는 코드가 두개 있다고 가정하겠다

단순하게, integer을 저장하고 꺼내거나 String을 저장하고 꺼내는 기능

그리고 아래와 같이 Main 코드를 작성해보겠다

public class BoxMain {
    public static void main(String[] args) {

        IntegerBox integerBox = new IntegerBox();
        integerBox.set(10); // == (Integer integerBox = Integer.valueOf(10)); -> 오토 박싱
        Integer integer = integerBox.get();
        System.out.println("integer = " + integer);

        StringBox stringBox = new StringBox();
        stringBox.set("hello");
        String str = stringBox.get();
        System.out.println("str = " + str);


    }
}

각각의 박스들은, 자신의 타입에 맞는 데이터들만 보관 할 수 있다.

근데 만약 내가 Double형의 박스도 필요하고, Boolean형의 박스도 필요하고, Long형의 박스도 필요하다면, 첫번째 사진의 코드처럼 계속 타입에 맞게 클래스를 작성하여 사용해야한다. -> 즉 코드를 중복해서 계속 작성해야하는 코드 재사용성이 매우 떨어지게 된다.

우리는 짧고, 굵은 코드를 짤 생각을 해야지 한컴타자 연습을 하려고 하는게 아니지 않은가?

그렇다면 코드 재사용성을 높이려면 어떻게 해야할까?


다형성을 통한 중복 해결

머리가 파바박 돌어간다면, 모든 클래스의 조상님인 Object 타입으로 만들면 해결 할 수 있지 않을까? 생각이 들었을 것이다.

나도 정확히 이 생각을 먼저 해서, 아래와 같이 수정을 해보았다.

// 다형성을 통해 중복성 해결 시도
public class ObjectBox {

    private Object value;

    public void set(Object object) {
        this.value = object;
    }

    public Object get() {
        return value;
    }
}

자, 이제 우리는 그냥 이제 이 클래스를 호출해서, 원하는 타입의 박스을 만들고 그 안에 타입에 대한 값들로 채울 생각에 싱글벙글 하면 된다(사실 아님ㅋㅋ).

//Object 클래스로 해결 시도
public class BoxMain2 {

    public static void main(String[] args) {

        ObjectBox integerBox = new ObjectBox();
        integerBox.set(10);
        Integer integer = (Integer) integerBox.get(); //Object -> Integer 캐스팅
        System.out.println("integer = " + integer);

        ObjectBox stringBox = new ObjectBox();
        stringBox.set("hello");
        String str = (String) stringBox.get(); //Objext -> String 캐스팅
        System.out.println("str = " + str);

        //잘못된 타입의 인수 전달시
        integerBox.set("문자100");
        Integer result = (Integer) integerBox.get();  //String -> Integer 캐스팅 안됨 -> 들어있는것은 String인데 Integer로 변환 시도
        System.out.println("result = " + result);
    }
}

아 얼마나 아름답고 깔끔하게 해결 했는가. 이제 책 덮고 누워 잘생각에 싱글벙글했던 나였다.

하지만, 세상은 그리 호락호락 내 뜻대로 흘러가지 않았다.


문제점

다형성으로 문제를 해결하려고 한 시도는 좋았지만, 몇가지 문제점이 존재했다.

1. Object의 get을 통한 리턴 값은 Object 타입으로 나온다

예를 들어, ObjectBox를 통해 integerBox를 만들어 정수를 저장해놓았을 때, get을 통해 데이터를 반환 받을 때 데이터는 항상 Object 타입으로 반환 된다.

즉 값을 리턴 받기 위해서는 반드시 다운캐스팅을 해줘야한다는 문제점이 발생한다.

2. 잘못된 타입의 인수가 전달 될 수 있다

Object는 모든 타입을 받을 수 있다. 

즉, 우리는 integerBox에는 integer만 저장하려고 했는데, String타입의 데이터도 저장이 될 수 있다 -> 캐스팅 에러 발생

 

우리는, 다형성을 통해 코드의 재사용성은 높였지만, 타입을 다운 캐스팅해서 반환 받거나, 다른 타입이 들어갈 수 도 있는 타입 안전성이 상당히 낮은 코드가 만들어진 것이다.

반면에, 제일 처음 만들었던 예시인, 각 타입마다 Box를 만들어준 방법은 정해 놓은 타입만 받을 수 있기에, 타입 안전성이  매우 높지만, 코드의 중복성이 높다는 문제점이 존재 했다.

여태 우리가 한 것은 타입 안전성  , 코드 재사용성을 모두 충족하지 않는 프로그램을 만든 것이다..

그럼 방법이 없는걸까.. 그냥 접어야 할까..? 만약 여기서 접을거였으면 이 포스팅을 쓰는 이유도 없었을 것이다ㅋㅋ

다 방법이 있다 이말이야


제네릭 도입

자 서론이 참 길었다.. 우리의 주목적은 코드의 재사용성과 타입 안전성 두마리 토끼를 모두 잡는 것이 목적이다.

바로 코드를 작성해보겠다.

public class GenericBox<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

<> (다이아몬드) 기호를 사용한 클래스를 제네릭 클래스라고 칭한다. 제네릭 클래스의 특징은 아래와 같다.

1. 제네릭 클래스 사용시, Integer, Long, String 과 같은 타입을 미리 지정하지 않는다.

2. 대신에 클래스 명 옆 <> 기호 안에 T(이것을 타입 매개변수라고 한다) 와 같이 선언하면, 제네릭 클래스가 된다.

3. 인스턴스를 생성할 때, <> 기호 안에 원하는 타입을 대입하여 동적으로 타입을 결정할 수 있다.

4. 클래스 내부에서는 T타입이 필요한 곳에 T value와 같이 타입 매개변수를 적어두면 된다.

자, 이제 이러한 특징들을 가진 제네릭 타입을 통해서 일타 쌍피를 잡으로 가보자.

 

public class BoxMain3 {

    public static void main(String[] args) {

        GenericBox<Integer> integerBox = new GenericBox<Integer>(); // T = Integer
        integerBox.set(10);
        Integer integer = integerBox.get(); // Integer 타입 반환 => 무리한 캐스팅이 필요 없다
        System.out.println("integer = " + integer);

        GenericBox<String> stringBox = new GenericBox<String>();
        stringBox.set("siuuu");
        String string = stringBox.get();
        System.out.println("string = " + string);

        GenericBox<Double> doubleBox = new GenericBox<Double>();
        doubleBox.set(123.456);
        Double doubleValue = doubleBox.get();
        System.out.println("doubleValue = " + doubleValue);

        // 타입 추론
        GenericBox<Integer> integerBox2 = new GenericBox<>(); //타입 부분을 비워놔도 된다
    }
}

먼저 객체를 생성하는 시점에, <> 안에 타입을 지정하여 객체를 생성한다.(1단락: Integer, 2단락: String, 3단락: Double)

간단하게 Integer 타입으로 생성하면 어떤 일이 일어나는지 보자

public class GenericBox<Integer> {

    private Integer value;

    public void set(Integer value) {
        this.value = value;
    }

    public Integer get() {
        return value;
    }
}

위와 같이 타입 매개변수인 T로 설정해두었던 모든 부분이 Integer로 바뀐 것을 확인할 수 있다.

즉 우리는 이제, integerBox에는  int타입(사실은 Integer 타입인데 오토박싱, 언박싱 발생) 만 value에 저장할 수 있고, int 타입만 set 할 수 있으며, Integer 타입으로 값을 반환 받을 수도 있다.


코드 재사용성, 타입 안전성 문제 해결

즉, 우리는 동적으로 타입을 바꿔주어 타입 안전성 문제를 해결 할 수 있게 되었다.

또한, 우리는 객체를 생성할 때, <> 안에 타입을 지정해서 객체를 생성할 수 있게 되어 코드 중복성 문제도 해결할 수 있게 되었다!

 


제네릭을 적용한 Main 코드의 마지막 부분에서, 주석에 타입 추론이란 내용을 적어놨는데,

// 기존 코드
GenericBox<Integer> integerBox2 = new GenericBox<Integer>();
// 타입 추론
GenericBox<Integer> integerBox2 = new GenericBox<>(); //타입 부분을 비워놔도 된다

이와 같이 왼쪽 편에 타입을 통해, 오른쪽에 객체에는 타입 부분을 비워놔도 우리 똑똑한 자바가 알아서 다 ~ 처리해준다.


더 자세한건.. 2편에서 다루도록 하겠다!

일단 제네릭을 왜 쓰는지에 대해서만 그렇구나 생각하고, 2편에서 응용해보자!

그럼 오늘도 20000!