Skip to content

Latest commit

 

History

History
103 lines (81 loc) · 8.66 KB

File metadata and controls

103 lines (81 loc) · 8.66 KB

02. Conversion — 타입 변환 시스템

무엇을 / 왜

설정 값은 거의 항상 문자열로 들어온다. "true", "30s", "1,2,3", "com.example.Foo". 그런데 빈의 프로퍼티는 boolean, Duration, List<Integer>, Class<?> 같은 강타입이다. 이 간극을 메우는 일이 타입 변환이며, 과거 JavaBeans PropertyEditor의 한계(상태 보유, 비스레드세이프, String 중심)를 넘어서기 위해 Spring 3.0이 도입한 것이 core.convert 패키지다.

핵심 약속은 세 가지다. (1) 스레드세이프한 단일 진입점 ConversionService. (2) 단순/복합을 아우르는 계층화된 Converter SPI. (3) 필드의 제네릭·어노테이션 맥락까지 전달하는 TypeDescriptor. 이 시스템은 spring-beans의 데이터 바인딩, @Value 해석, Spring MVC 파라미터 변환의 공통 엔진이다.

소스: spring-core/src/main/java/org/springframework/core/convert/

핵심 타입

   진입점
   ┌─────────────────────┐   convert(source, srcTD, tgtTD)
   │  ConversionService  │   canConvert(...)
   └──────────▲──────────┘
              │ extends (등록 API 추가)
   ┌─────────────────────────────┐
   │ ConfigurableConversionService│  = ConversionService + ConverterRegistry
   └──────────▲──────────────────┘
              │ implements
   ┌─────────────────────────────┐
   │  GenericConversionService    │  실제 엔진 (매칭 + 캐시)
   └──────────▲──────────────────┘
              │ extends
   ┌─────────────────────────────┐
   │  DefaultConversionService    │  + 기본 변환기 100여 개 사전 등록
   └─────────────────────────────┘

   Converter SPI (단순 → 복잡)
   ┌──────────────────────┬──────────────────────────────────────────┐
   │ Converter<S,T>       │ 1:1 변환. 가장 단순.                       │
   │ ConverterFactory<S,R>│ S → R의 하위타입군 (예: String→모든 Enum) │
   │ GenericConverter     │ 여러 타입쌍 + TypeDescriptor 맥락 접근     │
   │ ConditionalConverter │ 매칭 시점에 적용 여부 동적 판단            │
   └──────────────────────┴──────────────────────────────────────────┘
   내부적으로 Converter/ConverterFactory 는 모두 GenericConverter 로 어댑팅된다.

TypeDescriptor — 변환의 "맥락"

Class만으로는 List<Integer>List<String>을 구분할 수 없다. TypeDescriptor는 대상 타입에 더해 제네릭 정보(ResolvableType 기반), 어노테이션, 배열/컬렉션/맵 요소 타입까지 담는 컨텍스트 객체다. forObject(source)로 인스턴스에서, 필드/메서드파라미터에서도 만들 수 있다. 변환기는 이 맥락을 보고 "필드에 붙은 @DateTimeFormat" 같은 정보까지 반영할 수 있다.

동작 흐름 — convert() 한 번의 여정

GenericConversionService.convert(source, sourceType, targetType)가 전체 흐름의 중심이다.

   convert(source, srcTD, tgtTD)
        │
        ├─ srcTD == null ?  → null 소스 처리(convertNullSource), 끝
        │
        ├─ source 가 srcTD 인스턴스인지 검증 (아니면 IllegalArgument)
        │
        ▼
   getConverter(srcTD, tgtTD)            ← 핵심: 적합한 GenericConverter 찾기
        │   ┌──────────────────────────────────────────────┐
        │   │ 1) converterCache 조회 (ConverterCacheKey)     │
        │   │ 2) miss 면 Converters.find(srcTD,tgtTD):       │
        │   │    - src/tgt 의 클래스 계층을 BFS 로 펼쳐       │
        │   │      모든 (S,T) 쌍 후보를 순서대로 시도         │
        │   │    - ConditionalConverter.matches() 통과해야 채택 │
        │   │ 3) 못 찾으면 NO_MATCH 캐싱                       │
        │   └──────────────────────────────────────────────┘
        ▼
   converter != null ?
        ├─ yes → ConversionUtils.invokeConverter(...) → handleResult
        └─ no  → handleConverterNotFound (ConverterNotFoundException 등)

핵심은 타입 계층 탐색 매칭이다. getConverter는 소스/타깃의 슈퍼클래스·인터페이스를 펼쳐 가능한 모든 ConvertiblePair를 우선순위 순서로 시도하고, 후보가 ConditionalConverter라면 matches()까지 통과해야 선택한다. 결과는 ConcurrentReferenceHashMap 캐시에 키(ConverterCacheKey)로 저장되어 다음 호출부터는 매칭 비용을 건너뛴다.

등록의 정규화 — 모든 게 GenericConverter가 된다

addConverter(Converter)를 호출하면 내부에서 제네릭 시그니처(<S,T>)를 ResolvableType으로 뽑아내(getRequiredTypeInfo) ConverterAdapter로 감싼다. ConverterFactoryConverterFactoryAdapter로 감싼다. 즉 레지스트리 내부에는 오직 GenericConverter만 존재한다. 시그니처를 못 뽑으면(타입 파라미터 미지정) 명확한 예외를 던진다 — 람다 컨버터를 등록할 때 addConverter(Class,Class,Converter) 오버로드를 써야 하는 이유다.

// addConverter(Converter) — 시그니처 추출 후 어댑팅
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
if (typeInfo == null && converter instanceof DecoratingProxy dp) {  // 프록시면 원본 클래스로
    typeInfo = getRequiredTypeInfo(dp.getDecoratedClass(), Converter.class);
}
if (typeInfo == null) throw new IllegalArgumentException("...does the class parameterize those types?");
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));

핵심 메서드

  • canConvert(src, tgt): getConverter가 null이 아닌지로 판단. 단 컬렉션/배열/맵은 "원소가 변환 불가여도 true"를 반환할 수 있다는 점을 주석이 명시한다 — 실제 변환에서 ConversionException이 날 수 있으니 호출자가 처리해야 한다.
  • canBypassConvert(src, tgt): 변환기가 NO_OP_CONVERTER인지, 즉 "그냥 원본을 그대로 돌려줘도 되는 경우"인지 알려줘 불필요한 변환을 건너뛰게 한다.
  • DefaultConversionService.addDefaultConverters(registry): String↔Number/Boolean/Enum/UUID/Charset/Locale, Collection↔Array↔Object, Map↔Map, Object→Optional, 그리고 ObjectToStringConverter/FallbackObjectToStringConverter(최후의 toString())까지 수십 개를 한 번에 등록하는 정적 유틸. getSharedInstance()로 지연 초기화된 공용 인스턴스를 제공한다.

설계 포인트 / 확장점

  • SPI 계층화: 90%는 Converter<S,T>로 충분하고, "하나의 부모로 여러 자식 타입"은 ConverterFactory, "맥락 필요 + 다중 타입쌍"은 GenericConverter로 단계적으로 복잡도를 올린다. 문서가 명시적으로 "단순한 걸로 충분하면 GenericConverter 쓰지 말라"고 권한다.
  • ConditionalConverter: 동일 타입쌍에 대해 어노테이션 유무 등으로 적용을 분기. ConditionalGenericConverter는 둘을 합친 형태.
  • 캐시 무효화: 컨버터를 추가/제거하면 invalidateCache()로 매칭 캐시를 비운다 — 런타임 동적 등록을 안전하게 한다.
  • PropertyEditor 다리: ConvertingPropertyEditorAdapter가 레거시 PropertyEditor 세계와 새 변환 세계를 연결한다.

정리

core.convert는 "값 ↔ 타입" 변환을 스레드세이프한 ConversionService 한 점으로 모으고, 그 뒤에 ConverterConverterFactoryGenericConverter로 복잡도를 단계화한 SPI를 둔다. 모든 등록은 GenericConverter로 정규화되어 타입 계층 탐색으로 매칭되고 캐시된다. TypeDescriptor가 제네릭·어노테이션 맥락을 운반해, 단순 클래스 변환을 넘어 List<Integer>@DateTimeFormat 같은 실제 필드 의미까지 다룬다.