사이드 프로젝트를 하다가 ResponseWrapper 역할을 하는 ApiResponse<T> 클래스를 만들던 중 정적 메서드를 정의하면서 아래와 같은 코드를 작성했다.
public static ApiResponse<T> of(HttpStatus httpStatus, T data) {
return new ApiResponse<>(httpStatus, httpStatus.value(), data);
}
그런데 컴파일러는 이렇게 반응했다.
error: non-static type variable T cannot be referenced from a static context
에러 메시지 그대로 static한 문맥에서는 비정적(non-static) 타입 변수인 T를 참조할 수 없다는 얘기다.
public static ApiResponse<T> of(HttpStatus httpStatus, T data)
"아니 T data를 넘겼는데? 컴파일러는 그 타입이 뭔지 볼 수 있지 않나?"
라는 생각이 먼저 들었다.
그런데 조금만 더 생각해보니 이게 사실 말이 안 되는 구조였다.
static 메서드는 인스턴스가 생성되기 전에 실행 가능한데 클래스에 붙은 T는 인스턴스가 생성되어야만 어떤 타입인지 결정된다.
그런 T를 static 메서드 안에서 쓰는 건 시점적으로 모순이 생긴다.
public class ApiResponse<T> {
T data;
public static ApiResponse<T> of(HttpStatus status, T data) {
return new ApiResponse<>(status, data); // 컴파일 오류 발생
}
}
ApiResponse는 제네릭 타입 T를 가지고 있지만 of() 메서드는 static이다.
즉 인스턴스를 만들지 않아도 호출이 가능하다.
문제는 클래스에 선언된 T는 클래스를 사용할 때(즉, 객체를 만들거나, 그 타입으로 변수를 선언할 때) 구체적인 타입이 정해진다는 점이다.
ApiResponse<T>는 클래스 전체에 제네릭 T를 적용하지만,
static 메서드는 인스턴스와 무관하게 클래스 레벨에서 동작하기 때문에 클래스에 선언된 T를 사용할 수 없다.
그래서 컴파일러는 이 메서드 안에서 T가 뭔지 알 수 없다는 것이다.
클래스에 선언된 T는 객체가 만들어져야만 사용할 수 있는데
그런데 static 메서드는 "아직 뭘 담을지도 모르는 상태"에서 실행되니까
"어떤 걸 넣어야 할지(T가 뭔지) 난 모르겠어!"라고 말하는 것이다.
<T> 직접 선언하기public static <T> ApiResponse<T> of(HttpStatus status, T data) {
return new ApiResponse<>(status, status.value(), data);
}
여기서 <T>는 이 메서드 안에서만 사용할 수 있는 타입 변수다.
클래스의 T와는 전혀 다른 독립적인 제네릭이다.
즉 컴파일러한테 이렇게 말하는 것이다
"이 static 메서드를 호출할 땐 T라는 타입 하나를 새로 정해서 쓸게"
ApiResponse<String> response = ApiResponse.of(HttpStatus.OK, "data");
이 코드가 컴파일되는 이유는 메서드에 <T>가 선언되어 있어서 컴파일러가 "data"의 타입을 보고 T를 자동으로 추론할 수 있기 때문이다.
"즉 이 static 메서드는 호출될 때마다 새로운 T를 유추해서 쓸 수 있는 독립적인 메서드로 작동한다."
public class ApiResponse<T> {
public static <T> ApiResponse<T> of(T data) { ... }
}
"어? 클래스에도 T 있고, 메서드에도 T 있는데 같은 거 아냐?"
내 생각이 틀렸었다.
완전히 별개의 타입 변수다.
클래스의 T는 객체가 생성될 때 결정되는 제네릭
메서드의 <T>는 그 메서드 안에서만 쓰는 새로운 제네릭 변수
같은 이름을 썼을 뿐, 역할이나 범위가 완전히 다르다.
public class ApiResponse<C> {
public static <M> ApiResponse<M> of(M data) {
return new ApiResponse<>(data); // 클래스의 C와 무관
}
}
이렇게 C는 클래스용, M은 메서드용으로 확실히 분리해서 쓰면 더 직관적으로 알 수 있다.
| 항목 | 설명 |
|---|---|
| 클래스의 T | 인스턴스가 만들어질 때 결정됨 |
| static 메서드 | 인스턴스 없이 호출됨 |
| 에러 원인 | static 메서드에서 클래스의 T를 사용할 수 없음 |
| 해결 방법 | 메서드 자체에 를 선언해서 새로운 타입 변수로 지정 |
| 클래스 T와 메서드 | 이름이 같더라도 전혀 다른 타입 변수 (스코프 다름) |
이 경험을 통해 확실히 알게 된 건 하나다.
제네릭은 문법이 아니라 “타입이 언제 결정되는가”에 대한 감각이 중요하다.
처음엔 그냥 외워야 하나 싶었는데,
static은 인스턴스 없이 먼저 실행되고 제네릭 타입은 인스턴스가 사용되면서 타입이 결정되기 때문에
이 둘의 타이밍 차이를 깨닫는 것이 이번 오류의 본질이었다는 걸 확실히 느꼈다.
결국 이 둘을 동시에 쓰려면
메서드에서 직접 제네릭 타입을 선언해주는 것 외에는 방법이 없다.