영주의 개발노트

🏦 기술 부채 상환 | 제네릭 톺아보기(2): 유연한 코딩을 위한 두 걸음 본문

STUDY 📖/JAVA

🏦 기술 부채 상환 | 제네릭 톺아보기(2): 유연한 코딩을 위한 두 걸음

0JUUU 2024. 2. 1. 02:49

지난번에 알아봤던 제네릭 기본 지식에 이어 좀 더 알아보겠다. 

 

🏦 기술 부채 정산 | 제네릭 톺아보기(1): 유연한 코딩을 위한 한 걸음

김영한님의 '스프링 핵심 원리 - 고급편' 강의 예제 코드에서 제네릭이 등장해 순간 멈칫하여 해당 글을 작성하고자 한다. 필자는 자바를 약 4년간 사용하였음에도 제네릭에 대해 잘 모른다. 지

0juuu.tistory.com

해당 글을 통해 얻어갈 수 있는 내용

  • 제네릭 메서드 

제네릭 메서드

이전까지 제네릭 클래스에 대해 알아보았다. 경각심을 줬던 코드 중 하단의 코드를 보면 일반 클래스의 메서드에서도 타입 매개 변수를 사용하는 걸 볼 수 있다. 이렇게 타입 매개변수를 하나 이상 가지는 메서드를 제네릭 메서드라고 한다. 이 경우 타입 매개변수의 범위가 메서드 내부로 제한된다. 타입 매개변수는 반드시 메서드의 수식자와 반환형 사이에 위치되어야 한다. 

public class TraceTemplate {

    ...

    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;
        }
    }
}

 

여기서 잠깐🖐️ '제네릭 메서드에서는 타입 매개 변수의 범위가 메서드 내부로 제한된다.'는 말은 무슨 말일까?

어떤 느낌인지는 알겠지만 정확히 짚고 넘어가겠다. 제네릭 메서드에서 사용되는 타입 매개변수가 해당 메서드의 정의 내에서만 의미를 가지고, 메서드 바깥에서는 사용되지 않는다는 것을 의미한다. 제네릭 메서드가 호출될 때, 별도의 타입 매개변수의 인스턴스가 생성되며 이 매개 변수가 메서드 내에서만 유효하다는 것이다. 

 

 

 

제네릭 메서드와 제네릭 클래스의 조합

제네릭 메서드와 제네릭 클래스를 이용한 조합은 4가지 경우가 나온다. 제네릭 메서드는 제네릭 클래스 내부에서 선언되느냐, 일반 클래스에서 선언되느냐에 따라 제약조건이 달라진다.

  제네릭 클래스 O 제네릭 클래스 X
제네릭 메서드 O 1 2
제네릭 메서드 X 3 4

 

우선, 4번의 경우 우리가 흔히 생각하는 클래스와 메서드이다. 2번은 위에서 제네릭 메서드를 설명하며 보여주었기에 생략한다. 제네릭 클래스에서 제네릭 메서드를 사용하지 않을 경우 알아야 할 것이 있다. 

public class MyGenericClass<T> {
	
    public static T get(T id) {
    	return id;
    }
}

 

위 클래스의 경우 아래와 같은 사유로 컴파일 오류가 뜬다. static이 아닌 것은 static 문맥에서 사용될 수 없다는 것이다. 즉, 결론부터 말하면 제네릭 클래스에 선언된 타입 매개변수를 static으로 선언된 메서드에서 사용할 수 없다. 클래스 수준의 제네릭 타입 매개변수의 타입은 MyGenericClass가 객체로 생성될 때 개발자에 의해 결정된다. static 메서드는 인스턴스 없이 클래스 수준에서 호출된다. 그러므로 static 메서드는 객체가 생성되는 시점에 결정되는 타입 매개변수의 타입 정보를 알 수 없다. 

 

 

이제 제네릭 메서드와 제네릭 클래스를 모두 사용하는 경우에 대해 알아보자. 앞서 제네릭 메서드에서 사용되는 타입 매개변수는 해당 메서드의 범위 내에서만 의미를 가진다고 하였다. 자바의 지역변수와 전역변수를 생각하면 이해하기 쉬워진다. 제네릭 메서드에서 선언된 타입 매개변수는 지역변수, 제네릭 클래스에서 선언된 타입 매개변수는 전역변수인 것이다. 

public class MyGenericClass<T> {

    public static <T> T get1(T id) {
        return id;
    }

    public <S> T get2(T id, S name) {
        System.out.println("name = " + name);;
        return id;
    }
}

 

get1 메서드 리턴 타입과 파라미터 타입에 사용된 T는 MyGenericClass의 타입 매개변수 T가 아닌 get1 메서드의 타입 매개변수 T와 관련이 있는 것이다. get1 메서드의 T는 메서드 호출 시 결정된다. 그러므로, MyGeneric이 객체로 생성되지 않아도 get1 메서드는 사용할 수 있다. 이러한 차이로 제네릭 클래스 안에서 static을 사용할 수 있는 것처럼 보인다. get2 메서드와 같이 제네릭 클래스 내부에서 선언된 제네릭 메서드는 클래스 레벨의 타입 매개변수를 사용할 수 있다. 

 

하지만, 아래의 코드는 컴파일 오류가 발생한다. 앞서 설명한 내용을 이해했다면 어떤 부분에서 잘못되었는지 알 수 있을 것이다. 

public class MyGenericClass<T> {

    public static <S> T get1(S id) {	// 컴파일 오류
        return id;
    }
}

get1의 리턴 타입은 클래스 레벨에서 선언된 타입 매개 변수 T를 사용한다. T는 객체가 생성될 때 결정되므로 static 메서드가 호출되는 시점에서는 알 방법이 없어 컴파일 오류가 발생하는 것이다. 

 

이처럼 우리는 제네릭 클래스와 제네릭 메서드에 선언된 타입 매개변수를 독립적으로 생각해야 한다. 제네릭 클래스는 타입 매개변수를 클래스 전체에서 사용하는 반면, 제네릭 메서드는 해당 메서드 내에서만 타입 매개변수를 정의하고 사용한다. 클래스에 선언된 타입 매개변수는 객체 내에서 모두 동일한 타입을 참조한다. 반면, 제네릭 메서드에서 선언된 타입 매개변수는 메서드가 호출될 때마다 독립적으로 결정된다. 이는 같은 객체 내에서도 다양한 타입으로 메서드를 호출할 수 있음을 의미한다. 

 

의문점 해결

이전 글에서 궁금했던 부분에 대해 짚어보겠다. (이전 글 상단에서 관련 코드를 확인하길 권장한다.)

 

    1. 클래스 선언 시 <T>의 유무는 무엇에 의해 결정되는 거지? <T>는 뭐지?

    A. <T>는 타입 매개변수를 선언한 것이다. 제네릭 클래스에 선언되었는지 제네릭 메서드에 선언되었는지에 따라 해당 매개변수의 범위가 달라진다. 클래스 선언 시 <T>의 유무가 어떤 것에 의해 결정되는 것이 아니고, 개발자가 제네릭 클래스를 만들고 싶다면 클래스단에 <T>를 붙이는 것이다. 

 

    2. public <T> T라고 명시한 이유가 뭘까?

    A. TraceTemplate 은 일반 클래스이다. public <T> T는 제네릭 메서드를 나타내는 것으로 <T>라는 타입 매개변수를 선언하고 해당 메서드의 리턴값을 T라고 명시한 것이다. 

 

다시 궁금했던 부분을 살펴보니 제네릭에 대한 지식이 없었으며, 제네릭 클래스와 제네릭 메서드에 대한 차이를 알지 못해 생겼던 것들이었다. 

 

글을 마무리하며

앞선 글에서 나와 같이 의문점에 대해 답을 알고 있지 못했던 사람들이 이 시리즈를 읽고 제네릭에 대해 이해할 수 있는 기회가 되었으면 좋겠다. 

 

하지만 제네릭이 적용된 여러 코드들을 보면 <? extends T> 이런 것을 확인할 수 있다. 이 부분에 대해서는 다루지 않았다. 다음에는 '제네릭 한정된 매개변수', '와일드카드'라는 내용에 대해 정리하겠다. 

List 클래스 내부 메서드