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 @@ -24,6 +24,7 @@
import org.springframework.beans.factory.InitializingBean;

import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.Map.Entry;

Expand Down Expand Up @@ -56,7 +57,8 @@ public class TypeLoaderUsingMetadata implements TypeLoader, InitializingBean {
/**
* 기존에 load된 Type을 담고 있는 map
*/
private Map<String, Type> typePool = new HashMap<String, Type>();
// 싱글톤 빈으로 주입되어 여러 스레드에서 getType()으로 동시 접근되므로 thread-safe 맵을 사용한다.
private Map<String, Type> typePool = new ConcurrentHashMap<String, Type>();

/**
* Default Constructor
Expand Down Expand Up @@ -210,9 +212,9 @@ private Type getType(String id, Map<String, Type> loadingTypes) {
loadingTypes.remove(id);
}

typePool.put(id, type);

return type;
// 동시에 같은 id를 load한 다른 스레드가 있으면 먼저 등록된 인스턴스를 정본으로 사용한다.
Type existing = typePool.putIfAbsent(id, type);
return existing != null ? existing : type;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.egovframe.rte.itl.integration.type.support;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.egovframe.rte.itl.integration.type.Type;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* {@link TypeLoaderUsingMetadata} 의 동시 {@code getType} 호출에 대한 캐시 안전성을 검증한다.
*
* <p>{@code typePool} 은 싱글톤 빈에서 여러 스레드가 {@code getType} 으로 공유하는데, 이전
* 구현은 동기화 없는 {@code HashMap} 을 검사 후 갱신해 콜드 캐시에 동시 진입하면 맵 손상이나
* 스레드별로 서로 다른 인스턴스 반환이 발생할 수 있었다. {@code ConcurrentHashMap} 과
* {@code putIfAbsent} 적용 후, 동시 호출이 예외 없이 동일한 정본 인스턴스를 반환함을 검증한다.</p>
*/
class TypeLoaderUsingMetadataConcurrencyTest {

@Test
@DisplayName("동일 타입을 동시에 load해도 예외 없이 같은 정본 인스턴스를 반환한다")
void concurrentGetTypeReturnsCanonicalInstance() throws Exception {
// boolean[] 는 리스트-원시 타입이라 RecordTypeDefinitionDao 없이 캐싱 경로를 탄다.
TypeLoaderUsingMetadata loader = new TypeLoaderUsingMetadata();

int threads = 16;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CountDownLatch ready = new CountDownLatch(threads);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(threads);
List<Type> results = Collections.synchronizedList(new ArrayList<>());
List<Throwable> errors = Collections.synchronizedList(new ArrayList<>());

try {
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
ready.countDown();
try {
start.await();
results.add(loader.getType("boolean[]"));
} catch (Throwable t) {
errors.add(t);
} finally {
done.countDown();
}
});
}

assertTrue(ready.await(5, TimeUnit.SECONDS));
start.countDown();
assertTrue(done.await(5, TimeUnit.SECONDS));
} finally {
pool.shutdownNow();
}

assertTrue(errors.isEmpty(), "동시 getType 중 예외가 발생하지 않아야 한다: " + errors);
assertEquals(threads, results.size());

Type canonical = loader.getType("boolean[]");
for (Type t : results) {
assertSame(canonical, t, "모든 스레드가 캐시된 동일 인스턴스를 반환해야 한다");
}
}
}