Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public class EgovReflectionSupport<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(EgovReflectionSupport.class);

private Object object = null;
private Method[] methods;
private HashMap<String, Method> methodMap;
private volatile Method[] methods;
private volatile HashMap<String, Method> methodMap;
private Type[] fieldType;

public EgovReflectionSupport() {
Expand Down Expand Up @@ -151,23 +151,36 @@ public T generateObject(Class<?> type, List<String> tokens, String[] names) {
* Bean 생성시 한 번만 실행 된다.
*/
public void generateGetterMethodMap(String[] names, T item) {
if (methods == null) {
methods = item.getClass().getMethods();
methodMap = new HashMap<String, Method>();
// 멀티스레드 step에서 FieldExtractor/ItemWriter 빈이 공유되면 이 인스턴스도 공유된다.
// 기존 코드는 methods를 먼저 대입한 뒤 methodMap을 채워, 다른 스레드가 methods != null 만 보고
// 아직 채워지지 않은(또는 null인) methodMap을 사용하다 NPE/부분초기화에 노출되었다.
// double-checked locking + 지역변수로 완성한 뒤 발행하여 부분초기화 상태가 외부에 보이지 않게 한다.
if (methods != null) {
return;
}
synchronized (this) {
if (methods != null) {
return;
}
Method[] localMethods = item.getClass().getMethods();
HashMap<String, Method> localMap = new HashMap<String, Method>();
try {
if (ArrayUtils.isNotEmpty(names) && names.length > 0) {
for (int i = 0; i < names.length; i++) {
String strMethod;
if (names[i].length() > 0) {
strMethod = "get" + (names[i].substring(0, 1)).toUpperCase() + names[i].substring(1);
methodMap.put(names[i], retrieveMethod(methods, strMethod));
localMap.put(names[i], retrieveMethod(localMethods, strMethod));
}
}
}
//2017.02.15 장동한 시큐어코딩(ES)-부적절한 예외 처리[CWE-253, CWE-440, CWE-754]
} catch (StringIndexOutOfBoundsException | NullPointerException e) {
ReflectionUtils.handleReflectionException(e);
}
// 완성된 map을 먼저 발행한 뒤 methods를 마지막에 대입(초기화 완료 표식)한다.
methodMap = localMap;
methods = localMethods;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.egovframe.rte.bat.core.reflection;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
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.assertTrue;

/**
* EgovReflectionSupport 의 멀티스레드 안전성 테스트.
*
* <p>FieldExtractor/ItemWriter 빈이 멀티스레드 step 에서 공유되면 그 안의
* EgovReflectionSupport 인스턴스도 공유된다. 과거 generateGetterMethodMap 은
* methods 를 먼저 대입한 뒤 methodMap 을 채워, 다른 스레드가 부분초기화 상태를
* 사용하다 NPE 에 노출되었다(cold-start 경합). 본 테스트로 회귀를 방지한다.</p>
*
* @author 기여자
* @version 1.0
* @since 2026.06.09
*/
public class EgovReflectionSupportConcurrencyTest {

private static final String[] NAMES = {"name", "age"};

@Test
public void sharedInstanceColdStartIsThreadSafe() throws InterruptedException {
final int threads = 32;
final int rounds = 500;
ExecutorService pool = Executors.newFixedThreadPool(threads);
try {
for (int round = 0; round < rounds; round++) {
// 라운드마다 새 공유 인스턴스를 만들어 cold-start lazy-init 경합을 재현한다.
final EgovReflectionSupport<SampleVo> shared = new EgovReflectionSupport<SampleVo>();
final SampleVo item = new SampleVo("egov", 42);
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch doneGate = new CountDownLatch(threads);
final List<Throwable> failures = new CopyOnWriteArrayList<Throwable>();
final List<Object> names = new CopyOnWriteArrayList<Object>();
final List<Object> ages = new CopyOnWriteArrayList<Object>();

for (int t = 0; t < threads; t++) {
pool.execute(() -> {
try {
startGate.await();
shared.generateGetterMethodMap(NAMES, item);
names.add(shared.invokeGettterMethod(item, "name"));
ages.add(shared.invokeGettterMethod(item, "age"));
} catch (Throwable e) {
failures.add(e);
} finally {
doneGate.countDown();
}
});
}

startGate.countDown();
assertTrue(doneGate.await(10, TimeUnit.SECONDS), "round " + round + " timed out");
assertTrue(failures.isEmpty(),
"round " + round + " concurrent failure: " + failures);
for (Object n : names) {
assertEquals("egov", n, "round " + round + " name mismatch");
}
for (Object a : ages) {
assertEquals(42, a, "round " + round + " age mismatch");
}
}
} finally {
pool.shutdownNow();
}
}

@Test
public void singleThreadGetterExtractsValues() {
EgovReflectionSupport<SampleVo> reflection = new EgovReflectionSupport<SampleVo>();
SampleVo item = new SampleVo("egov", 42);
reflection.generateGetterMethodMap(NAMES, item);
List<Object> values = new ArrayList<Object>();
for (String name : NAMES) {
values.add(reflection.invokeGettterMethod(item, name));
}
assertEquals("egov", values.get(0));
assertEquals(42, values.get(1));
}

/**
* 테스트용 VO. public getter 가 reflection 으로 조회된다.
*/
public static class SampleVo {
private final String name;
private final int age;

public SampleVo(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}
}