쌩로그

김영한의 실전 자바 - 중급 2편 - Sec 02. 제네릭 - Generic1 본문

Language/JAVA

김영한의 실전 자바 - 중급 2편 - Sec 02. 제네릭 - Generic1

.쌩수. 2025. 1. 1. 19:11
반응형

목차

  1. 포스팅 개요
  2. 본론
     2-1. 제네릭이 필요한 이유
     2-2. 다형성을 통한 중복 해결 시도
     2-3. 제네릭 적용
     2-4. 제네릭 용어와 관례
     2-5. 제네릭 활용 예제
  3. 요약

1. 포스팅 개요

해당 포스팅은 김영한의 실전 자바 중급 2편 Section 2의 제너릭 - Generic1 에 대한 학습 내용이다.

프로젝트 생성 part는 생략한다.

학습 레포 URL : https://github.com/SsangSoo/inflearn-holyeye-java-mid2
(해당 레포는 완강시 public으로 전환 예정이다.)

2. 본론

2-1. 제네릭이 필요한 이유

다음과 같이 IntegerString을 보관하는 객체가 있다고 하고 이것을 활용하는 코드를 보자.

// Integer를 보관하는 객체  
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 value) {  
        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);  
    }  
}

// 결과
integer = 10
str = hello

Integer, String 을 담는 Box 객체를 만들었다.
그런데, 두 타입 뿐만 아니라 다른 타입을 담는 Box 객체를 만들어야 한다면 그 타입에 따른 Box 클래스를 또 만들어야 한다.
XxxBox 클래스를 만들어야 할 것이다.
이 문제를 어떻게 해결할 수 있을까?

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

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

// Object 박스
public class ObjectBox {  

    private Object value;  

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

    public Object get() {  
        return value;  
    }  
}

// 활용 예제 코드
public class BoxMain2 {  

    public static void main(String[] args) {  
        ObjectBox objectBox = new ObjectBox();  
        objectBox.set(10);  
        Integer integer = (Integer) objectBox.get();  
        System.out.println("integer = " + integer);  

        ObjectBox stringBox = new ObjectBox();  
        stringBox.set("hello");  
        String str = (String) stringBox.get();  
        System.out.println("str = " + str);  
        // 여기까진 이전과 동일결과  


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

// 결과
integer = 10
str = hello
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
    at generic.ex1.BoxMain2.main(BoxMain2.java:18)

이전과 동일한 결과를 얻을 수 있어 잘 작동하는 것 같아보이지만,
이후 협업이나 유지보수시 잘못된 타입의 인수가 전달되면 타입의 불일치로 예외가 발생할 수 있다.

사실 set() 메서드로 값을 넣을 때는 문제가 되지 않지만, 값을 꺼내올 때 문제가 발생한다.

숫자타입이 입력되기를 기대하여 integerBox라는 이름을 썼지만, 문자열의 입력이 가능하기 때문에 실수로 문자열을 집어넣을 수도 있다.

타입에 따른 XxxBox클래스를 만드는 중복을 제거했지만, 입력시 실수로 원하지 않는 타입이 들어갈 수 있는 타입 안정성 문제가 발생한다.

Object 받았던 박스는

  • 타입 안정성 문제는 없지만,
  • 중복 문제를 제거하여 코드르 재사용할 수 있었다.

XxxBox

  • 코드의 중복이 발생하여 코드의 재사용은 불가했지만,
  • 타입 안정성 문제는 해결했다.

이 두가지의 장점과 단점을 동시에 아우르는 것이 바로 자바에서 제공하는 제네릭이다.

2-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) {  
        GenericBox<Integer> integerBox = new GenericBox<>(); // 생성 시점에 T의 타입 결정  
        integerBox.set(10);  
        Integer integer = integerBox.get();  
        System.out.println("integer = " + integer);  

        GenericBox<String> stringBox = new GenericBox<>(); // 생성 시점에 T의 타입 결정  
        stringBox.set("hello");  
        String str = stringBox.get();  
        System.out.println("str = " + str); 

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

// 결과
integer = 10
str = hello

아래는 컴파일 오류 장면이다.

Integer 를 담을 박스에 문자를 넣으니 컴파일 오류가 발생했다.

  • 다이아몬드 안에 타입을 지정하면 객체 생성 시점에 T의 타입이 결정된다.
  • 제네릭 클래스를 사용하면 GenericBox 객체를 생성하는 시점에 원하는 타입을 마음껏 지정할 수 있다.

참고로 이때는 GenericBox<Integer>, Generic<String> 과 같은 코드가 실제로 만들어지는 것이 아니다.
자바 컴파일러가 우리가 입력한 타입 정보를 기반으로 이런 코드가 있다고 가정하고 컴파일 과정에 타입 정보를 반영한다.
이 과정에서 타입이 맞지 않으면 컴파일 오류가 발생한다.(컴파일 오류 장면 참고)

타입 추론

왼쪽에 변수를 선언할 때 제네릭 클래스 타입을 선언하면서 다이아몬드 안에 타입을 지정했으면,
우측에 제네릭 클래스를 생성할 때 다이아몬드에는 타입을 지정하지 않아도 된다.
왼쪽에 있는 제네릭 다이아몬드의 타입을 보고, 추론을 하기 때문이다.

이처럼 자바가 스스로 타입 정보를 추론해서 개발자가 타입 정보를 생략할 수 있는 것을 타입 추론이라고 한다.

타입추론 예시

GenericBox<Integer> integerBox = new GenericBox<>(); // O
GenericBox<> integerBox = new GenericBox<Integer>(); // Identifier expected(컴파일 오류)

타입추론이 아무 때나 할 수 있는 것이 아니라, 문법상 가능할 때만 가능하다.

2-4. 제네릭 용어와 관례

제네릭의 핵심

  • 사용할 타입을 미리 결정하지 않음.
  • 클래스 내부에서 사용하는 타입을 클래스를 정의하는 시점에 결정하는 것이 아니라 실제 사용하는 생성 시점에 타입을 결정한다.

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

  • 제네릭 클래스의 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미룬다.
    • 반면 메서드의 매개변수는 사용할 에 대한 결정을 나중으로 미룬다.
  • 제네릭에서 사용하는 용어도 메서드와 같이 매개변수, 인자의 용어를 그대로 가져다 사용한다.
    • 다만 값이 아니라 타입을 결정하는 것이기 때문에 앞에 타입을 붙인다.
      • 타입 매개변수 : GenericBox<T>에서 T
      • 타입 인자
        • GenericBox<Integer>에서 Integer
        • GenericBox<String>에서 String

용어 정리

  • 제네릭(Generic) 단어
    • 제네릭이라는 단어는 일반적인, 범용적인이라는 영어 단어 뜻이다.
    • 풀어보면 특정 타입에 속한 것이 아니라 일반적으로 범용적으로 사용할 수 있다는 뜻이다.
  • 제네릭 타입 (Generic Type)
    • 클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다.
    • 제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.
      • 타입은 클래스, 인터페이스, 기본형(int등)을 모두 합쳐서 부르는 말이다.
    • 예 : class GenericeBox<T> { private T t; }
    • 여기에서 GenericBox<T> 를 제네릭 타입이라 한다.
  • 타입 매개변수 (Type Parameter)
    • 제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체된다.
    • 예 : GenericBox<T>
    • 여기에서 T를 타입 매개변수라 한다.
  • 타입 인자(Type Argument)
    • 제네릭 타입을 사용할 때 제공되는 실제 타입이다.
    • 예 : GenericBox<Integer>
    • 여기에서 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)를 사용하면 된다.

로 타입(row type)

public class RowTypeMain {  

    public static void main(String[] args) {  
        GenericBox integerBox = new GenericBox();  
        integerBox.set(10);  
        Integer result = (Integer) integerBox.get();  
        System.out.println("result = " + result);  
    }  
}
  • 다이아몬드엔 아무것도 선언하지 않았다.
    • 이를 원시 타입 혹은 로 타입이라고 한다.
  • 로 타입은 자바에 제네릭이 등장할 당시 하위 호환을 위해 두었다.
  • 로 타입은 사용하지 않고, 제네릭 타입을 사용해야 한다.

2-5. 제너릭 활용 예제

  • Animal (부모)
    • Dog(Animal 상속)
    • Cat(Animal 상속)
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<Anmimal>에 해당하는 부분은 DogCatAnimal을 상속하므로, Animal 의 하위타입인 DogCat 도 들어갈 수 있다.

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);  
    }  
}

// 결과
findAnimal = Animal{name='냐옹이', size=50}
  • get() 을 하면 Animal 이 나온다.
  • 그리고 출력을하면 마지막으로 들어갔던 값이 출력된다.

3. 요약

제네릭 클래스에 대해 알아보았다.

  • 제네릭 클래스로 인해서 중복 코드 제거타입 안정성을 한 번에 잡을 수 있다.
  • 제네릭 클래스의 타입 매개변수로 인해서 클래스가 생성될 시점에 내부의 타입 결정을 추론하여 결정할 수 있었다.
728x90
Comments