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 @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -280,16 +287,15 @@ 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);
}
}

/**
* Bean 초기화 함수로 최초 생성시 필요한 Property 세티처리
*/
public void afterPropertiesSet() throws IOException, FdlException {
egovProperties = new PropertiesConfiguration();
egovProperties.setListDelimiterHandler(new DefaultListDelimiterHandler(','));
egovProperties = createPropertiesConfiguration();
refreshPropertyFiles();
}

Expand Down Expand Up @@ -333,46 +339,49 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
/**
* 파일위치정보를 가지고 resources 정보 추출
*
* @param propertiesConfiguration 프로퍼티를 적재할 대상 설정
* @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);
}
}

/**
* 멀티로 지정된 경우 처리를 위해 LOOP 처리
*
* @param propertiesConfiguration 프로퍼티를 적재할 대상 설정
* @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);
}
}

/**
* 파일 정보를 읽어서 egovProperties에 저장
*
* @param propertiesConfiguration 프로퍼티를 적재할 대상 설정
* @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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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 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 되는지를 검증합니다.
*/
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");
}

@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());
service.setExtFileName(extFileName);
service.setProperties(properties);
service.afterPropertiesSet();
return service;
}

private Set<String> orderedExtFiles(Path... paths) {
Set<String> 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 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);
}

private String toFileLocation(Path path) {
return path.toAbsolutePath().toUri().toString();
}

}
Loading