From 59e8f98af24b268840d9684e12a87d8c8e241f36 Mon Sep 17 00:00:00 2001 From: Clickin Date: Sat, 9 May 2026 23:08:38 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=EB=93=9C=EB=A5=BC=20ATOMIC=ED=95=98=EA=B2=8C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=ED=95=B4=EC=84=9C=20=EB=A6=AC=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EB=B3=B4=EC=A1=B4=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/EgovPropertyServiceImpl.java | 38 +++--- ...opertyServiceAtomicReloadScenarioTest.java | 123 ++++++++++++++++++ .../property/PropertyServiceRefreshTest.java | 77 +++++++++++ 3 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java diff --git a/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java b/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java index 59e079e0..28f4dd24 100755 --- a/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java +++ b/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java @@ -242,12 +242,19 @@ public void refreshPropertyFiles() throws IOException, FdlException { if (egovProperties == null) { return; } - egovProperties.clear(); - loadExternalPropertyFiles(); - applyProgrammaticProperties(); + PropertiesConfiguration refreshedProperties = createPropertiesConfiguration(); + loadExternalPropertyFiles(refreshedProperties); + applyProgrammaticProperties(refreshedProperties); + egovProperties = refreshedProperties; } - private void loadExternalPropertyFiles() throws IOException { + private PropertiesConfiguration createPropertiesConfiguration() { + PropertiesConfiguration propertiesConfiguration = new PropertiesConfiguration(); + propertiesConfiguration.setListDelimiterHandler(new DefaultListDelimiterHandler(',')); + return propertiesConfiguration; + } + + private void loadExternalPropertyFiles(PropertiesConfiguration propertiesConfiguration) throws IOException { if (extFileName == null) { return; } @@ -263,12 +270,12 @@ private void loadExternalPropertyFiles() throws IOException { } else { fileName = (String) element; } - loadPropertyResources(fileName, enc); + loadPropertyResources(propertiesConfiguration, fileName, enc); } } @SuppressWarnings("rawtypes") - private void applyProgrammaticProperties() throws FdlException { + private void applyProgrammaticProperties(PropertiesConfiguration propertiesConfiguration) throws FdlException { if (properties == null) { return; } @@ -280,7 +287,7 @@ private void applyProgrammaticProperties() throws FdlException { if (key == null || key.isEmpty()) { throw new FdlException(messageSource, "error.properties.check.essential", null); } - egovProperties.addProperty(key, value); + propertiesConfiguration.addProperty(key, value); } } @@ -288,8 +295,7 @@ private void applyProgrammaticProperties() throws FdlException { * Bean 초기화 함수로 최초 생성시 필요한 Property 세티처리 */ public void afterPropertiesSet() throws IOException, FdlException { - egovProperties = new PropertiesConfiguration(); - egovProperties.setListDelimiterHandler(new DefaultListDelimiterHandler(',')); + egovProperties = createPropertiesConfiguration(); refreshPropertyFiles(); } @@ -336,13 +342,13 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * @param location 파일위치 * @param encoding Encoding 정보 */ - private void loadPropertyResources(String location, String encoding) throws IOException { + private void loadPropertyResources(PropertiesConfiguration propertiesConfiguration, String location, String encoding) throws IOException { if (resourceLoader instanceof ResourcePatternResolver) { Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); - loadPropertyLoop(resources, encoding); + loadPropertyLoop(propertiesConfiguration, resources, encoding); } else { Resource resource = resourceLoader.getResource(location); - loadPropertyRes(resource, encoding); + loadPropertyRes(propertiesConfiguration, resource, encoding); } } @@ -352,10 +358,10 @@ private void loadPropertyResources(String location, String encoding) throws IOEx * @param resources 리소스정보 * @param encoding 인코딩정보 */ - private void loadPropertyLoop(Resource[] resources, String encoding) { + private void loadPropertyLoop(PropertiesConfiguration propertiesConfiguration, Resource[] resources, String encoding) { Assert.notNull(resources, "Resource array must not be null"); for (int i = 0; i < resources.length; i++) { - loadPropertyRes(resources[i], encoding); + loadPropertyRes(propertiesConfiguration, resources[i], encoding); } } @@ -365,14 +371,14 @@ private void loadPropertyLoop(Resource[] resources, String encoding) { * @param resource 리소스정보 * @param encoding 인코딩정보 */ - private void loadPropertyRes(Resource resource, String encoding) { + private void loadPropertyRes(PropertiesConfiguration propertiesConfiguration, Resource resource, String encoding) { InputStream inputStream = null; InputStreamReader inputStreamReader = null; // 2026.02.28 KISA 보안취약점 조치 try { inputStream = resource.getInputStream(); inputStreamReader = new InputStreamReader(inputStream, StringUtils.isEmpty(encoding) ? DEFAULT_ENCODING : encoding); - egovProperties.read(inputStreamReader); + propertiesConfiguration.read(inputStreamReader); } catch (ConfigurationException | IOException e) { throw new RuntimeException(e); } finally { diff --git a/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java new file mode 100644 index 00000000..13d4a050 --- /dev/null +++ b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java @@ -0,0 +1,123 @@ +package org.egovframe.rte.fdl.property; + +import org.egovframe.rte.fdl.property.impl.EgovPropertyServiceImpl; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * EgovPropertyServiceImpl이 atomic 하게 reload 되는지를 검증합니다. + */ +public class PropertyServiceAtomicReloadScenarioTest { + + private final Path testDir = Path.of("target", "property-service-atomic-reload-scenario", UUID.randomUUID().toString()); + + @Test + public void failedReloadDoesNotPublishPartiallyLoadedSnapshotAndCanRecover() throws Exception { + Path applicationFile = testDir.resolve("application.properties"); + Path databaseFile = testDir.resolve("database.properties"); + Path missingFile = testDir.resolve("missing.properties"); + + write(applicationFile, """ + scenario.version=v1 + application.mode=stable + feature.enabled=false + """); + write(databaseFile, """ + datasource.url=jdbc:old + shared.key=file-v1 + """); + + EgovPropertyServiceImpl service = createPropertyService( + orderedExtFiles(applicationFile, databaseFile), + Map.of("shared.key", "programmatic")); + + assertSnapshot(service, "v1", "stable", "false", "jdbc:old", "file-v1"); + + write(applicationFile, """ + scenario.version=v2 + application.mode=refreshed + feature.enabled=true + """); + write(databaseFile, """ + datasource.url=jdbc:new + shared.key=file-v2 + """); + + service.refreshPropertyFiles(); + + assertSnapshot(service, "v2", "refreshed", "true", "jdbc:new", "file-v2"); + + write(applicationFile, """ + scenario.version=v3 + application.mode=partial-candidate + feature.enabled=false + """); + write(databaseFile, """ + datasource.url=jdbc:partial-candidate + shared.key=file-v3 + """); + service.setExtFileName(orderedExtFiles(applicationFile, missingFile, databaseFile)); + + assertThrows(RuntimeException.class, service::refreshPropertyFiles); + + assertSnapshot(service, "v2", "refreshed", "true", "jdbc:new", "file-v2"); + + service.setExtFileName(orderedExtFiles(applicationFile, databaseFile)); + service.refreshPropertyFiles(); + + assertSnapshot(service, "v3", "partial-candidate", "false", "jdbc:partial-candidate", "file-v3"); + } + + private EgovPropertyServiceImpl createPropertyService(Set extFileName, Map properties) throws Exception { + EgovPropertyServiceImpl service = new EgovPropertyServiceImpl(); + service.setResourceLoader(new PathMatchingResourcePatternResolver()); + service.setExtFileName(extFileName); + service.setProperties(properties); + service.afterPropertiesSet(); + return service; + } + + private Set orderedExtFiles(Path... paths) { + Set extFileName = new LinkedHashSet<>(); + for (Path path : paths) { + extFileName.add(toFileLocation(path)); + } + return extFileName; + } + + private void assertSnapshot( + EgovPropertyService propertyService, + String version, + String mode, + String featureEnabled, + String datasourceUrl, + String sharedFileValue) { + assertEquals(version, propertyService.getString("scenario.version")); + assertEquals(mode, propertyService.getString("application.mode")); + assertEquals(featureEnabled, propertyService.getString("feature.enabled")); + assertEquals(datasourceUrl, propertyService.getString("datasource.url")); + assertArrayEquals(new String[]{sharedFileValue, "programmatic"}, propertyService.getStringArray("shared.key")); + } + + private void write(Path path, String content) throws Exception { + Files.createDirectories(testDir); + Files.writeString(path, content, StandardCharsets.UTF_8); + } + + private String toFileLocation(Path path) { + return path.toAbsolutePath().toUri().toString(); + } + +} diff --git a/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceRefreshTest.java b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceRefreshTest.java index b04e7af7..23e14d87 100755 --- a/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceRefreshTest.java +++ b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceRefreshTest.java @@ -4,14 +4,25 @@ import org.egovframe.rte.fdl.cmmn.exception.FdlException; import org.egovframe.rte.fdl.property.config.PropertyServiceExtendConfig; import org.egovframe.rte.fdl.property.config.PropertyTestConfig; +import org.egovframe.rte.fdl.property.impl.EgovPropertyServiceImpl; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * PropertyServiceRefreshTest @@ -33,6 +44,8 @@ public class PropertyServiceRefreshTest { @Resource(name = "propertyServiceExtend") private EgovPropertyService propertyService; + private final Path testDir = Path.of("target", "property-service-refresh-test", UUID.randomUUID().toString()); + @Test public void testRefreshPropertiesFiles() throws FdlException, IOException { for (String value : propertyService.getStringArray("tokens_on_multiple_lines")) { @@ -47,4 +60,68 @@ public void testRefreshPropertiesFiles() throws FdlException, IOException { assertEquals(Double.valueOf(1234), Double.valueOf(propertyService.getDouble("number.double"))); } + @Test + public void refreshPropertyFilesReloadsChangedFile() throws Exception { + Path propertyFile = writeProperties("reload.properties", "reload.key=old\n"); + EgovPropertyServiceImpl service = createPropertyService(Set.of(toFileLocation(propertyFile)), null); + + assertEquals("old", service.getString("reload.key")); + + Files.writeString(propertyFile, "reload.key=new\n", StandardCharsets.UTF_8); + service.refreshPropertyFiles(); + + assertEquals("new", service.getString("reload.key")); + } + + @Test + public void refreshPropertyFilesPreservesExistingPropertiesWhenReloadFails() throws Exception { + Path propertyFile = writeProperties("stable.properties", "stable.key=old\n"); + Path missingFile = testDir.resolve("missing.properties"); + Set extFileName = new LinkedHashSet<>(); + extFileName.add(toFileLocation(propertyFile)); + extFileName.add(toFileLocation(missingFile)); + EgovPropertyServiceImpl service = createPropertyService(Set.of(toFileLocation(propertyFile)), null); + + Files.writeString(propertyFile, "stable.key=new\n", StandardCharsets.UTF_8); + service.setExtFileName(extFileName); + + assertThrows(RuntimeException.class, service::refreshPropertyFiles); + assertEquals("old", service.getString("stable.key")); + } + + @Test + public void refreshPropertyFilesAppliesProgrammaticPropertiesAfterFileProperties() throws Exception { + Path propertyFile = writeProperties("override.properties", "override.key=file\n"); + EgovPropertyServiceImpl service = createPropertyService( + Set.of(toFileLocation(propertyFile)), + Map.of("override.key", "programmatic")); + + assertArrayEquals(new String[]{"file", "programmatic"}, service.getStringArray("override.key")); + + Files.writeString(propertyFile, "override.key=file-refresh\n", StandardCharsets.UTF_8); + service.refreshPropertyFiles(); + + assertArrayEquals(new String[]{"file-refresh", "programmatic"}, service.getStringArray("override.key")); + } + + private Path writeProperties(String fileName, String content) throws IOException { + Files.createDirectories(testDir); + Path propertyFile = testDir.resolve(fileName); + Files.writeString(propertyFile, content, StandardCharsets.UTF_8); + return propertyFile; + } + + private String toFileLocation(Path path) { + return path.toAbsolutePath().toUri().toString(); + } + + private EgovPropertyServiceImpl createPropertyService(Set extFileName, Map properties) throws Exception { + EgovPropertyServiceImpl service = new EgovPropertyServiceImpl(); + service.setResourceLoader(new PathMatchingResourcePatternResolver()); + service.setExtFileName(extFileName); + service.setProperties(properties); + service.afterPropertiesSet(); + return service; + } + } From e10bee9ea035dc5184217c2bb02668688bcd3cf3 Mon Sep 17 00:00:00 2001 From: Clickin Date: Sat, 9 May 2026 23:25:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20javadoc=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20thread=20safety=20=EA=B3=A0=EB=A0=A4?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EA=B3=84=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/EgovPropertyServiceImpl.java | 5 +- ...opertyServiceAtomicReloadScenarioTest.java | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java b/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java index 28f4dd24..421b46c1 100755 --- a/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java +++ b/Foundation/org.egovframe.rte.fdl.property/src/main/java/org/egovframe/rte/fdl/property/impl/EgovPropertyServiceImpl.java @@ -65,7 +65,7 @@ public class EgovPropertyServiceImpl implements EgovPropertyService, Application private static final Logger LOGGER = LoggerFactory.getLogger(EgovPropertyServiceImpl.class); - private PropertiesConfiguration egovProperties; + private volatile PropertiesConfiguration egovProperties; private ResourceLoader resourceLoader; private MessageSource messageSource; private Set extFileName; @@ -339,6 +339,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws /** * 파일위치정보를 가지고 resources 정보 추출 * + * @param propertiesConfiguration 프로퍼티를 적재할 대상 설정 * @param location 파일위치 * @param encoding Encoding 정보 */ @@ -355,6 +356,7 @@ private void loadPropertyResources(PropertiesConfiguration propertiesConfigurati /** * 멀티로 지정된 경우 처리를 위해 LOOP 처리 * + * @param propertiesConfiguration 프로퍼티를 적재할 대상 설정 * @param resources 리소스정보 * @param encoding 인코딩정보 */ @@ -368,6 +370,7 @@ private void loadPropertyLoop(PropertiesConfiguration propertiesConfiguration, R /** * 파일 정보를 읽어서 egovProperties에 저장 * + * @param propertiesConfiguration 프로퍼티를 적재할 대상 설정 * @param resource 리소스정보 * @param encoding 인코딩정보 */ diff --git a/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java index 13d4a050..f238acee 100644 --- a/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java +++ b/Foundation/org.egovframe.rte.fdl.property/src/test/java/org/egovframe/rte/fdl/property/PropertyServiceAtomicReloadScenarioTest.java @@ -11,10 +11,17 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * EgovPropertyServiceImpl이 atomic 하게 reload 되는지를 검증합니다. @@ -80,6 +87,44 @@ public void failedReloadDoesNotPublishPartiallyLoadedSnapshotAndCanRecover() thr assertSnapshot(service, "v3", "partial-candidate", "false", "jdbc:partial-candidate", "file-v3"); } + @Test + public void concurrentReadersDoNotObserveClearedOrMissingPropertiesDuringRefresh() throws Exception { + Path applicationFile = testDir.resolve("concurrent-application.properties"); + Path databaseFile = testDir.resolve("concurrent-database.properties"); + writeSnapshot(applicationFile, databaseFile, "v1"); + EgovPropertyServiceImpl service = createPropertyService( + orderedExtFiles(applicationFile, databaseFile), + Map.of("shared.key", "programmatic")); + CountDownLatch start = new CountDownLatch(1); + AtomicBoolean reading = new AtomicBoolean(true); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future reader = executorService.submit(() -> { + try { + start.await(5, TimeUnit.SECONDS); + while (reading.get()) { + assertPublishedPropertiesPresent(service); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + try { + start.countDown(); + for (int i = 0; i < 50; i++) { + String version = i % 2 == 0 ? "v2" : "v1"; + writeSnapshot(applicationFile, databaseFile, version); + service.refreshPropertyFiles(); + } + + reading.set(false); + reader.get(5, TimeUnit.SECONDS); + } finally { + reading.set(false); + executorService.shutdownNow(); + } + } + private EgovPropertyServiceImpl createPropertyService(Set extFileName, Map properties) throws Exception { EgovPropertyServiceImpl service = new EgovPropertyServiceImpl(); service.setResourceLoader(new PathMatchingResourcePatternResolver()); @@ -111,6 +156,31 @@ private void assertSnapshot( assertArrayEquals(new String[]{sharedFileValue, "programmatic"}, propertyService.getStringArray("shared.key")); } + private void assertPublishedPropertiesPresent(EgovPropertyService propertyService) { + String version = propertyService.getString("scenario.version"); + String mode = propertyService.getString("application.mode"); + String datasourceUrl = propertyService.getString("datasource.url"); + String[] sharedValues = propertyService.getStringArray("shared.key"); + + assertTrue("v1".equals(version) || "v2".equals(version)); + assertTrue("v1-mode".equals(mode) || "v2-mode".equals(mode)); + assertTrue("jdbc:v1".equals(datasourceUrl) || "jdbc:v2".equals(datasourceUrl)); + assertEquals(2, sharedValues.length); + assertTrue("file-v1".equals(sharedValues[0]) || "file-v2".equals(sharedValues[0])); + assertEquals("programmatic", sharedValues[1]); + } + + private void writeSnapshot(Path applicationFile, Path databaseFile, String version) throws Exception { + write(applicationFile, """ + scenario.version=%s + application.mode=%s-mode + """.formatted(version, version)); + write(databaseFile, """ + datasource.url=jdbc:%s + shared.key=file-%s + """.formatted(version, version)); + } + private void write(Path path, String content) throws Exception { Files.createDirectories(testDir); Files.writeString(path, content, StandardCharsets.UTF_8);