Search
Duplicate

아이템 30 - 이왕이면 제네릭 메서드로 만들라

작성자
챕터
5장 - 제네릭
최종 편집
2023/07/22 04:00
생성 시각
2023/07/21 12:45

핵심어

제네릭 싱글턴 팩터리
제네릭 싱글턴 팩터리는 특정 타입에 대해 단 하나의 인스턴스만 생성하는 디자인 패턴이다. 이를 활용하면 메모리를 절약하고, 인스턴스가 단일 상태를 공유하기 때문에 동기화에 대한 부담을 줄일 수 있다. 제네릭을 이용한 싱글턴 팩터리의 핵심은, 타입 인자에 따라 서로 다른 타입의 같은 객체를 봉인(encapsulation)하도록 하는 것이다. 즉, 호출하는 쪽에서는 실제 비즈니스 로직을 모르더라도 필요한 타입의 객체를 얻을 수 있게끔 한다.
public class GenericSingletonFactory { // 싱글턴 인스턴스 private static Object instance = new Object(); // 제네릭 싱글턴 팩터리 메소드 public static <T> T getInstance(Class<T> type) { return type.cast(instance); } } /*--------------------------------------------------*/ Object obj = GenericSingletonFactory.getInstance(Object.class);
Java
복사
하지만 이 코드는 실제로 잘 사용되지 않는데, 이유는 각 타입에 대한 싱글턴 인스턴스를 반환해야 할 때 해당 인스턴스가 실제로 해당 타입의 인스턴스인지 보장할 수 없기 때문이다.(왜 씀?) 따라서 이 방식은 그닥 유용하지 않고, 일반적인 싱글턴 패턴의 사용이 더 바람직하다고 한다. 이 팩터리 메소드의 주된 용도는 Collections.emptyList(), Collections.emptySet() 등과 같이 빈 컬렉션 인스턴스를 반환하는 것이다. → 결국 싱글턴 팩터리이기 때문에, 인스턴스를 새로 생성하는 것이 아니므로(전달 받고 반환하는 레퍼런스는 동일) 한계가 있을 것 같다.
Identity Function(항등 함수)
항등 함수(identity function)는 입력값을 그대로 출력하는 함수를 말한다. 수학적으로 보면, 항등함수는 모든 실수 x에 대해 f(x) = x 라는 성질을 가진다. Java에서 Function.identity()는 이러한 항등 함수를 반환하는 메소드를 제공한다. 예를 들어, 아래 코드는 항등 함수를 사용해 Stream의 모든 요소를 그대로 새로운 List에 넣는 것이다:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); List<Integer> sameList = list.stream() .map(Function.identity()) .collect(Collectors.toList());
Java
복사
코드에서 Function.identity()는 입력을 그대로 반환하는 항등 함수를 만든다. 따라서 map을 통해 리스트의 각 요소를 그대로 적용, sameList는 원래 리스트와 동일한 요소를 가지게 된다.

요약

형변환이 필요한 메서드의 경우에는 제네릭 메서드로 변경하는 것을 고려하라.

내 생각

결국 제네릭의 요점은 ‘컴파일 시 타입 안전하고, 런타임 시에는 타입 소거’와 ‘타입에 대해 유연하기 때문에 재사용성이 높아진다’인 것 같다.
와일드 카드<?>와 제네릭 타입<E>의 가장 큰 차이점
수정 가능성(Mutability)와 컴파일 타임시의 타입 추론에서 차이가 있는 것 같다.
‘어떤 타입이 들어오든 받아들일 수 있음’은 와일드 카드와 제네릭 타입 둘 다 동일하다 - 어차피 제네릭 자체의 특성으로, 공유되니까.
하지만, 내부적으로 사용될 때 type safe(<E>)냐 아니냐(<?>)에서 갈리고, 이로 인해서 커버할 수 있는 범위가 달라지고, 수정 가능성이 갈린다. <E>와 같은 경우에는 해당 호출 시(코드 레벨에서 컴파일 타임)에 타입이 결정되고, 그 타입에 맞게끔 동작함의 제약을 받는다. <?>와 같은 경우에는 무엇이든 받아들일 수 있다.
한편, <?>의 경우에 메서드 내부에서 특정 작업(수정, 교환)등을 수행할 수는 없다.

예제 코드

제네릭 싱글톤 팩터리 이용해보기
public class AllTypeAffordableSingletonSet { private static final Set EMPTY_SET = new HashSet<>(); @SuppressWarnings("unchecked") public static <T> Set<T> getInstance() { return (Set<T>) EMPTY_SET; } } @Test void 제네릭_싱글톤_팩터리() { Set<String> stringSet = AllTypeAffordableSingletonSet.getInstance(); Set<Integer> integerSet = AllTypeAffordableSingletonSet.getInstance(); stringSet.add("hello"); integerSet.add(123); System.out.println("stringSet = " + stringSet); // stringSet = [hello, 123] }
Java
복사
제네릭 타입<E> 안 쓰고 <?>로 사용하면 벌어지는 일
제네릭 타입으로 메서드에서 지정해주면 이런 케이스를 막아줄 수 있다.
public static <E> List<E> mergeLists(List<E>... lists) { List<E> result = new ArrayList<>(); for (List<E> list : lists) { result.addAll(list); } return result; } List<Integer> a = Arrays.asList(1, 2, 3); List<Integer> b = Arrays.asList(4, 5, 6); List<Integer> merged = mergeLists(a, b); // [1, 2, 3, 4, 5, 6] /*------------------------------------------------------------*/ public static List<?> mergeLists(List<?>... lists) { List<?> result = new ArrayList<>(); for (List<?> list : lists) { result.addAll(list); // 컴파일 에러 } return result; }
Java
복사
와일드카드 타입의 경우에는 수정이 불가능 하므로 컴파일 에러가 나타난다.