Post

전문 해석기 배치 LineMapper - TelegramFieldSetMapper

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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
  * 전문 필드 타입 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)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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)
                )
            )
        })
    }
}
This post is licensed under CC BY 4.0 by the author.