File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
package kr.itn.itnhub.code;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 코드값을 기동 후 첫 조회에 한 번 읽어 메모리에 들고 있는다. 코드는 관리 화면에서 가끔
* 바뀔 뿐이라 매 요청 조회가 낭비이고, 단일 인스턴스 운영이라 분산 캐시는 필요 없다.
*
* <p>지연 로딩을 쓰는 이유는 Flyway 마이그레이션이 끝나기 전에 빈 초기화가 돌 수 있기
* 때문이다. 첫 조회 시점에는 마이그레이션이 반드시 끝나 있다.</p>
*/
@Service
public class CodeService {
private static final Logger log = LoggerFactory.getLogger(CodeService.class);
private final CodeMapper mapper;
private final ObjectMapper json = new ObjectMapper();
/** 그룹ID -> 활성 코드(정렬순). 화면 선택지가 여기서 나간다. */
private volatile Map<String, List<Code>> activeByGroup;
/** 그룹ID -> 코드ID -> 코드(비활성 포함). 과거 데이터를 화면에 그릴 때 라벨이 필요하다. */
private volatile Map<String, Map<String, Code>> allByGroup;
public CodeService(CodeMapper mapper) {
this.mapper = mapper;
}
/** 관리 화면에서 코드를 고친 뒤 부른다. */
public synchronized void reload() {
Map<String, List<Code>> active = new LinkedHashMap<>();
Map<String, Map<String, Code>> all = new LinkedHashMap<>();
for (CodeRow row : mapper.findAllCodes()) {
Code code = toCode(row);
all.computeIfAbsent(code.groupId(), key -> new LinkedHashMap<>())
.put(code.code(), code);
if (code.active()) {
active.computeIfAbsent(code.groupId(), key -> new ArrayList<>()).add(code);
}
}
this.activeByGroup = active;
this.allByGroup = all;
}
private Code toCode(CodeRow row) {
Map<String, Object> attrs;
try {
attrs = json.readValue(row.attrsJson(), new TypeReference<>() {
});
} catch (Exception e) {
// 잘못된 JSON 하나가 전체 코드 로딩을 막지는 않게 한다. 해당 코드만 속성이 빈다.
log.warn("코드 attrs 파싱 실패 - {}:{} 의 속성을 비운다", row.groupId(), row.code(), e);
attrs = Map.of();
}
return new Code(row.groupId(), row.code(), row.label(), row.sortOrder(), row.active(), attrs);
}
private void ensureLoaded() {
if (activeByGroup == null) {
reload();
}
}
/** 활성 코드만, 그룹별 정렬순. */
public Map<String, List<Code>> all() {
ensureLoaded();
return activeByGroup;
}
/** 활성 코드만. 없는 그룹이면 빈 목록. */
public List<Code> group(String groupId) {
ensureLoaded();
return activeByGroup.getOrDefault(groupId, List.of());
}
/** 비활성 코드도 찾는다 - 과거 데이터의 라벨을 그려야 하기 때문이다. */
public Optional<Code> find(String groupId, String code) {
ensureLoaded();
return Optional.ofNullable(allByGroup.getOrDefault(groupId, Map.of()).get(code));
}
/** attrs.scopes에 해당 자리가 든 활성 코드만. 공공누리 유형이 이걸 쓴다. */
public List<Code> scoped(String groupId, String scope) {
return group(groupId).stream().filter(code -> scopes(code).contains(scope)).toList();
}
@SuppressWarnings("unchecked")
private List<String> scopes(Code code) {
Object raw = code.attrs().get(Codes.ATTR_SCOPES);
return raw instanceof List<?> list ? (List<String>) list : Collections.emptyList();
}
/** 저장 요청 값이 이 그룹의 활성 코드인지. */
public boolean isValid(String groupId, String code) {
return group(groupId).stream().anyMatch(candidate -> candidate.code().equals(code));
}
/** 코드 관리 화면이 쓰는 그룹 메타. 캐시하지 않는다 - 호출 빈도가 낮다. */
public List<CodeGroup> groups() {
return mapper.findAllGroups();
}
}