지금까지 JAVA에서 '제네릭'이라고 하면 그냥 T로 작성해서 어떤 객체든 들어올 수 있게 하는 것 정도로만 이해하고 있다가 이번에 인터넷 강의를 들으면서 제네릭이 생각보다 더 많이 쓰이고 더 많은 내용을 담고 있어서 한번 이해해 보고자 나름대로 정리해 봤다.
JAVA 제네릭
Java에서 제네릭(Generic)은 코드의 재사용성을 높이고 타입 안전성을 강화하기 위한 중요한 기능이다. 이번 포스트에서는 제네릭의 개념과 함께 TraceTemplate 클래스 예시를 통해 제네릭 메서드가 어떻게 사용되는지 자세히 살펴보겠다.
제네릭이란?
제네릭은 코드를 작성할 때는 타입이 결정되지 않지만, 실행 시점에 타입이 결정되도록 하는 기법이다. 이는 주로 컴파일 타임에 타입 검사를 강화하고, 런타임 시 타입 안정성을 보장하기 위해 사용된다.
제네릭을 통해 클래스나 메서드는 다양한 타입에 대해 동작할 수 있게 되며, 코드의 재사용성을 높이고 타입 관련 오류를 줄일 수 있다.
제네릭 메서드란?
제네릭 메서드는 메서드 정의에 타입 파라미터를 사용하는 메서드이다. 이를 통해 메서드는 호출될 때마다 다양한 타입을 처리할 수 있는 유연성을 갖게 된다. 제네릭 메서드는 다음과 같은 구조를 가진다:
public <T> T methodName() {
// 메서드 로직
}
여기서 <T>가 메서드가 제네릭 타입 T를 사용할 것임을 선언하는 부분이고, T는 메서드의 반환 타입을 지정하는 부분이다.
예시: TraceTemplate 클래스
다음은 제네릭 메서드를 포함한 TraceTemplate 클래스의 예시이다:
TraceCallback 인터페이스
public interface TraceCallback<T> {
T call();
}
TraceCallback 인터페이스는 제네릭 타입 T를 사용하여 call 메서드를 정의하고 있다. 이는 call 메서드가 T 타입의 값을 반환한다는 의미이다.
TraceTemplate 클래스
public class TraceTemplate {
private final LogTrace trace;
public TraceTemplate(LogTrace trace) {
this.trace = trace;
}
public <T> T execute(String message, TraceCallback<T> callback) {
TraceStatus status = null;
try {
status = trace.begin(message);
// 로직 호출
T result = callback.call();
trace.end(status);
return result;
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
제네릭 메서드 분석
execute 메서드는 제네릭 메서드로 정의되어 있으며, 다음과 같은 구조를 가진다:
- 타입 파라미터 선언(<T>): 메서드가 제네릭 타입 T를 사용할 것임을 선언한다. 여기서 T는 메서드 호출 시에 구체적인 타입으로 대체된다.
- 반환 타입(T): 메서드가 T 타입의 값을 반환함을 나타낸다. 구체적인 타입은 메서드 호출 시 결정된다.
제네릭 메서드의 유용성
TraceTemplate 클래스의 execute 메서드는 제네릭 메서드로, 다양한 타입의 작업 결과를 반환할 수 있는 유연성을 제공한다.
예를 들어, TraceCallback 인터페이스의 구현에 따라 반환 타입 T가 결정된다. 만약 TraceCallback이 주어지면, execute 메서드는 String 타입을 반환한다.
이러한 구조를 통해 개발자는 하나의 메서드로 다양한 타입의 작업을 처리할 수 있어 코드의 재사용성을 높일 수 있다. 또한, 제네릭을 사용하면 컴파일 타임에 타입 검사를 수행하므로 타입 안전성을 강화할 수 있다.
예제 코드
다음은 TraceTemplate 클래스와 TraceCallback 인터페이스를 사용하는 예제 코드이다:
public class Example {
public static void main(String[] args) {
LogTrace trace = new LogTraceImpl();
TraceTemplate template = new TraceTemplate(trace);
// String 타입을 반환하는 TraceCallback 구현
String result = template.execute("Hello", new TraceCallback<String>() {
@Override
public String call() {
return "Hello, World!";
}
});
System.out.println(result); // 출력: Hello, World!
// Integer 타입을 반환하는 TraceCallback 구현
Integer numberResult = template.execute("Number", new TraceCallback<Integer>() {
@Override
public Integer call() {
return 42;
}
});
System.out.println(numberResult); // 출력: 42
}
}
제약 사항
제네릭의 타입 소거(Type Erasure)
제네릭은 컴파일 타임에 타입 검사를 수행하고, 런타임에는 모든 제네릭 타입이 Object 타입으로 취급된다. 이를 타입 소거라고 하며, 이는 제네릭이 컴파일 타임에만 유효하다는 것을 의미한다.
제네릭의 한계
제네릭을 사용할 때 몇 가지 제약이 있다. 예를 들어, 제네릭 타입 파라미터로 배열을 생성할 수 없으며, 제네릭 타입 파라미터에 기본 타입을 사용할 수 없다.
제네릭 타입 파라미터로 배열 생성 불가:
public class GenericArray<T> {
private T[] array;
public GenericArray(int size) {
// array = new T[size]; // 컴파일 오류
array = (T[]) new Object[size]; // 타입 캐스팅을 사용한 우회 방법
}
}
위 예제에서 new T[size]와 같은 방식으로 제네릭 타입 파라미터로 배열을 직접 생성할 수 없다.
대신, new Object[size]로 생성한 후에 타입 캐스팅을 사용하는 우회 방법이 있다.
기본 타입 사용 불가:
public class GenericClass<T> {
private T value;
public GenericClass(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
// GenericClass<int> obj = new GenericClass<>(10); // 컴파일 오류
GenericClass<Integer> obj = new GenericClass<>(10); // 박싱을 사용한 우회 방법
제네릭 타입 파라미터로 기본 타입(int, char 등)을 사용할 수 없다. 대신, 래퍼 클래스(Integer, Character 등)을 사용해야 한다.
제네릭의 상한 및 하한 바운드
제네릭의 상한 및 하한 바운드를 사용하면 타입 파라미터에 제약을 걸 수 있다.
상한 바운드(Upper Bound):
public <T extends Number> void printNumber(T number) {
System.out.println(number);
}
이렇게 하면 printNumber 메서드는 Number 타입의 서브 타입만 허용한다.
Number 클래스는 Java의 추상 클래스이며, 여러 기본 숫자 타입의 슈퍼 클래스이다. 예를 들어, Integer, Double, Float, Long, Byte, Short 등이 Number 클래스를 상속받는다. 이를 통해 다양한 숫자 타입을 처리할 수 있는 공통 인터페이스를 제공한다.
하한 바운드(Lower Bound)
public <T super Integer> void addNumber(List<T> list) {
list.add(Integer.valueOf(42));
}
위 예제에서 addNumber 메서드는 Integer 타입의 슈퍼 타입만 허용한다.
결론
제네릭 메서드는 다양한 타입을 유연하게 처리할 수 있는 강력한 도구이다. TraceTemplate 클래스와 같은 예시를 통해 제네릭 메서드의 개념과 사용법을 이해할 수 있다. 제네릭을 잘 활용하면 코드의 재사용성과 타입 안전성을 크게 향상할 수 있다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
중첩 foreach 제거: Function.identity()로 코드 간결화하기 (0) | 2024.05.23 |
---|---|
폴링(Polling) 사용해서 CompletableFuture 결과 확인하기 (0) | 2024.02.07 |
CompletableFuture를 활용한 비동기 메일 전송 구현 (1) | 2024.02.06 |
AtomicInteger를 활용한 파일 저장 순서 동기화 문제 해결 (0) | 2024.02.04 |
[JAVA] 코드 최적화를 위한 매직 상수 사용법 (4) | 2023.12.31 |