infinity : 무한한 성장가능성

자바 제네릭 완전정복 (1) 본문

Develop/💜Java

자바 제네릭 완전정복 (1)

인피니 2024. 1. 21. 15:26

자바 제네릭을 다루게 된 이유?


스프링 배치를 개발하던 중 MyBatisItemWriterBuilder을 사용해서 MyBatisBatchItemWriter 객체를 만들 때 파라미터의 값을 기본 Item 값에서 추가해야 하는 경우가 있었다.

MyBatisWriterBuilder의 itemToParameterConverter 을 통해 파라미터를 수정할 수 있었는데, 해당 메서드의 내부를 살펴보니 

  public MyBatisBatchItemWriterBuilder<T> itemToParameterConverter(Converter<T, ?> itemToParameterConverter) {
    this.itemToParameterConverter = itemToParameterConverter;
    return this;
  }

 제네릭이 보이는 것이다..!

위의 코드가 하나도 이해되지 않아, 제네릭을 공부하고 위의 메서드를 분석해보고자 한다. 

(자바를 예전에 공부할때 제네릭을 분명 공부했었는데, 까먹어 버렸다.. 코드를 살펴볼 때 자주 등장하는 제네릭인만큼 이번에 완벽히 이해하고 넘어가고자 한다.)

 

 

자바 제네릭 이란?


제네릭: 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법 즉 인스턴스를 생성할때 타입을 결정한다.

public class Animal<T> {
    public T info;
}

예를들어, 이렇게 Ainimal 클래스를 만들 때 클래스 내부에서 사용되는 info의 타입을 Ainimal 객체 생성 시 결정하는 것이다.

public class  Main{
    public static void main(String[] args){
        Animal<String> animal = new Animal<>();
    }
}

 new Animal<String>(); 으로 객체 생성 시 info의 타입은 String으로 결정된다. 

위에서 new Animal<String> 에서 String 이 생략된 이유는 좌측의 Animal <String> animal에서 제네릭 타입을 추론할 수 있기 때문에 생략할 수 있다.

 

제네릭이 등장한 이유?


제네릭이 없는 경우를 먼저 생각해보자

public class Animal {
    private Object info;

    public Animal(Object info) {
        this.info = info;
    }

    public Object getInfo() {
        return info;
    }
}

info에 여러 동물의 객체 타입이 올 수 있다면, Object 타입을 쓰는 방법밖에 없다. 

public class  Main{
    public static void main(String[] args){
        Animal animal = new Animal(new DogInfo("솜", "3"));
        DogInfo dogInfo = (DogInfo) animal.getInfo();
        dogInfo.print();
    }

}
public class DogInfo {

    private String name;
    private String age;

    public DogInfo(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public void print() {
        System.out.println("name: " + name + "age: " + age);
    }
}

Animal 클래스에서 info 에 들어간 동물 클래스 객체를 가져와 해당 동물 클래스의 메서드를 호출하고 싶다면?

👉 animal 클래스에서 info 필드는 Object 타입이기 때문에 info에 할당된 객체를 가져와 메서드를 호출하려면 할당된 객체타입으로 형변환을 하는 방법밖에 없다. 

 

위의 코드에서는 두 가지 문제점이 존재한다.

1) TypeSafe 하지 않음 

    public static void main(String[] args){
        Animal animal = new Animal(new DogInfo("솜", "3"));
        CatInfo dogInfo = (CatInfo) animal.getInfo();
        dogInfo.print();
    }

animal 인스턴스에 들어간 info type 은 DogInfo인데 개발자가 실수로 CatInfo  타입으로 형변환을 한다면?

런타임에 애러가 발생한다.

컴파일 시에 해당 오류를 발견하지 못한다. 

제일 좋은 오류는 컴파일 오류다...! 

 

2) 불필요한 형변환 

위의 main 메서드에서 dogInfo.print() 메서드를 호출하기 전 형변환을 통해 info 객체 값을 가져오는 것을 볼 수 있다.

info 타입에 할당된 인스턴스에 메서드를 호출하려면 형변환을 해줘야 한다는 것이다. 

 

제네릭을 사용한다면?


public class Animal<T> {
    private T info;

    public Animal(T info) {
        this.info = info;
    }

    public T getInfo() {
        return info;
    }
}
public class  Main{
    public static void main(String[] args){
        Animal<DogInfo> animal = new Animal<>(new DogInfo("솜", "3"));
        DogInfo dogInfo = animal.getInfo();
        dogInfo.print();
    }

}

위의 두 가지 문제가 해결된 것을 볼 수 있다.

Animal 객체 생성시점에 info의 타입을 결정하기 때문에 불필요한 형변환을 할 필요가 없고, Type Safe 하다.

 

제네릭의 타입과 개수 


위의 코드에서 들어간 제네릭 타입을 보면 T 만 들어간 것을 볼 수 있는데,

<> 안에 들어가는 알파벳은 T, E, K 등 아무거나 상관없지만 대문자 알파벳 한글자라는 것을 지켜주면 된다.

위 규칙을 지키지 않는다고 에러가 발생하는 것은 아니지만 개발자들 사이에서의 관례로 지켜주는 것이 좋다.

public class Animal<T,K> {
    private T info;
    private K ancestor;

    public Animal(T info, K ancestor) {
        this.info = info;
        this.ancestor = ancestor;
    }
}

위 코드처럼 제네릭 타입 파라미터는 여러 개가 가능하다.

 

지금까지 다룬 지식만으로 맨 위 코드를 분석할 수 있을까? 

 

아니다.. ㅎ 제네릭 메서드와 와일드카드(?)가 남았다..!!!

다음 포스팅에서 제네릭 메서드와 와일드카드의 개념에 대해 학습하고, 맨 위의 코드를 분석하고자 한다.

 

참고

https://www.youtube.com/watch?v=YUinFIexEQ4

https://kyleyj.tistory.com/35

'Develop > 💜Java' 카테고리의 다른 글

MapStruct 에 대해 알아보자  (0) 2025.01.19
ModelMapper 에 대해 알아보자  (0) 2025.01.05
자바 제네릭 완전정복 (2)  (0) 2024.02.18