공부/Java

자바 제네릭(Generic) 1편

Stair 2024. 9. 23. 12:57
반응형

제네릭을 배우기에 앞서 다음과 같은 코드를 살펴보자

혹시라도 래퍼 클래스에 대해 잘 모른다면 이전 글을 참고하는게 좋다.

https://surrealcode.tistory.com/59

 

자바 래퍼 클래스(wrapper class)

기본형의 한계 1자바는 객체 지향 언어이다. 그런데 바자 안에 객체가 아닌 것이 있다. 바로 int, double 같은 기본형(Primitive Type)이다. 기본형은 객체가 아니기 때문에 다음과 같은 한계가 있다.1.

surrealcode.tistory.com

public class IntegerBox {
    private Integer value;

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

    public Integer get() {
        return value;
    }
}
public class StringBox {

    private String value;

    public String get() {
        return value;
    }

    public void set(String value) {
        this.value = value;
    }
}
public class BoxMain1 {

    public static void main(String[] args) {
        IntegerBox integerBox = new IntegerBox();
        integerBox.set(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);

    }
}

 

코드를 보면 먼저 숫자를 보관하는 IntegerBox를 생성하고, 그곳에 숫자 10을 보관 후 꺼낸다음 출력하였다.

(오토박싱에 의해 int형이 Integer로 자동 변환되어 문자가 담겼다)

다음으로 문자열을 보관하는 StringBox를 생성하고 그곳에 문자열 "hello"를 보관 후 출력했다.

 

문제 : 이후에 Double,Boolean을 포함한 다양한 타입을 담는 박스가 필요하다면 타입별로 여러 클래스를 새로 만들어야 한다. 담는 타입이 수십개라면 수십개의 박스를 만들어야 해야할까? 이것은 명백한 문제이다. 이 문제를 어떻게 해결할 수 있을까?

 

 

Object는 모든 타입의 부모이다 .Object를 사용해서 이 문제를 간단히 해결할 수 있을것 같다.

public class ObjectBox {

    private Object value;

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

    public Object get() {
        return value;
    }
}
public class BoxMain2 {

    public static void main(String[] args) {
        ObjectBox integerBox = new ObjectBox();
        integerBox.set(10);
        //Object -> Integer 캐스팅
        Integer integer = (Integer) integerBox.get();
        System.out.println("integer = " + integer);

        ObjectBox StringBox = new ObjectBox();
        StringBox.set("hello");
        //Object -> String 캐스팅
        String string = (String) StringBox.get();
        System.out.println("string = " + string);
    }
}

 

Object는 모든 타입의 부모이다. 그렇기에 Object를 활용하여 다운캐스팅을 진행해서 Integer와 String타입에 잘 맞춘것처럼 보인다.

하지만 이 코드에는 문제점이 있다.

일단 꺼낼때 다운캐스팅을 무조건 해줘야 한다는 문제이다.

 

또한 잘못된 타입의 인수를 전달 시에도 문제가 발생한다.

//잘못된 타입의 인수 전달 시
integerBox.set("문자100");
Integer result = (Integer)integerBox.get();
System.out.println("result = " + result);

개발자의 의도는 integerBox에는 변수 이름과 같이 숫자 타입이 입력되기를 기대했다.

하지만 set()메서드는 모든 타입의 부모인 Object를 매개변수로 받기 때문에 세상의 어떤 데이터도 입력받을 수 있다. 따라서 위처럼 문자열을 입력받아도 자바 언어 입장에선 잘못된 코드가 아니기에 아무런 문제가 되지 않는다.

 

따라서 잘못된 타입의 값을 전달하면 다운캐스팅하여 꺼낼때 문제가 발생한다.

 

-> 다형성을 활용한 덕분에 코드의 중복을 제거(StringBox,IntegerBox등을 안만들어도 됨)하고, 기존 코드를 재사용할 수 있게 되었다. 하지만 입력할 때 실수로 원하지 않는 타입이 들어갈 수 있는 타입 안정성의 문제가 발생했다.

 

지금까지 개발한 프로그램은 코드 재사용과 타입 안정성이라는 두마리 토끼를 한번에 잡을 수 없다.

 

BoxMain1: 코드 재사용X, 타입 안정성O

BoxMain2: 코드 재사용O, 타입 안정성X

 

 

이 문제를 해결하기 위해 제네릭이 적용된다.

제네릭을 사용하면 코드 재사용성타입 안전성이라는 두마리 토끼를 한번에 잡을 수 있다.

제네릭을 사용해서 문제를 해결해보자.

public class GenericBox<T> {
    
    private T value;
    
    public void set(T value){
        this.value = value;
    }

    public T get() {
        return value;
    }
}

1. <>를 사용한 클래스를 제네릭 클래스라 한다. 이 기호(<>)를 보통 다이아몬드라고 한다.

2. 제네릭 클래스를 사용할 때는 Integer, String같은 타입을 미리 결정하지 않는다.

3. 클래스 명 오른쪽에 <T>와 같이 선언하면 제네릭 클래스가 된다. 여기서 T를 타입 매개변수라 한다. 이 타입 매개변수는 이후에 Integer, String으로 변할 수 있다.

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

 

 

생성 시점에 원하는 타입을 지정한다.

제네릭 클래스는 다음과 정의한다. <>(다이아몬드)안에 타입 매개변수를 정의하면 된다.

class GenericBox<T>

 

제네릭 클래스는 생성하는 시점에 <>사이에 원하는 타입을 지정한다.

new GenericBox<Integer>()

 

이렇게 하면 앞서 정의한 GenericBox의 T가 다음과 같이 지정한 타입으로 변한 다음 생성된다.

public class BoxMain3 {

    public static void main(String[] args) {
        //생성 시점에 T의 타입이 결정됨
        GenericBox<Integer> integerBox = new GenericBox<>();
        integerBox.set(10);//Integer 타입만 허용, 컴파일 오류
        Integer integer = integerBox.get();
        System.out.println("integer = " + integer);

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

        //원하는 모든 타입 사용 가능
        GenericBox<Double> doubleBox = new GenericBox<>();
        doubleBox.set(10.5);
        Double double1 = doubleBox.get();
        System.out.println("double1 = " + double1);

    }
}

 

 

제네릭 클래스를 사용하면 다음과 같이 GenericBox 객체를 생성하는 시점에 원하는 타입을 마음껏 지정할 수 있다.

new GenericBox<Double>()

new GenericBox<Boolean>()

new GenericBox<MyClass>()

 

참고로 제네릭을 도입한다고 해서 앞서 설명한 GenericBox<String>과 같은 코드가 실제 만들어 지는 것은 아니다. 대신에 자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이러한 코드가 있다고 가정 후 컴파일시 타입 정보를 반영한다. 이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생한다.

 

 

타입 추론(생성하는 제네릭 타입 생략 가능)

Generic<Integer> integerBox = new GenericBox<Integer>()

Generic<Integer> integerBox = new GenericBox<>()

첫째 줄을 보면 변수를 선언할 때 <Integer>가 두번 나온다. 자바는 왼쪽에 있는 변수를 선언할 때의 <Integer>를 보고 오른쪽에 있는 객체를 생성할 때 필요한 타입 정보를 얻을 수 있다. 따라서 두 번째 줄의 오른쪽 코드 new의 GenericBox<>()와 같이 타입 정보를 생략할 수 있다. 이렇게 자바가 스스로 타입 정보를 추론해서 개발자가 타입 정보를 생략할 수 있는 것을 타입 추론이라 한다.

+ 타입 추론이 그냥 되는 것은 아니고, 자바 컴파일러가 타입을 추론할 수 있는 상황에만 가능하다. 쉽게 이야기해서 읽을 수 있는 타입 정보가 주변에 있어야 추론할 수 있다.

 

 

정리 - 제네릭을 사용한 덕분에 코드 재사용과 타입 안정성이라는 두 마리 토끼를 모두 잡을 수 있게 되었다.

 

 

제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다. 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 정의하는 것이 아니라 실제 사용하는 시점에 정의하는 것이다.

이것을 쉽게 비유하자면 메서드의 매개변수와 인자의 관계와 비슷하다

void method1(){

    println("hello");

}

위 코드는 hello라는 문자열만 출력하게 되는 재사용성이 떨어지는 코드이다 이 코드를

void method2(String param){

    println(param);

}

 

psvm(){

    method2("Hello");

    method2("Hi");

}

처럼 메서드에 피룡한 값을 인자를 통해 매개변수로 전달해서 결정하면, 메서드에 필요한 값을 메서드를 실제 사용하는 시점으로 미룰 수 있다.

이렇게 하면 이 메서드는 실행 시점에 얼마든지 다른 값을 받아 처리할 수 있어, 재사용성이 크게 늘어난다.

 

 

void method(String param) //매개변수

 

void main(){

    String arg = "Hello";

    method(arg) //인수 전달

}

매개변수 : param

인수, 인자 : arg

 

 

 

제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계처럼 작동한다.

제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하는 것이 아니라, 해당 클래스를 실제 사용하는 생성 시점에 내부에서 사용할 타입을 결정하는 것이다. 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다.

 

정리하면 다음과 같다.

1. 메서드는 매개변수인자를 전달해서 사용할 값을 결정한다

2. 제네릭 클래스는 타입 매개변수타입 인자를 전달해서 사용할 타입을 결정한다.

 

제네릭에서 사용하는 용어도 매개변수, 인자의 용어를 그대로 가져다 사용한다. 다만 값이 아니라 타입을 결정하는 것이기 때문에 앞에 타입을 붙힌다.

타입 매개변수 : T

타입 인자 : String, Integer 등

 

용어 정리

제네릭(Generic) : 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻이다.

제네릭 타입(Generic Type) : 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다. 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다. ex) class GenericBox<T>

타입 매개변수(Type Parameter) : 제네릭 타입이나 메서드에 사용되는 변수로, 실제 타입으로 대체된다. ex)  <T>

타입 인자(Type Argument) : 제네릭 타입을 사용할 때 제공되는 실제 타입이다. 여기에서 Integer or String or Double 등을 타입 인자라 한다.

 

제네릭 명명 관례

타입 매개변수는 일반적인 변수명처럼 소문자로 사용해도 문제는 없다.

하지만 일반적으로 대문자를 사용하고 용도에 맞는 단어의 첫글자를 사용하는 관례를 따른다

 

제네릭은 다음과 같이 한번에 여러 타입 매개변수를 선언할 수도 있다.

class Data<K, V>()

 

타입 인자로 기본형은 사용할 수 없다.

제네릭의 타입 인자로 기본형(int, double)은 사용할 수 없다. 대신에 래퍼 클래스를 사용하면 된다.

 

 

로 타입(Row Type)

GenericBox genericBox = new GenericBox();
genericBox.set(10);
Integer result =(Integer) genericBox.get();
System.out.println("result = " + result);

 

제네릭 타입을 사용할 때는 항상 <>를 사용해서 사용시점에 원하는 타입을 지정해야 한다.

그런데 다음과 같이 <>을 지정하지 않을 수 있는데, 이런 것을 로 타입(row type) 또는 원시 타입이라 한다.

원시 타입을 사용하면 내부의 타입 매개변수가 Object로 사용된다고 이해하면 된다.

자바의 제네릭이 자바 처음 등장할 때 부터 있었던 것이 아니라 자바가 오랜기간 사용된 이후 등장했기 때문에 제네릭이 없던 시절의 과거 코드와 하위 호환이 필요했다. 그래서 어쩔 수 없이 로 타입을 지원한다.

 

하지만 로 타입은 사용하지 말아야 한다.

 

 

 

이제 제네릭을 어떻게 사용하는지 알아보자.

public class Animal {

    private String name;
    private int size;

    public Animal(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public int getSize() {
        return size;
    }

    public void sound(){
        System.out.println("동물 울음 소리");
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", size=" + size +
                '}';
    }
}
public class Cat extends Animal{

    public Cat(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("냐옹");
    }
}
public class Dog extends Animal{


    public Dog(String name, int size) {
        super(name, size);
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

Animal을 상속 받는 Dog과 Cat이 있다고 가정하자.

 

그 상태에서 제네릭 클래스인 Box를 만들었다.

이 제네릭 클래스는 객체를 보관하고 꺼낼 수 있는 단순한 코드이다.

public class Box <T>{

    private T value;

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

    public T get() {
        return value;
    }
}

Box는 제네릭클래스이므로 Animal, Dog, Cat 같은 다양한 동물 모두를 담을 수 있다.

 

public class AnimalMain1 {

    public static void main(String[] args) {
        Animal animal = new Animal("동물", 0);
        Dog dog = new Dog("멍멍이", 100);
        Cat cat = new Cat("냐옹이", 50);

        Box<Dog> dogBox = new Box<>();
        dogBox.set(dog);
        Dog findDog = dogBox.get();
        System.out.println("findDog = " + findDog);

        Box<Cat> catBox = new Box<>();
        catBox.set(cat);
        Cat findCat = catBox.get();
        System.out.println("findCat = " + findCat);

        Box<Animal> animalBox = new Box<>();
        animalBox.set(animal);
        Animal findAnimal = animalBox.get();
        System.out.println("findAnimal = " + findAnimal);

    }
}
public class AnimalMain2 {

    public static void main(String[] args) {
        Animal animal = new Animal("동물", 0);
//        Dog dog = new Dog("멍멍이", 100);
        Cat cat = new Cat("냐옹이", 50);

        Box<Animal> animalBox = new Box<>();
//        animalBox.set(animal);
//        animalBox.set(dog);
        animalBox.set(cat);

        Animal findAnimal = animalBox.get();
        System.out.println("findAnimal = " + findAnimal);

    }
}

 

 

 

반응형