Java

자바 제네릭(Generics)의 이해와 활용

김승진 2025. 3. 20. 23:07

 

public class StringPrinter {
    private String value;
    public StringPrinter(String value) { this.value = value; } // 중복
    public void print() { System.out.println(value); }
}

public class IntegerPrinter {
    private int value;
    public IntegerPrinter(int value) { this.value = value; } // 중복
    public void print() { System.out.println(value); }
}

 

자바를 사용하다 보면 여러 타입을 지원하는 유연한 코드를 작성해야 할 때가 많습니다. 예를 들어, 같은 기능을 하는 메서드나 클래스인데도 타입이 다르다는 이유로 중복된 코드를 작성해야 한다면 비효율적입니다.

이러한 문제를 해결하기 위해 등장한 것이 **제네릭(Generics)**입니다. 제네릭을 활용하면 컴파일 시 타입을 지정할 수 있어 안전성을 확보하면서도 중복 코드를 줄일 수 있습니다. "왜 제네릭이 필요한가?"라는 부분부터 고민해 봅시다.

 

 

왜 제네릭이 필요한가?

자바에는 Object라는 최상위 클래스를 활용하여 여러 타입을 처리하는 방법이 있습니다. 그런데 왜 굳이 제네릭이 필요할까요?

import java.util.*;

public class ObjectExample {
    public static void main(String[] args) {
        List objectList = new ArrayList();
        objectList.add("Hello");
        objectList.add(10);  // 문자열과 정수를 혼합 저장

        String str = (String) objectList.get(0);
        String num = (String) objectList.get(1); // ClassCastException 발생
    }
}

 

objectList는 Object 타입이기에 String과 Integer 타입을 섞어 저장할 수 있습니다. 하지만 String num = (String) objectList.get(1); 실행 시 런타임에서 ClassCastException이 발생합니다.

즉, 타입을 강제로 변환하는 캐스팅 과정이 필요하며, 실수로 다른 타입을 넣으면 컴파일러가 오류를 잡아주지 못합니다.

이러한 문제를 해결하기 위해 제네릭이 등장했습니다.

 

 

제네릭을 사용하면 무엇이 달라지는가?

import java.util.*;

public class GenericExample {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("Hello");
        // stringList.add(10);  // 컴파일 오류 발생

        String str = stringList.get(0);
        System.out.println(str);
    }
}

 

List<String>을 선언하면 타입을 명시했기에 문자열(String)만 허용됩니다. 'stringList.add(10);' 은 Integer 타입이기에 컴파일 오류가 발생하여 미리 문제를 방지할 수 있습니다. 데이터를 꺼낼 때 캐스팅이 필요하지 않다는 이점이 있습니다.

즉, 제네릭을 사용하면 안정성과 코드 가독성이 향상됩니다.

 

제네릭이 자주 사용되는 예시 및 활용

자바에서 제공하는 다양한 API에서도 제네릭이 사용됩니다.

 

[컬렉션]

List<Integer> numbers = new ArrayList<>();
Map<String, Integer> nameToAgeMap = new HashMap<>();

 

이처럼 컬렉션에 특정 타입만 저장하도록 제한하여 안전한 데이터 관리가 가능합니다.

 

[공통 API]

Optional<String> optionalString = Optional.of("안녕하세요.");

Optional<T>는 null 값을 처리할 때 제네릭을 활용하여 타입 안정성을 보장합니다.

 

 

[사용자 정의 제네릭 클래스]

public record ApiResponse<T>(
	String status,
	int code,
	String message,
	T data
) {
	public static <T> ApiResponse<T> ok(T data, int code) {
		return ApiResponse.<T>builder()
			.data(data)
			.code(code)
			.build();
	}
	public static <T> ApiResponse<T> error(String errorMessage, int code) {
		return ApiResponse.<T>builder()
			.message(errorMessage)
			.code(code)
			.build();
	}
}

제네릭 선언을 통해 다양한 데이터 타입에 대해 일관된 API 응답 구조를 유지할 수 있습니다.

 

Post post = PostService.createPost();
ApiResponse<Post> response = ApiResponse.ok(post, 201);

..

User user  = UserService.signIn();
ApiResponse<Post> response = ApiResponse.ok(user, 200);
..

 

이처럼 사용할 때, 처리된 응답 데이터만 전해주면 되기에 간편합니다.

 

int와 같은 Primitive Type 이 있는데 Integer와 같은 Refence Type 을 사용하는 이유는?

제네릭을 사용할 때 int 대신 Integer를 사용해야 합니다. 왜 일까요? 제네릭은 참조값만 지원하기 때문입니다.

List<int> intList = new ArrayList<>(); // 컴파일 오류
List<Integer> list = new ArrayList<>();

컬렉션을 사용할 때, 암묵적으로 int 대신 Integer을 사용합니다. 위에서 말했듯이 제네릭은 참조값만 지원합니다.

따라서 기본 타입이 있더라도 Integer와 같은 래퍼 클래스를 사용해야 합니다.

 

제네릭의 확장: 와일드카드(?)

이처럼 와일드카드 '?'가 있는 것을 종종 볼 수 있습니다. 이는 제네릭에서 제공하는 기능으로 제네릭 타입을 제한하는 역할을 합니다.

  •  <?> : Unbounded Wildcards (제한 없음) 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다
  • <? extends 상위타입> : Upper Bounded Wildcards (상위 클래스 제한) 타입 파라미터를 대치하는 구체적인 타입으로 상위 타입이나 상위 타입의 하위 타입만 올 수 있다
  • <? super 하위타입> : Lower Bounded Wildcards (하위 클래스 제한) 타입 파라미터를 대치하는 구체적인 타입으로 하위 타입이나 하위 타입의 상위 타입만 올 수 있다 

위 예시에서 '? super String' 은 String 하위 클래스로 제한둔다는 의미이고 '?' 는 어떤 타입이든지 다 가능하다는 의미입니다.

 

 

마무리

제네릭을 실제로 많이 사용해보지는 않았더라도, 그 개념과 활용법을 잘 이해한다면 매우 강력한 도구로 활용할 수 있습니다.
특히, 재사용성, 타입 안정성 등의 장점을 제공하기 때문에 유지보수성이 뛰어난 코드를 작성하는 데 큰 도움이 됩니다.

실제로 구현해 사용한 적은 없지만 잘 사용하면 여러 프로그래밍 패턴에서 유용하게 활용될 수 있어 보입니다.

 

참조

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%ADGenerics-%EA%B0%9C%EB%85%90-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0#%EC%A0%9C%EB%84%A4%EB%A6%AD_%ED%83%80%EC%9E%85_%EB%B2%94%EC%9C%84_%ED%95%9C%EC%A0%95%ED%95%98%EA%B8%B0

 

☕ 자바 제네릭(Generics) 개념 & 문법 정복하기

제네릭 (Generics) 이란 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 자바에서 배

inpa.tistory.com

https://f-lab.kr/insight/understanding-java-generics-and-their-applications

 

자바 제네릭의 이해와 실무적용

자바에서 제네릭의 기본적인 개념, 실무에서의 응용 사례, 타입 이레이저와 같은 고급 주제 및 제한사항, 그리고 제네릭의 미래와 발전 방향에 대해 탐구합니다.

f-lab.kr

 

'Java' 카테고리의 다른 글

Java에서 멀티 스레드와 동기화 처리  (0) 2025.04.10
Thread와 Thread Pool  (0) 2025.03.27