[Java] 자바 중급2 제네릭

wony·2024년 5월 13일

Java

목록 보기
17/30

개요

제네릭에 대해 알아보자!

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 = object;
    }
    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);
    }
}
  • 코드를 보면 먼저 숫자를 보관하는 IntegerBox 를 생성하고, 그곳에 숫자 10 을 보관하고, 꺼낸 다음에 출력했다. 참고로 오토 박싱에 의해 intInteger 로 자동 변환된다.
  • 다음으로 문자열을 보관하는 StringBox 를 생성하고 그곳에 문자열 "hello" 를 보관하고, 꺼낸 다음에 출력했다.

문제
이후에 Double , Boolean 을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 DoubleBox ,BooleanBox 와 같이 클래스를 새로 만들어야 한다. 담는 타입이 수십개라면, 수십개의 XxxBox 클래스를 만들어야 한다.

2. 다형성을 통한 중복 해결 시도

Object는 모든 타입의 부모이다. 따라서, 다형성을 사용해서 이 문제를 간단히 해결할 수 있다.

public class ObjectBox {

    private Object value;

    public void set(Object object) {
        this.value = object;
    }
    public Object get() {
        return value;
    }
}
  • 내부에 Object value를 가지고 있다. Object는 모든 타입의 부모이다. 부모는 자식을 담을 수 있으므로 세상의 모든 타입을 ObjectBox에 보관할 수 있다.
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(); //Object -> Integer 캐스팅
        System.out.println("str = " + str);
        //잘못된 타입의 인수 전달시
        integerBox.set("문자100");
        Integer result = (Integer) integerBox.get(); // String -> Integer 캐스팅 예외
        System.out.println("result = " + result);
    }
}
  • 잘 동작하는 것 같지만 몇 가지 문제가 있다.
  • 반환 타입이 맞지 않는 문제

먼저 IntegerBox를 만들어서 숫자 10을 보관했다. 숫자를 입력하는 부분에는 문제가 없어 보이지만, integerBox.get()을 호출할 때 문제가 나타난다.

  • integerBox.get()의 반환 타입은 Object이다.
  • Integer = Object 는 성립하지 않는다. 자식은 부모를 담을 수 없다. 따라서 다음과 같이 (Integer) 타입 캐스팅 코드를 넣어서 Object 타입을 Integer 타입으로 직접 다운 캐스팅해야 한다.
  • stringBox 의 경우도 마찬가지이다. stringBox.get()Object 를 반환하므로 다음과 같이 다운 캐스팅해야 한다.

정리
다형성을 활용한 덕분에 코드의 중복을 제거하고, 기존 코드를 재사용할 수 있게 되었다. 하지만, 입력할 때 실수로 원하지 않는 타입이 들어갈 수 있는 타입 안정성 문제가 발생한다. 예를 들어서 integerBox 에는 숫자만 넣어야 하고,stringBox 에는 문자열만 입력할 수 있어야 한다. 하지만 박스에 값을 보관하는 set() 의 매개변수가 Object 이기 때문에 다른 타입의 값을 입력할 수 있다. 그리고 반환 시점에도 Object 를 반환하기 때문에 원하는 타입을 정확하게 받을 수 없고, 항상 위험한 다운 캐스팅을 시도해야한다. 결과적으로 이 방식은 타입 안전성이 떨어진다.

지금까지 개발한 프로그램은 코드 재사용과 타입 안전성이라는 2마리 토끼를 한번에 잡을 수 없다. 코드 재사용을 늘리기 위해 Object 와 다형성을 사용하면 타입 안전성이 떨어지는 문제가 발생한다.

  • BoxMain1 : 각각의 타입별로 IntegerBox , StringBox 와 같은 클래스를 모두 정의
    • 코드 재사용
    • 타입 안전성O
  • BoxMain2 : ObjectBox 를 사용해서 다형성으로 하나의 클래스만 정의
    • 코드 재사용O
    • 타입 안전성X

3. 제네릭 적용

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

public class GenericBox<T> {

    private T value;

    public void set(T value) {
        this.value = value;
    }
    public T get() {
        return value;
    }
}
  • <> 를 사용한 클래스를 제네릭 클래스라 한다. 이 기호( <> )를 보통 다이아몬드라 한다.
  • 제네릭 클래스를 사용할 때는 Integer , String 같은 타입을 미리 결정하지 않는다.
    대신에 클래스명 오른쪽에 <T> 와 같이 선언하면 제네릭 클래스가 된다. 여기서 T타입 매개변수라 한다. 이 타입 매개변수는 이후에 Integer , String 으로 변할 수 있다.
  • 그리고 클래스 내부에 T 타입이 필요한 곳에 T value 와 같이 타입 매개변수를 적어두면 된다.
public class BoxMain3 {
    public static void main(String[] args) {
        // 생성 시점에 T의 타입 결정
        GenericBox<Integer> integerBox = new GenericBox<Integer>();
        integerBox.set(10);
        //integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류
        Integer integer = integerBox.get(); // Integer 타입 반환
        System.out.println("integer=" + integer);

        GenericBox<String> stringBox = new GenericBox<String>();
        stringBox.set("hello"); // String 타입만 허용
        String str = stringBox.get(); //String 타입만 반환
        System.out.println("str = " + str);

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

        // 타입 추론 : 제네릭 타입 생략 가능
        GenericBox<Integer> integerBox2 = new GenericBox<>();
    }
}

실행 결과

integer = 10
str = hello
doubleValue = 10.5

생성 시점에 원하는 타입 지정

  • 제네릭 클래스는 <>(다이아몬드 기호) 안에 타입 매객변수를 정의하면 된다.
class GenericBox<T>
  • 제네릭 클래스는 생성하는 시점에 <> 사이에 원하는 타입을 지정한다.
new GenericBox<Integer>()
  • 이렇게 하면 앞서 정의한 GenericBoxT가 다음과 같이 지정한 타입으로 변한 다음 생성된다.

T에 Integer를 적용한 GenericBox 클래스

public class GenericBox<Integer> {
	private Integer value;

    public void set(Integer value) {
		this.value = value;
	}
	public Integer get() {
		return value;
	}
}
  • T가 모두 Integer로 변한다. 따라서, Integer 타입을 입력하고 조회할 수 있다.
  • 이렇게 지정하면 set(Integer value) 이므로 이 메서드에는 Integer 숫자만 담을 수 있다.
integerBox.set(10); //성공
integerBox.set("문자100"); // Integer 타입만 허용, 컴파일 오류

get()의 경우에도 Integer를 반환하기 때문에 타입 캐스팅 없이 숫자 타입으로 조회할 수 있다.

Integer integer = integerBox.get(); // Integer 타입 반환 (캐스팅 X)

원하는 모든 타입 사용 가능

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

new GenericBox<Double>()
new GenericBox<Boolean>()
new GenericBox<MyClass>()
  • 참고로 제네릭을 도입한다고 해서 앞서 설명한 GenericBox<String> , GenericBox<Integer> 와 같은 코드가 실제 만들어지는 것은 아니다.
  • 대신에 자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이런 코드가 있다고 가정하고 컴파일 과정에 타입 정보를 반영한다.

정리

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

4. 제네릭 용어와 관례

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

메서드의 매개변수와 인자

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

void main() {
	String arg = "hello";
    method(arg) // 인수 전달
}
  • 메서드의 매개변수에 인자를 전달해서 메서드의 사용 값을 결정한다.

제네릭의 타입 매개변수와 타입 인자

  • 제네릭도 앞서 설명한 메서드의 매개변수와 인자의 관계와 비슷하게 작동한다.
  • 제네릭 클래스를 정의할 때 내부에서 사용할 타입을 미리 결정하는 것이 아니라, 해당 클래스를 실제 사용하는 생성 시점에 내부에서 사용할 타입을 결정하는 것이다.
  • 차이가 있다면 메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루는 것이고, 제네릭의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것이다.

정리하면 다음과 같다.

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

용어 정리

  • 제네릭 단어
    • 제네릭이라는 단어는 일반적인, 범용적이라는 뜻
  • 제네릭 타입
    • 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것
    • 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
    • 예 : class GenericBox<t>{private T t;}
  • 타입 매개변수
    • 제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체
    • 예 : GenericBox<T>
    • 여기에서 T를 타입 매개변수라 한다.
  • 타입 인자
    • 제네릭 타입을 사용할 때 제공되는 실제 타입이다.
    • 여기에서 Integer를 타입 인자라 한다.

제네릭 명명 관례

주로 사용하는 키워드는 다음과 같다.

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

제네릭 기타

class Data<K, V> {}

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

로 타입 - raw type( 이건 사용하면 안된다!! )

public class RawTypeMain {
    public static void main(String[] args) {
        GenericBox integerBox = new GenericBox();
        //GenericBox<Object> integerBox = new GenericBox<>(); // 권장
        integerBox.set(10);
        Integer result = (Integer) integerBox.get();
        System.out.println("result = " + result);
    }
}
  • 제네릭 타입을 사용할 때는 항상 <>를 사용해서 사용시점에 타입을 지정해야 한다. 그런데 다음과 같이 <>를 지정하지 않을 수 있는데, 이런 것을 로 타입(raw type), 또는 원시타입이라 한다.
GenericBox integerBox = new GenericBox();

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

  • 제네릭 타입을 사용할 때는 항상 <>를 사용해서 사용시점에 타입을 지정해야 한다.
  • 그런데 왜 이런 로 타입을 지원하는 것일까?
    • 자바의 제네릭이 자바가 처음 등장할 때부터 있었던 것이 아니라 자바가 오랜기간 사용된 이후에 등장했기 때문에 제네릭이 없던 시절의 과거 코드와의 하위 호환이 필요했다. 그래서 어쩔 수 없이 이런 로 타입을 지원한다
  • 정리하면 로 타입을 사용하지 않아야 한다.
  • 만약에 Object 타입을 사용해야 한다면 다음과 같이 타입 인자로 Object 를 지정해서 사용하면 된다.

5. 제네릭 활용 예제

이번에는 직접 클래스를 만들고, 제네릭도 도입해보자.
지금부터 사용할 Animal 관련 클래스들은 이후 예제에서도 사용하므로generic.animal 이라는 별도의 패키지에서 관리한다.

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("멍멍");
    }

}
  • .super 키워드를 사용하면 현재 클래스가 확장한 상위 클래스의 생성자를 호출할 수 있습니다.

  • 고양이 클래스인 Cat에서는 Animal 클래스의 생성자를 호출해야 합니다. 그렇지 않으면 상위 클래스인 Animal 클래스의 초기화가 제대로 이루어지지 않아 오류가 발생합니다.

  • 따라서 Cat 클래스의 생성자에서는 다음과 같이 super(name, size); 를 사용하여 Animal 클래스의 생성자를 호출합니다.

여기서부터 패키지 경로 다름

public class Box<T>{
    private T value;

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

실행 결과

findDog = Animal{name='멍멍이', size=100}
findCat = Animal{name='냐옹이', size=50}
findAnimal = Animal{name='동물', size=0}
  • Box 제네릭 클래스에 각각의 타입에 맞는 동물을 보관하고 꺼낸다.
  • Box<Dog> dogBox : Dog 타입을 보관할 수 있다.
  • Box<Cat> catBox : Cat 타입을 보관할 수 있다.
  • Box<Animal> animalBox : Animal 타입을 보관할 수 있다.

여기서 Box<Animal> 의 경우 타입 매개변수 T 에 타입 인자 Animal 을 대입하면 다음 코드와 같다

public class Box<Animal> {
	private Animal value;

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

	public Animal get() {
		return value;
	}
}
  • 따라서, set 이므로 set()Animal의 하위 타입인 Dog, Cat 도 전달할 수 있다.
  • 물론 이 경우 꺼낼 때는 Animal 타입으로만 꺼낼 수 있다.
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); // Animal = Dog
		animalBox.set(cat); // Animal = Cat
		Animal findAnimal = animalBox.get();
		System.out.println("findAnimal = " + findAnimal);
	}
}	
profile
안녕하세요. wony입니다.

0개의 댓글