celina의 이것저것

자바에서 제네릭을 쓰는 이유? 본문

CS/JAVA

자바에서 제네릭을 쓰는 이유?

celinayk 2025. 6. 11. 21:45
반응형

 

문제1) 반복적인 코드 

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 void set(String object) {
        this.value = value;
    }
    public String get() {
        return 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);
    }
}

 

 

해결1)

"다형성"을 활용해보자 -> 모든 타입의 부모인 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);
        Integer integer = (Integer) integerBox.get(); //다운캐스팅
        System.out.println("integer = " + integer);

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

        integerBox.set("문자100");
        Integer result = (Integer) integerBox.get(); //String -> Integer 캐스팅 예외
        System.out.println("result = " + result);
    }
}

반복코드는 줄였으나 또 다른 문제가 발생한다. 

"반환 타입의 불일치"의 문제가 발생한다. 

integerBox의 타입은 Object이다. 그런데 integerBox.get()을 통해 반환받으려고 하는 타입은 Integer다.

그래서 반환을 받으려면 다운캐스팅을 해줘야한다. 

 

또 하나 문제가 있다.

"잘못된 타입의 인수 전달"의 문제가 발생한다.

일단 set 메서드는 Object타입이다. 즉 모든 타입이 가능하다.

나는 숫자 타입이 입력되길 바랐지만 누군가 문자를 입력해버려도 아무런 오류가 발생하지 않는다.

 

 

문제1: 코드 재사용 X ,타입 안전성 O

해결1: 코드 재사용 O, 타입 안전성 X

 

 



제네릭

public class GenericBox<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

제네릭 클래스가 등장한다.

제네릭 클래스란 <>를 사용한 클래스이다. 즉 타입을 미리 결정하지 않고 비워두는거다. 그래서 T라고 표기했다.

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

 

public class BoxMain3 {
    public static void main(String[] args) {
        GenericBox<Integer> integerBox = new GenericBox<Integer>();
        integerBox.set(10);
        Integer integer =  integerBox.get(); 
        System.out.println("integer = " + integer);

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

        GenericBox<Double> doubleBox = new GenericBox<Double>();
        doubleBox.set(10.5);
        Double doubleValue =  doubleBox.get(); //String -> Integer 캐스팅 예외
        System.out.println("result = " + doubleValue);
    }
}

이렇게 생성될때 각 타입을 정해주면, 아까 문제를 해결할 수 있다.

"반환 타입의 불일치" -> 생성되는 시점에 타입을 지정하므로 그 타입을 넣으면 되니까 반환타입 일치함

"잘못된 타입의 인수 전달" -> set메서드에 지정된 타입의 값만 넣을 수 있기때문에 오류를 방지할 수 있다

 

 

제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다.

 

 


V1 다형성 

public class AnimalHospitalV1 {

    private Animal animal;

    public void set(Animal animal) {
        this.animal = animal;
    }

    public void checkup() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public Animal getBigger(Animal target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

V0은 생략했지만 개병원,고양이병원 클래스를 각각 만드는것이다. 코드 재사용성이 떨어지기 때문에 다형성을 이용해서 Animal이라는 부모 타입으로 중복을 제거했다. 

 

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV1 {
    public static void main(String[] args) {
        AnimalHospitalV1 dogHospital = new AnimalHospitalV1();
        AnimalHospitalV1 catHospital = new AnimalHospitalV1();

        Dog dog = new Dog("멍멍이1", 100);
        Cat cat = new Cat("냐옹이1", 300);

        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();

        //개 병원에 고양이 전달 -> animal 타입이라서 컴파일 오류 발생안함
        dogHospital.set(cat);


        dogHospital.set(dog); // 개 타입으로 반환하려면 캐스팅 필요함
        Dog biggerDog = (Dog)dogHospital.getBigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " +biggerDog);
    }


}

하지만 여전히 두개의 문제가 발생한다.

"반환 타입의 불일치" 

"잘못된 타입의 인수 전달" 

 

 

 

V2 제네릭 도입

package generic.ex3;

public class AnimalHospitalV2<T> {

    private T animal;

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

    public void checkup() {
        animal.toString();
        animal.equals(null); //t의 타입을 메서드 정의시점에서는 알 수 없음 . 오브젝트의 기능만 사용가능함
    }

    public T getBigger(T target) {
        return null;
    }
}

뭔가 잘 해결된것같다. 코드 중복성도 없앴고, 다형성으로 인해 발생했던 두가지 문제도 해결한것같다.

하지만 또 다른 문제가 있다.

 

Object가 제공하는 메서드만 호출할 수 있다.

 

 

즉, 해당 클래스를 만들때 타입을 T로 지정했다. 자바는 T를 가장 최종 부모인 Object타입으로 지정하기 때문에 우리가 Animal타입에서 제공하는 메서드는 사용할 수 없다. 

 

두번째문제가 또 발생한다.

동물과 관계없는 타입이 인자로 전달될 수 있다. (어떤 타입이든 들어올 수 있다)

 

우리는 저기에 최소 Animal이나 그 자식의 타입이 들어가야한다는걸 안다. 개, 고양이 등등

하지만 T로 선언했고, Object, Integer와 같은 타입도 들어갈 수 있다.

 

 

 

V3 타입 매개변수 제한

우리가 원하는건 제네릭이지만 integer같은거 말고 Animal과 그 자식만 받고 싶다. T의 상한을 Animal로 제한하고 싶다.

package generic.ex3;

import generic.animal.Animal;

public class AnimalHospitalV3<T extends Animal> {

    private T animal;

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

    public void checkup() {
        System.out.println("동물 이름: " + animal.getName());
        System.out.println("동물 크기: " + animal.getSize());
        animal.sound();
    }

    public T getBigger(T target) {
        return animal.getSize() > target.getSize() ? animal : target;
    }
}

이렇게 extends로 제한을 할 수 있다.

 

package generic.ex3;

import generic.animal.Cat;
import generic.animal.Dog;

public class AnimalHospitalMainV3 {
    public static void main(String[] args) {
        AnimalHospitalV3<Dog> dogHospital = new AnimalHospitalV3();
        AnimalHospitalV3<Cat> catHospital = new AnimalHospitalV3();

        Dog dog = new Dog("멍멍이1", 100);
        Cat cat = new Cat("냐옹이1", 300);

        // 개 병원
        dogHospital.set(dog);
        dogHospital.checkup();

        // 고양이 병원
        catHospital.set(cat);
        catHospital.checkup();

        //개 병원에 고양이 전달
        //dogHospital.set(cat);

        dogHospital.set(dog); // 개 타입으로 반환하려면 캐스팅 필요함
        Dog biggerDog = dogHospital.getBigger(new Dog("멍멍이2", 200));
        System.out.println("biggerDog = " +biggerDog);
    }
}

그럼 아까 제네릭을 쓰면서 발생한 두가지 문제를 모두 해결할 수 있다.

Object가 제공하는 메서드만 호출할 수 있다.

어떤 타입이든 들어올 수 있다

이 두가지를 해결 가능하다. 

 

덕분에 코드 재사용과 타입 안전성 두가지 문제를 해결 할 수 있다. 

 

 

 

 


제네릭 메서드

앞서는 제네릭 타입이었다.

지금부터는 특정 메서드에 제네릭을 적용하는 경우이다.

 

 

정리

제네릭 타입: GenericClass<T> (객체 생성 시점에 타입 인자 전달됨)

제네릭 메서드: <T> T genericMethod(T t) (메서드 호출하는 시점에 타입 인자 전달됨)

 

제네릭 메서드는 특정 메서드 단위로 제네릭을 도입할 때 사용
핵심은 메서드를 실제 호출하는 시점에 타입을 정하고 호출한다. 
package generic.ex4;

public class GenericMethod {
    public static Object objMethod(Object obj) {
        System.out.println("object print:" + obj);
        return obj;
    }

    public static <T> T genericMethod(T t) {
        System.out.println("generic print: " + t);
        return t;
    }

    public static <T extends  Number> T numberMethod(T t) {
        System.out.println("boud print: "+ t);
        return t;
    }
}
package generic.ex4;

public class MethodMain1 {
    public static void main(String[] args) {
        Integer i = 10;
        Object object = GenericMethod.objMethod(i);

        System.out.println("명시적 타입 인자 전달");
        Integer result = GenericMethod.<Integer>genericMethod(i);
        Integer integerValue = GenericMethod.<Integer>numberMethod(10);
        Double doubleValue = GenericMethod.<Double>numberMethod(20.0);
    }
}

 

 

 

제네릭 메서드는 인스턴스 메서드와 static 메서드에 모두 적용할 수 있다.

 

참고

제네릭타입은 static 메서드에 타입매개변수 사용할 수 없다.

제네릭타입은 객체를 생성하는 시점에 타입이 정해지는데, static 메서드는 인스턴스 단위가 아니라 클래스 단위이기 때문에 제네릭 타입이랑 무관함. 따라서 staic 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 한다. 

 

 


제네릭 타입과 제네릭 메서드듸 우선순위

 

정적 메서드는 제네릭 메서드만 적용할 수 있지만, 인스턴스 메서드는 제네릭 타입, 제네릭 메서드 둘다 적용 가능

 

Comments