From 552781b4960cbc0e152beb47c95fa3a16afc323b Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Mon, 22 Jun 2026 11:41:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20TypeLoaderUsingMetadata=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=BA=90=EC=8B=9C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typePool이 동기화 없는 HashMap이라, 싱글톤으로 주입된 TypeLoader의 getType()을 여러 스레드가 콜드 캐시에서 동시에 호출하면 검사(get)와 갱신(put) 사이 경합으로 맵 손상(리사이즈 무한 루프·엔트리 유실)이나 스레드마다 서로 다른 인스턴스가 반환될 수 있었다. typePool을 ConcurrentHashMap으로 바꾸고, load 후 putIfAbsent로 먼저 등록된 인스턴스를 정본으로 반환해 동시 호출에도 일관된 타입을 제공한다. 16스레드 동시 getType 단위테스트로 예외 없이 동일 정본 인스턴스 반환을 검증했다. --- .../type/support/TypeLoaderUsingMetadata.java | 10 ++- ...ypeLoaderUsingMetadataConcurrencyTest.java | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 Integration/org.egovframe.rte.itl.integration/src/test/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadataConcurrencyTest.java diff --git a/Integration/org.egovframe.rte.itl.integration/src/main/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadata.java b/Integration/org.egovframe.rte.itl.integration/src/main/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadata.java index bfc1b034..1b05f10b 100755 --- a/Integration/org.egovframe.rte.itl.integration/src/main/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadata.java +++ b/Integration/org.egovframe.rte.itl.integration/src/main/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadata.java @@ -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; @@ -56,7 +57,8 @@ public class TypeLoaderUsingMetadata implements TypeLoader, InitializingBean { /** * 기존에 load된 Type을 담고 있는 map */ - private Map typePool = new HashMap(); + // 싱글톤 빈으로 주입되어 여러 스레드에서 getType()으로 동시 접근되므로 thread-safe 맵을 사용한다. + private Map typePool = new ConcurrentHashMap(); /** * Default Constructor @@ -210,9 +212,9 @@ private Type getType(String id, Map loadingTypes) { loadingTypes.remove(id); } - typePool.put(id, type); - - return type; + // 동시에 같은 id를 load한 다른 스레드가 있으면 먼저 등록된 인스턴스를 정본으로 사용한다. + Type existing = typePool.putIfAbsent(id, type); + return existing != null ? existing : type; } } diff --git a/Integration/org.egovframe.rte.itl.integration/src/test/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadataConcurrencyTest.java b/Integration/org.egovframe.rte.itl.integration/src/test/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadataConcurrencyTest.java new file mode 100644 index 00000000..7df5c1e3 --- /dev/null +++ b/Integration/org.egovframe.rte.itl.integration/src/test/java/org/egovframe/rte/itl/integration/type/support/TypeLoaderUsingMetadataConcurrencyTest.java @@ -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} 호출에 대한 캐시 안전성을 검증한다. + * + *

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

+ */ +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 results = Collections.synchronizedList(new ArrayList<>()); + List 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, "모든 스레드가 캐시된 동일 인스턴스를 반환해야 한다"); + } + } +}