RecordFieldSetMapper로 모든 케이스의 전문 변환이 커버 가능할까? => 아니다.

 

public interface ConversionService {
    override fun convert(source: Any?, sourceType: TypeDescriptor?, targetType: TypeDescriptor): Any?
}

// 위 메서드는 아래 호출 구문을 통해서 넘어오는데...

public class RecordFieldSetMapper<T> implements FieldSetMapper<T> {
    public T mapFieldSet(FieldSet fieldSet) {
        args[i] = this.typeConverter.convertIfNecessary(fieldSet.readRawString(name), type);
        ...
    }


즉, ConversionService에서 알 수 있는 정보는, [source ("   abcde  "), sourceType (String), targetType (String)] 뿐이다.

 

"   abcde  "가 FieldSet에서 어떤 name이었는지는 여기서 알 수가 없다.
따라서 name을 기반으로 찾아야만 하는 enum도 찾을 수가 없고,

enum을 찾을 수 없으니 align, tokenType도 알 수가 없다. (결과 dto type은 targetType으로 넘어와 알 수 있지만 tokenType은 알 수 없다.)

 

그렇다면... 해결 방안은? ConversionService에 name을 함께 넘겨주는 FieldSetMapper를 구현하는 방법 뿐이다.
(애너테이션 기반이든, TField 기반이든 모두 똑같다. FieldSetMapper를 구현해야만 align, tokenType을 알아낼 수 있다.)

 

But, TokenInfo를 반드시 보아야 하는가?에 대해서 생각해 볼 필요가 있다.

tokenType에 따른 디테일한 align 처리나 trim 처리가 필요하다면 직접 FieldSetMapper를 구현해야 하는게 맞으나

 

대부분의 경우 뒤쪽 공백 trim이거나, 양쪽 trim해도 상관 없고

숫자에 대한 처리는 앞에 오는 0이나 ' ' 공백을 없애버려도 무방하다.

 

즉, 대부분의 경우 RecordFieldSetMapper에 trim을 더한 StringToStringConverter만 추가하는 것으로 요구사항을 충분히 만족시킬 수 있다.

(make things right 관점에서는 FieldSetMapper를 직접 구현하는 것이 옳지만..)

 

RecordFieldSetMapper를 활용한 TelegramFieldSetMapper

/**
 * 전문 필드 타입 STRING에 대해, 항상 오른쪽 trim하는 경우만 있었기 때문에 오른쪽 trim하는 FieldSetMapper.
 * NUMBER 타입은 자동으로 leading 0s 제거하고 변환됨.
 *
 * align, trim에 대한 디테일한 처리가 필요하다면 RecordFieldSetMapper에 대한 Composition을 제거하고
 * 직접 align, trim 정보를 enum 또는 annotation으로 가져와 처리하는 방식으로 확장.
 *
 * R이 필요한 이유?
 * 실제 targetClass의 타입은 각각 ATelegramHeader, ATelegramData, ATelegramTrailer이지만
 * 필요한 타입은 이들의 상위타입인 FieldSetMapper<ATelegram>이기 때문.
 * FieldSetMapper가 무공변이기 때문에 out 지정 불가하고, 별도로 R을 써주어야 한다.
 */
class TelegramFieldSetMapper<T: Any, R: T>(
    targetType: Class<R>,
) : FieldSetMapper<T> {

    private val recordFieldSetMapper = RecordFieldSetMapper(
        targetType,
        DefaultConversionService().apply {
            this.addConverter(String::class.java, String::class.java) { it.trimEnd() }
        }
    )

    override fun mapFieldSet(fieldSet: FieldSet): T {
        return recordFieldSetMapper.mapFieldSet(fieldSet)
    }
}
@StepScope
@Bean(STEP_NAME + "ItemReader")
fun itemReader(): FlatFileItemReader<ATelegram> {
    ...

    return FlatFileItemReader<ATelegram>().apply {
        setEncoding("utf-8")
        setResource(FileSystemResource(filePath))
        setLineMapper(PatternMatchingCompositeLineMapper<ATelegram>().apply {
            setTokenizers(
                mapOf(
                    "H*" to AHeaderTokenInfo.tokenizer,
                    "D*" to ADataTokenInfo.tokenizer,
                    "T*" to ATrailerTokenInfo.tokenizer
                )
            )
            setFieldSetMappers(
                mapOf(
                    "H*" to TelegramFieldSetMapper(ATelegramHeader::class.java),
                    "D*" to TelegramFieldSetMapper(ATelegramData::class.java),
                    "T*" to TelegramFieldSetMapper(ATelegramTrailer::class.java)
                )
            )
        })
    }
}