LineMapper는 크게 Tokenizer와 FieldSetMapper로 이루어진다.

전문 특성상

LineMapper로는 PatternMatchingCompositeLineMapper

Tokeinizer로는 FixedLengthTokenizer를 사용하면 되는데

 

FieldSetMapper로는 세 가지 선택지가 있다.

1. BeanWrapperFieldSetMapper

2. RecordFieldSetMapper

3. 직접 구현

 

이 중 SpringBoot에서 기본 제공하는 1, 2에 대해 비교해보았다.

class SampleModel() {
    var field1: String? = null
    var field2: Int? = null
    var field3: Double? = null
    var field4: BigDecimal? = null
}
data class SampleRecord(
    val field1: String,
    val field2: Int,
    val field3: Double,
    val field4: BigDecimal
)
class FieldSetMappersTest {

    fun tokenize(): FieldSet {
        val line = "   abcde  000001234500.8000.51"
        val tokenizer = FixedLengthTokenizer().apply {
            setNames("field1", "field2", "field3", "field4")
            setColumns(Range(1, 10), Range(11, 20), Range(21, 25), Range(26, 30))
        }
        return tokenizer.tokenize(line)
    }

    @Test
    fun tokenizerTest() {
        val fs = tokenize()

        assert(fs.names[0] == "field1")
        assert(fs.names[1] == "field2")
        assert(fs.names[2] == "field3")
        assert(fs.names[3] == "field4")
        assert(fs.values[0] == "   abcde  ")
        assert(fs.values[1] == "0000012345")
        assert(fs.values[2] == "00.80")
        assert(fs.values[3] == "00.51")
    }


    @Test
    fun beanWrapperFieldSetMapperTest() {
        val fs = tokenize()

        val beanWrapperFieldSetMapper = BeanWrapperFieldSetMapper<SampleModel>().apply {
            setTargetType(SampleModel::class.java)
        }
        // BeanWrapperFieldSetMapper를 쓰려면 SampleModel에 반드시 NoArgsConstructor가 필요하다.

        val sampleModel = beanWrapperFieldSetMapper.mapFieldSet(fs)
        assert(sampleModel.field1 == "abcde")   // 앞뒤 trim이 자동으로 들어간다. 왜?
        assert(sampleModel.field2 == 12345)
        assert(sampleModel.field3 == 0.8)
        assert(sampleModel.field4 == "0.51".toBigDecimal())

        /**
         * 전문 field 앞이나 뒤 한쪽에 의도적으로 공백을 넣는 경우가 있을까? 그런 경우가 있다면 대응이 안될 것 같다.
         * BeanWrapperFieldSetMapper도 깊숙히 들어가면 ConversionService를 쓰는 것 같긴 한데...
         * 아무튼 ConversionService를 직접 컨트롤 할 수 있는 RecordFieldSetMapper가 더 유연해보인다.
         * 프로토타입 빈을 쓸 필요가 없는 대부분의 경우 Record쪽이 더 낫지 않을까?
         */
    }

    @Test
    fun recordFieldSetMapperTest() {
        val fs = tokenize()

        /**
         * ConversionService를 명시하지 않는다면 DefaultConversionService가 설정된다.
         * SampleRecord는 반드시 java record 일 필요는 없다. kotlin data class와도 잘 호환 된다.
         * 생성자 기반 매핑이기 때문에 SampleRecord는 반드시 생성자에 매핑 대상 필드들을 명시해야 한다.
         * fieldSet names와 생성자 파라미터의 파라미터 명 전체가 정확히 일치하지 않으면 IllegalArgumentException 발생한다. (good!)
         */
        val recordFieldSetMapper = RecordFieldSetMapper(SampleRecord::class.java)

        val sampleRecord = recordFieldSetMapper.mapFieldSet(fs)
        assert(sampleRecord.field1 == "   abcde  ")   // 앞뒤 trim 자동으로 들어가지 않는다.
        assert(sampleRecord.field2 == 12345)
        assert(sampleRecord.field3 == 0.8)
        assert(sampleRecord.field4 == "0.51".toBigDecimal())
    }
}

 

 

 

BeanWrapperFieldSetMapper에서 자동으로 trim이 되는 이유?

public class DefaultFieldSet implements FieldSet {
    @Override
    public Properties getProperties() {
        if (names == null) {
            throw new IllegalStateException("Cannot create properties without meta data");
        }
        Properties props = new Properties();
        for (int i = 0; i < tokens.length; i++) {
            String value = readAndTrim(i);
            if (value != null) {
                props.setProperty(names.get(i), value);
            }
        }
        return props;
    }
}

내부에서 property 변환하면서 앞뒤 trim 한다.

 

ConversionService Hierarch

 

그렇다면 전문 변환 시 사용할만한 기본 제공 FieldSetMapper는?

  • trim이나 align에 대한 세부적인 처리가 필요하지 않은 경우 -> RecordFieldSetMapper
  • trim이나 align에 대한 세부적인 처리가 필요한 경우 -> FieldSetMapper 직접 구현 필요.