[Java] 객체지향 관점에서 제네릭(Generic) 이해하기

728x90

안녕하세요✋

오늘은 Java 에서 제네릭 에대해 알아보겠습니다.

 

제가 간단하게 알았던 제네릭 타입은 그냥

List<String> list = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();

< > 안에 들어가는 타입을 미리 결정하는게 제네릭 타입 이구나 정도로 알고 있었습니다.

 

그러다가 저 리스트에 String이 아닌 객체가 들어올수도 있고, Integer, Double 등 다른값이 들어올수도 있는 상황을 생각해 봤습니다. 

 

즉 나중에 저 List 안에 String 도 들어갈수도 있고, Integer 도 들어갈 수 있는

즉, 재사용성이 가능한 코드를 어떻게 하면 만들 수 있을까?

라는 고민을 해봤고, 그에 대한 해답은 바로 제네릭 을 잘 이해하고 사용하자 였습니다. 

 

고민 1)

어떠한 타입이 들어올지 모르니, 그냥 Object 를 넣고 그냥 그때 그때 상황 봐가면서 원하는 타입을 넣으면 해결되는거 아닌가요?

 

위 생각을 했습니다. 

뭐 틀린말까지는 아니라고 생각합니다. 하지만 최상위 객체인 Object 로 선언하면 Object 의 하위 객체들에 대해서는 

추상적으로 넣고 싶은 모든걸 넣을 수 있습니다.

public class ExGeneric<Object> {
	private Object data;

	public ExGeneric(Object data) {
		this.data = data;
	}

	public Object getData() {
		return data;
	}

	public static void main(String[] args) {
		ExGeneric<String> example1 = new ExGeneric<>("Hello");
		ExGeneric<Integer> example2 = new ExGeneric<>(123);

		System.out.println(example1.getData()); // Output: Hello
		System.out.println(example2.getData()); // Output: 123
	}
}

 

위 처럼 상위 클래스에 Object 로 선언을 하고 실제 객체를 생성하고 인스턴스를 만들 때만

타입을 확실히 지정해주면 전혀 문제가 없습니다.

 

고민2) 

하지만 User, Fruit 이런 객체들이 나왔을 땐 어떻게 해결해야 할까요?

 

음 여기서부터 조금더 고민이 많이 됬습니다. 클래스 객체는 Object 하위도 아니고, 어떻게 해야 

한 메소드 또는 클래스를 선언해두고 내가 필요할 때 마다 객체를 바꿔가면서 사용할 수 있을까?

 

위에 대한 해결은 객체지향 원칙에서 말하는, 추상화 와 다형성을 적절하게 사용해보면서 의문을 해결할 수 있었습니다.

 

위 과정에서 자바 레퍼런스 문서를 보는 능력또한 향샹된 것 같아 기분이 좋았습니다ㅎㅎ😋

 

객체지향에서 말하는 추상화란 무엇일까요?

어떤 코드가 추상화된 코드이고, 어떤 코드가 추상화가 되지 않은 코드일까요?

 

abstract 를 붙여야 추상화 된 코드인가요? 

 

보통 추상화를 생각하면 interface 를 생각할 것이라고 생각합니다. 

// 추상 클래스 예제
abstract class Animal {
    abstract void makeSound(); // 추상 메서드
}

// 인터페이스 예제
interface Animal {
    void makeSound(); // 추상 메서드
}

 

위 코드에서 더 익숙한 코드는 무엇인가요?

 

저는 interface 에 더 익숙합니다. 둘이 최종적으로 같은 역할을 하는데 뭔가 그냥 interface 가 친숙한?? 그런 느낌

 

두 코드 의 차이를 간단하게 알아보면 

 

1️⃣

= abstract 클래스는 구현시 extends(상속)을 받아야 합니다.

= Interace 클래스는 구현시 Implements(구현)을 받습니다.

 

2️⃣

= 추상 클래스는 인스턴스 변수를 포함할 수 있고, 이러한 변수의 초기화 및 관리에 대한 코드를 포함합니다.

= 인터페이스는 정적 상수와 추상 메소드만을 가지며, 구현하는 클래스에서는 상태를 유지할 수 없습니다.

 

간단하게 차이점을 보고 제가 느낀 바로는

클래스를 상속하고 확장하는 관점에서는 추상 클래스가 용이하고,

 

클래스가 여러 타입의 동작을 가지고 있거나, 다른 객체와 상호작용 하는 경우는 인터페이스가 좋다고 생각합니다.

인터페이스를 사용하여 의존의 최소화 할 수 있을 것 같습니다.

 

 

본론으로 돌아가서 그럼 추상화가 된 코드와 안된 코드를 비교해 보겠습니다. 

public interface Animal {
    void makeSound();
}

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}

public class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

public class AnimalSound {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.makeSound(); 
        cat.makeSound(); 
    }
}

 

위 코드는 인터페이스로 추상화가 잘 되어, Dog, Cat 클래스가 각각 의 다른 행동을 리턴 합니다.

 

이번에는 추상화가 되지않은 코드를 보겠습니다.

public class Dog {
    public void bark() {
        System.out.println("Woof!");
    }
}

public class Cat {
    public void meow() {
        System.out.println("Meow!");
    }
}

public class AnimalSound {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.bark(); 
        cat.meow();
    }
}

 

위 코드는 Dog, Cat이 독립적으로 구현되어 있어, 코드가 구체적인 동물 종류에 의존하므로 확장성과 유연성이 떨어집니다.

지금은 뭐 간단한 코드닌까 상관은 없지만, 실제로 실무에서 저런 코드를 본다면 좀 힘들것 같아요😅 

 

그러면 제네릭 타입에서 어떻게 해야 추상화를 잘 할수 있고 유연한 코드가 되는건가요??

 

바로 제네릭 표기법을 이해할 필요가 있습니다. 

 

가끔 자바 레퍼런스 문서를 보면 아주아주 다양한 코드가 있습니다. 그 중에서도 

<T> , <?> , <? extends T> ,<? super T> 이런 표기가 된 API 문서를 본적이 있으신가요? 

 

저는 처음에 저 코드를 보면 사실 잘 안 읽고 레퍼런스 문서 설명만 복사해서 번역기를 돌려서 이해를 도왔습니다. 

뭔가 저 어지러운 코드가 너무 어려워 보였습니다..

하지만 저 코드를 알면 확실히 API 코드를 보는데 도움이 많이 되는 건 맞는거 같아 요번에 공부를 해보게 되었죠ㅠㅠ

 

<T>

 

위 코드는 자바 레퍼런스 Comparable 인터페이스 입니다.

위 코드에 있는 <T> 는 무엇을 의미하는 걸까요?

 

위 코드에서 인터페이스 클래스에서 제네릭 타입을 <T> 로 추상적으로 선언을 해두었습니다.

 

이렇게 함으로써 해당 인터페이스를 구현할 때 여러 종류의 타입에 대해 사용할 수 있습니다. 

 

당연하게도 위 인터페이스를 구현한 클래스는 안에 포함된 메소드를 구현해야 하고,

위 메소드는 어떠한 타입이든 받을 수 있습니다. 

 

또 생각을 해보면 그냥 명시적으로 T 라고 하는거지, 여러분 들이 사용할 때는 T 가 아니라, E,F,G 뭐든 상관없습니다.

 

그냥 관행적으로 T 라고 사용해왔고, 레퍼런스 문서도 T 라고 되어 있으니 다들 많이 사용을 하시는 것 같네요

 

그리고 보통 T 또는 문자로 선언한 제네릭 같은 경우는 자바의 타입을 추상화 할 때 사용합니다.

ex) String, Integer, Double etc..

 

import java.util.ArrayList;

public class ExGenericType<T> {
    private T data;

    public ExGenericType(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public static void main(String[] args) {
        ExGenericType<Integer> example = new ExGenericType<>(10);
        System.out.println(example.getData()); 
    }
}

 

 

<?>

위 물음표는 보통 어떠한 클래스 객체가 들어올지 모를 때 선언을 한다고 생각하면 편합니다. 

? 는 와일드 카드 라고도 부릅니다. 

String, Integer 타입 객체도 가능하지만, 클래스 객체를 받아야 할때 많이 사용됩니다.

public class Api<T> {

    private Result result;

    @Valid
    private T body;

    public static <T> Api<T> OK(T data){
        var api = new Api<T>();
        api.result = Result.OK();
        api.body = data;
        return api;
    }
@RestController
@RequiredArgsConstructor
public class UserApiController {

    private final UserBusiness userBusiness;

    @GetMapping("/user")
    public Api<UserResponse> me(
        @UserSession User user
    ){
        var response = userBusiness.me(user);
        return Api.OK(response);
    }
}

 

위 처럼 클래스를 생성한 후 구현할 때 클래스 객체를 직접 넣어서 사용하는 예시 입니다. 

 

<? extends T>

상위 클래스 나 인터페이스를 제한하는데 사용됩니다. 

import java.util.ArrayList;
import java.util.List;

public class ExampleUpperBoundedWildcard {
    public static double sum(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);

        System.out.println(sum(intList)); // Output: 6.0
    }
}

 

위 코드에서 List<? extends Number> 은

'Number' 클래스 또는 그 하위 클래스들의 리스트를 의미합니다.

 

<? super T>

위 의미는 하위클래스나 인터페이슬 제한하는데 사용합니다.

import java.util.ArrayList;
import java.util.List;

public class ExampleLowerBoundedWildcard {
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void main(String[] args) {
        List<Object> objList = new ArrayList<>();
        addIntegers(objList);
        System.out.println(objList); // Output: [1, 2, 3]
    }
}

 

위 코드에서 List<? suepr Integer> 는

'Integer' 클래스 또는 그 상위 클래스들의 리스트를 나타냅니다.

 

 

https://st-lab.tistory.com/153

위 표는 generic 타입 관련 표 입니다.

 

 

최종 정리를 해보자면

 

<T>:
- 제네릭 클래스나 메서드에서 사용되며, 타입 매개변수를 선언할 때 사용됩니다.
- T는 일반적으로 타입 파라미터의 이름으로 사용되며, 실제 사용될 때 구체적인 타입으로 대체됩니다.
- Ex) ArrayList<T>에서 T는 리스트가 저장할 요소의 타입을 나타냅니다.

- <T> , <E> 다양한 방식으로 사용이 됩니다. 

 

<?> (Unbounded Wildcard):
- 모든 타입을 나타내는 와일드카드입니다.
- 제네릭 타입을 사용할 때, 타입 매개변수를 특정하지 않고 어떤 타입이든 받을 수 있음을 나타냅니다.
- 주로 읽기 전용 작업에 사용됩니다. 예를 들어, List<?>는 어떤 종류의 리스트이든 받아들일 수 있습니다.

    - 읽기 전용이라 함은, CRUD 에서 R -> Select 를 의미하는 것 입니다. 

 

<? extends T> (Upper Bounded Wildcard):
- 특정 타입의 서브타입을 나타내는 와일드카드입니다.
- 주어진 상한 타입 T와 그 서브클래스 중 하나를 나타냅니다.
- 이 와일드카드를 사용하면 해당 타입과 그 하위 타입의 요소를 읽을 수 있지만 쓸 수는 없습니다.

 

<? super T> (Lower Bounded Wildcard):
- 특정 타입의 슈퍼타입을 나타내는 와일드카드입니다.
- 주어진 하한 타입 T와 그 슈퍼클래스 중 하나를 나타냅니다.
- 이 와일드카드를 사용하면 해당 타입과 그 상위 타입을 요소로 포함하는 컬렉션에 요소를 추가할 수 있습니다.
- 따라서 <?>와 T는 서로 다른 상황에서 사용되며, 각각의 용도에 따라 적절하게 선택되어야 합니다. 일반적으로 와일드카드는 유연성을  제공하고 타입 안전성을 유지하는 데 도움이 되며, 타입 매개변수는 특정한 타입이 필요할 때 사용됩니다.

 

 

👍🏽 Generic(제네릭)의 장점

1) 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.

-> 컴파일 할때 지정한 타입으로 캐스팅하여, 매개변수화 된 유형을 삭제해준다. 

2) 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환할 필요가 없다. -> 유지보수성 용이

3) 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다. 

 

 

++ 추가(2024.05.09)

제네릭 메소드 선언 방법

public <T> T genericMethod(T t) {
		...
}	

[접근제어자] <제네릭타입> [반환타입] [메소드명]( [제네릭타입] [파라미터]) {
	// 로직
}

제네릭 클래스와 다르게 반환타입 이전에 제네릭 타입을 선언했습니다. 

 

위 방식은 정적 메소드(static)로 선언할 때 필요하다고 합니다. 

 

제네릭 사용 주의사항

 

이상 포스팅 마치겠습니다.

 

Ref : https://inpa.tistory.com/entry/JAVA
Ref : https://st-lab.tistory.com/153 
728x90