자기 제한 타입은 복잡해 보이지만, 사실은 하위 클래스가 타입 인수를 마음대로 바꿔 버리지 못하게 막기 위한 방법입니다. A라는 제네릭 클래스가 있고, 이 클래스는 어떤 타입의 인수든 받을 수 있다고 해봅시다:
class A<T> {
T property;
void setProperty(T t) { property = t; }
T getProperty() { return property; }
}
A에는 A<Integer>나 A<String>처럼 어떤 타입의 인수라도 사용할 수 있습니다. B라는 클래스가 A를 상속하되 A의 타입 인수를 B로 고정하고 싶다면 보통 다음과 같이 작성합니다.
class B extends A<B> {}
이 패턴을 F-유계 다형성(CRTP)이라고 합니다. 쉽게 말해, B가 A를 상속하고, A는 내부에서 다시 B를 타입 인수로 사용하는 구조입니다.
문제점은 어디에?
문제는 이 방식만으로는 B 자체를 타입 인수로 반드시 사용하도록 강제할 수 없다는 점입니다. 예를 들어 다음과 같이 작성할 수도 있습니다.
class C {}
class B extends A<C> {}
이렇게 하면 B는 A를 상속하면서도 타입 인수로 C를 사용하게 됩니다. 우리가 원하던 규칙은 완전히 깨지는 셈입니다.
자기 제한 타입의 해결 방법
이 문제를 해결하려면 자기 제한 타입을 사용합니다.
class A<T extends A<T>> {}
이렇게 바꾸면 T라는 제네릭 매개변수는 A<T>를 상속하는 타입이어야 합니다. 즉, 하위 클래스가 A를 상속할 때 자기 자신을 타입 인수로 전달해야 합니다.
이제 다음과 같습니다.
class C {}
class B extends A<C> {} // 编译失败
위 코드는 C가 A<C>를 상속하지 않으므로 컴파일되지 않습니다. 반대로 다음 코드는 유효합니다.
class B extends A<B> {} // 编译通过
그러면 B가 A를 상속할 때 타입 인수를 B 자신으로만 사용할 수 있도록 보장됩니다.
자기 제한 타입의 실제 사례: MyBatis-Plus Wrapper
MyBatis-Plus는 널리 쓰이는 ORM 프레임워크이며, Wrapper는 쿼리 조건을 구성하는 데 자주 쓰이는 도구 클래스입니다. Wrapper 클래스와 그 하위 클래스인 QueryWrapper, UpdateWrapper 등은 구현에서 자기 제한 타입을 대량으로 활용해 반환 타입이 호출 시점의 구체적인 타입과 일치하도록 보장합니다. 덕분에 플루언트 API(fluent API) 형태의 호출을 지원할 수 있습니다.
Wrapper 정의
AbstractWrapper 클래스의 단순화된 정의를 살펴보겠습니다.
public abstract class AbstractWrapper<T, R, This extends AbstractWrapper<T, R, This>> {
// 假设有一些字段和方法
public This eq(R column, Object val) {
// 实现逻辑
return (This) this;
}
public This like(R column, Object val) {
// 实现逻辑
return (This) this;
}
// 其他条件方法...
}
이 정의에서 This는 AbstractWrapper 자체를 상속하도록 제한된 자기 제한 타입 매개변수입니다. 즉, AbstractWrapper를 상속하는 모든 클래스는 This의 타입 인수로 자기 자신을 전달해야 합니다. 따라서 체이닝한 호출이 기본 타입이 아니라 구체적인 하위 타입을 반환하도록 보장됩니다.
QueryWrapper 구현
이어서 QueryWrapper의 단순화된 구현을 살펴보겠습니다.
public class QueryWrapper<T> extends AbstractWrapper<T, String, QueryWrapper<T>> {
// 这里可以定义额外的查询条件
}
QueryWrapper는 AbstractWrapper를 상속하면서 세 번째 제네릭 인수로 QueryWrapper<T>를 전달합니다. 따라서 eq나 like 같은 메서드를 호출해도 반환 타입은 계속 QueryWrapper<T>이며, 플루언트 API의 일관성이 유지됩니다.
예제 코드
User 테이블이 있고 QueryWrapper를 사용해 쿼리 조건을 구성하려 한다고 해봅시다.
위 코드에서 eq와 like 메서드는 모두 QueryWrapper<User> 객체를 반환하므로, 다른 조건 메서드도 계속 체이닝할 수 있습니다.
자기 제한 타입의 역할
이 설계에서 자기 제한 타입의 역할은 매우 뚜렷합니다.
- 타입 안전성: 잘못된 타입이 반환되는 것을 방지합니다. 예를 들어
QueryWrapper의eq메서드가 부모 클래스인AbstractWrapper가 아니라 다시QueryWrapper를 반환하도록 보장합니다. - 체이닝: 자기 제한 타입을 사용하면 메서드 체인이 끊기지 않도록 보장되어 코드가 더 간결하고 직관적이 됩니다.
한마디로 정리하면
자기 제한 타입은 클래스가 상속될 때 자기 자신을 제네릭 인수로 사용하도록 강제하는 기법으로, 타입 전달 과정에서 오류를 방지합니다. 겉으로는 복잡해 보이지만, 타입을 엄격하게 제어해야 하는 상황에서는 꽤 유용합니다. 실제 개발에서는 MyBatis-Plus처럼 자기 제한 타입을 활용하는 프레임워크가 더 우아하고 타입 안전한 API 설계를 가능하게 합니다.