diff --git a/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregator.java b/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregator.java index b8d2f5df..ec1dcb90 100755 --- a/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregator.java +++ b/Batch/org.egovframe.rte.bat.core/src/main/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregator.java @@ -18,9 +18,6 @@ import org.springframework.batch.item.file.transform.ExtractorLineAggregator; import org.springframework.util.Assert; -import java.util.ArrayList; -import java.util.List; - /** * Object 배열로 구성된 item 정보들을 Write 하기위해 fixedLength 방식으로 String화 하는 클래스 * @@ -37,21 +34,11 @@ */ public class EgovFixedLengthLineAggregator extends ExtractorLineAggregator { - /** - * paddingList 생성 사이즈 - */ - private static final int PADDING_LISTSIZE = 100; - /** * 각 field가 차지 할 length 배열 */ private int[] fieldRanges; - /** - * 사용할 padding들을 저장하고 있는 list - */ - private List paddingList; - /** * Padding Pattern */ @@ -83,9 +70,6 @@ public void setFieldRanges(int[] fieldRanges) { */ @Override protected String doAggregate(Object[] fields) { - if (paddingList == null) { - createPaddingList(); - } Assert.notNull(fieldRanges, "This argument is required : It must not be null"); return aggregateFixedLength(obtainFieldValueLength(fields), fields); } @@ -111,17 +95,9 @@ private String aggregateFixedLength(int[] fieldValueLength, Object[] fields) { if (fieldRanges[k] >= fieldValueLength[k]) { value.append(fields[k].toString()); if (fieldRanges[k] > fieldValueLength[k]) { + // 부족한 길이만큼 padding 문자로 채운다. (무상태 생성 → thread-safe) int needPaddingSize = fieldRanges[k] - fieldValueLength[k]; - if (needPaddingSize <= PADDING_LISTSIZE) { - value.append(paddingList.get(needPaddingSize - 1)); - } else { - int addMaxPaddingCount = needPaddingSize / PADDING_LISTSIZE; - int remainderPaddingSize = needPaddingSize % PADDING_LISTSIZE; - value.append(String.valueOf(paddingList.get(PADDING_LISTSIZE - 1)).repeat(addMaxPaddingCount)); - if (remainderPaddingSize != 0) { - value.append(paddingList.get(remainderPaddingSize - 1)); - } - } + value.append(String.valueOf(padding).repeat(needPaddingSize)); } } else { //2. VO의 field 길이가 XML에서 지정한 field 범위 길이를 벗어나면 예외 발생. @@ -132,20 +108,6 @@ private String aggregateFixedLength(int[] fieldValueLength, Object[] fields) { return value.toString(); } - /** - * n개(1~paddingListSize)짜리 padding을 생성하여 paddingList에 저장한다. - */ - private void createPaddingList() { - paddingList = new ArrayList(PADDING_LISTSIZE); - StringBuilder paddingBuilder = new StringBuilder(); - for (int i = 1; i <= PADDING_LISTSIZE; i++) { - paddingBuilder.append(padding); - if (paddingBuilder.length() == i) { - paddingList.add(paddingBuilder.toString()); - } - } - } - /** * 정보 각각의 length를 구한다. * diff --git a/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregatorTest.java b/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregatorTest.java new file mode 100644 index 00000000..3ce26c25 --- /dev/null +++ b/Batch/org.egovframe.rte.bat.core/src/test/java/org/egovframe/rte/bat/core/item/file/transform/EgovFixedLengthLineAggregatorTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2008-2024 MOIS(Ministry of the Interior and Safety). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.egovframe.rte.bat.core.item.file.transform; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link EgovFixedLengthLineAggregator} 단위 테스트. + * + *

고정길이 라인 집계의 우측 패딩 동작과, 동일 인스턴스가 여러 스레드에서 공유될 때의 + * thread-safety(멀티스레드 step / 공유 빈 환경)를 검증한다.

+ */ +public class EgovFixedLengthLineAggregatorTest { + + private EgovFixedLengthLineAggregator aggregator(int... ranges) { + EgovFixedLengthLineAggregator agg = new EgovFixedLengthLineAggregator<>(); + agg.setFieldRanges(ranges); + return agg; + } + + @Test + public void padsShortValuesToFieldWidth() { + // "ab" -> 폭 10, "cd" -> 폭 10, 우측 공백 패딩 + String out = aggregator(10, 10).doAggregate(new Object[]{"ab", "cd"}); + assertEquals("ab cd ", out); + assertEquals(20, out.length()); + } + + @Test + public void keepsExactWidthValuesUnchanged() { + String out = aggregator(3, 2).doAggregate(new Object[]{"abc", "de"}); + assertEquals("abcde", out); + } + + @Test + public void usesCustomPaddingCharacter() { + EgovFixedLengthLineAggregator agg = aggregator(5); + agg.setPadding('0'); + assertEquals("ab000", agg.doAggregate(new Object[]{"ab"})); + } + + @Test + public void padsBeyondLegacyChunkBoundary() { + // 과거 구현의 PADDING_LISTSIZE(100) 분기 경계를 넘는 폭도 동일하게 패딩되어야 한다. + String out = aggregator(250).doAggregate(new Object[]{"x"}); + assertEquals(250, out.length()); + assertEquals('x', out.charAt(0)); + assertEquals(" ".repeat(249), out.substring(1)); + } + + @Test + public void throwsWhenValueLongerThanFieldRange() { + assertThrows(IllegalStateException.class, + () -> aggregator(2).doAggregate(new Object[]{"toolong"})); + } + + @Test + public void sharedInstanceIsThreadSafe() throws Exception { + int threads = 32; + int rounds = 500; + ConcurrentLinkedQueue errors = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue wrong = new ConcurrentLinkedQueue<>(); + String expected = "ab cd "; // width 20 + + for (int r = 0; r < rounds && errors.isEmpty() && wrong.isEmpty(); r++) { + // 매 라운드 fresh 인스턴스 → cold-start 동시 진입(초기화 경합) 노출 + EgovFixedLengthLineAggregator agg = aggregator(10, 10); + Object[] fields = {"ab", "cd"}; + + ExecutorService pool = Executors.newFixedThreadPool(threads); + CountDownLatch gate = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threads); + for (int t = 0; t < threads; t++) { + pool.submit(() -> { + try { + gate.await(); + String out = agg.doAggregate(fields); + if (!expected.equals(out)) { + wrong.add(out); + } + } catch (Throwable e) { + errors.add(e); + } finally { + done.countDown(); + } + }); + } + gate.countDown(); + done.await(10, TimeUnit.SECONDS); + pool.shutdownNow(); + } + + assertTrue(errors.isEmpty(), + "concurrent aggregate threw: " + (errors.isEmpty() ? "" : errors.peek())); + assertTrue(wrong.isEmpty(), + "concurrent aggregate produced wrong output, e.g.: " + (wrong.isEmpty() ? "" : "[" + wrong.peek() + "]")); + } +}