Enum 클래스는 Enum<E extends Enum<E>>로 선언되어 있으며, 이는 F-한정 다형성(또는 재귀적 유형 한정)으로 알려진 패턴입니다. 이 선언은 유형 매개변수 E가 자신에 의해 매개변수화된 Enum의 하위 클래스가 되도록 제한합니다. 이는 각 구체적인 열거형 유형(예: DayOfWeek)을 자신의 클래스 리터럴에 연결합니다. 이 설계는 compareTo 메서드가 매개변수를 원시 Enum이 아닌 유형 E로 선언할 수 있도록 하여, 컴파일 타임에 DayOfWeek가 다른 DayOfWeek와만 비교될 수 있도록 보장합니다. 따라서 컴파일러는 런타임 instanceof 확인이나 캐스트 없이 교차 유형 서수 비교를 방지하여 유형 안전성과 서수 기반 정렬의 성능을 유지합니다.
개발 팀은 where() 및 limit()와 같은 기본 메서드가 특정 하위 클래스 유형을 반환해야 하는 유창한 QueryBuilder API를 데이터 접근 계층을 위해 설계해야 했습니다. 이는 파생된 빌더에서 메서드 체이닝을 가능하게 하기 위해 필요합니다, 예를 들어 SqlQueryBuilder나 GraphQlQueryBuilder와 같은 경우입니다.
해결책 1: 명시적 오버라이드로 상관 반환 유형.
각 하위 클래스는 자신의 특정 반환 유형을 선언하기 위해 모든 유창한 메서드를 오버라이드할 수 있습니다. 이는 컴파일 타임 안전성을 제공하지만, 기본 API가 발전할 때마다 모든 하위 클래스에서 보일러플레이트 코드가 필요하여 심각한 유지보수 오버헤드를 발생시키고 상속 계층에서 DRY 원칙을 위반합니다.
해결책 2: 검사되지 않은 캐스트를 사용하는 원시 유형 반환.
기본 클래스는 원시 QueryBuilder 유형을 반환할 수 있으며, 하위 클래스는 this를 특정 유형으로 캐스트해야 합니다. 이 접근 방식은 보일러플레이트를 제거하지만, 컴파일러 경고가 발생하고, 상속 구조가 복잡해질 경우 런타임에서 ClassCastException 위험이 발생하여 본질적으로 유형 안전성을 손상시킵니다.
해결책 3: F-한정 다형성.
팀은 기본 클래스를 abstract class QueryBuilder<T extends QueryBuilder<T>>로 선언하고 유창한 메서드는 T를 반환하도록 하였습니다. 하위 클래스는 자신을 class SqlQueryBuilder extends QueryBuilder<SqlQueryBuilder>로 정의했습니다. 이 기법은 Enum과 동일한 재귀적 한정 패턴을 활용하여, 컴파일러가 where()가 정확히 SqlQueryBuilder를 반환하고 캐스트나 메서드 중복 없이 보장합니다.
팀은 해결책 3을 선택했습니다. 이로 인해 코드 중복이 제거되었고 상속 체인 전체에서 유형 안전성이 유지되었습니다. 결과적으로 DSL은 일반 작업 후 서브클래스 특유의 메서드를 올바르게 제안하도록 자동 완성을 허용하여 API 도입 단계에서 통합 결함을 40% 줄였습니다.
질문 1: Enum<E extends Enum<E>> 선언이 필요한 이유는 무엇인가요? 단순히 Enum<E>라고 선언하는 것과는 어떻게 다른가요?
단순히 Enum<E>를 선언하게 되면 무작위 유형이 매개변수로 전달될 수 있어 특정 열거형 유형만이 허용되지 않습니다. 재귀적 한정 E extends Enum<E>는 E가 자신으로 매개변수화된 Enum을 확장하는 구체적인 열거형 클래스가 되도록 강제합니다. 이 자기 참조 제약은 메서드가 compareTo(E o)와 같이 정확히 열거형 하위 유형만을 수용할 수 있도록 하여, 런타임 ClassCastException으로 감지를 미루는 대신 컴파일 타임에 교차 유형 비교를 방지합니다. 이 한정이 없으면 Comparable 구현은 원시 Enum이나 Object를 수용해야 하여, 효율적인 EnumSet 및 EnumMap 구현을 가능하게 하는 유형 특수성을 잃게 됩니다.
질문 2: F-한정 다형성이 열거형 상수를 검색할 때 리플렉션과 어떻게 상호작용합니까?
열거형 클래스에서 getEnumConstants()를 리플렉션으로 호출할 때, 재귀적 한정은 반환된 배열의 유형이 원시 객체 배열이 아닌 E[]로 지정되도록 보장합니다. 이는 Enum 생성자가 getDeclaringClass()를 통해 Class<E> 객체를 캡처하기 때문에 가능합니다. 여기서 유형 매개변수가 올바르게 특정 서브클래스에 바인딩되어야 하기 때문입니다. 후보자들은 이 바인딩이 JVM이 열거형에 대한 스위치 문을 최적화하는 것을 가능하게 하여 tableswitch 바이트코드 명령어를 사용하도록 한다는 점을 종종 간과합니다. 컴파일러는 바인딩된 유형 정보를 통해 컴파일 타임에 정확한 유한 상수 집합을 알고 있으므로 느린 lookupswitch를 피할 수 있습니다.
질문 3: 재귀적 유형 한정이 제너릭 배열 생성을 통해 힙 오염을 초래할 수 있는지, 그리고 Enum이 이를 어떻게 회피하는지요?
한정 자체는 유형 안전성이 있지만, 후보자들은 유형 매개변수의 배열을 생성하려고 시도할 때 종종 실패합니다(예: new E[10]). 유형 소거로 인해 이것은 금지됩니다. 하지만, Enum 클래스는 컴파일러 마법을 통해 이 한계를 우회합니다: 컴파일러는 각 열거형에 대해 E[]를 반환하는 인위적인 정적 values() 메서드를 생성하며, 이는 재귀적 한정에서 가져온 특정 열거형의 Class 토큰으로 java.lang.reflect.Array.newInstance()를 사용하여 배열을 구성합니다. 이는 반환된 배열이 올바른 구체화된 구성 요소 유형을 가지도록 하고, ClassCastException이나 힙 오염을 초래하지 않습니다. 수동 제너릭 클래스는 리플렉션 없이 이를 쉽게 복제할 수 없습니다.