[Developer] #424 speed up permission object selection

This commit is contained in:
devmrko
2026-06-25 14:05:53 +09:00
parent 314d185b17
commit 0a48273b6d
6 changed files with 169 additions and 14 deletions

View File

@@ -0,0 +1,50 @@
package com.cloudhandson.vpdbackoffice.config;
import com.cloudhandson.vpdbackoffice.service.PermissionService;
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
import java.sql.Connection;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class DbPoolWarmup {
private static final Logger log = LoggerFactory.getLogger(DbPoolWarmup.class);
private final DataSource dataSource;
private final ProtectedObjectService protectedObjectService;
private final PermissionService permissionService;
public DbPoolWarmup(
DataSource dataSource,
ProtectedObjectService protectedObjectService,
PermissionService permissionService
) {
this.dataSource = dataSource;
this.protectedObjectService = protectedObjectService;
this.permissionService = permissionService;
}
@EventListener(ApplicationReadyEvent.class)
public void warmup() {
try (Connection ignored = dataSource.getConnection()) {
log.info("Backoffice DB pool warmed up");
warmupBackofficeCatalog();
} catch (Exception exception) {
log.warn("Backoffice DB pool warm-up failed: {}", exception.getMessage());
}
}
private void warmupBackofficeCatalog() {
long started = System.nanoTime();
var objects = protectedObjectService.findEnabled();
objects.forEach(object -> protectedObjectService.findColumns(object.objectId()));
protectedObjectService.findDatabaseObjects();
permissionService.findRoles();
permissionService.findPermissionViews();
log.info("Backoffice DB catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
}
}

View File

@@ -36,5 +36,7 @@ public interface ProtectedObjectMapper {
int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
int enableObject(@Param("objectId") long objectId);
int disableObject(@Param("objectId") long objectId);
}

View File

@@ -10,7 +10,9 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,6 +21,10 @@ public class ProtectedObjectService {
private final ProtectedObjectMapper mapper;
private final AuditService auditService;
private volatile CacheEntry<List<DatabaseObjectOption>> databaseObjectsCache;
private final Map<String, CacheEntry<List<String>>> databaseColumnsCache = new ConcurrentHashMap<>();
private final Map<Long, CacheEntry<List<ProtectedColumn>>> protectedColumnsCache = new ConcurrentHashMap<>();
private static final long CATALOG_CACHE_MILLIS = 60_000L;
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
this.mapper = mapper;
@@ -30,11 +36,24 @@ public class ProtectedObjectService {
}
public List<DatabaseObjectOption> findDatabaseObjects() {
return mapper.findDatabaseObjects();
CacheEntry<List<DatabaseObjectOption>> cached = databaseObjectsCache;
if (cached != null && !cached.expired()) {
return cached.value();
}
List<DatabaseObjectOption> objects = List.copyOf(mapper.findDatabaseObjects());
databaseObjectsCache = new CacheEntry<>(objects, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
return objects;
}
public List<String> findDatabaseColumns(String owner, String objectName) {
return mapper.findDatabaseColumns(owner, objectName);
String key = owner.trim().toUpperCase(Locale.ROOT) + "." + objectName.trim().toUpperCase(Locale.ROOT);
CacheEntry<List<String>> cached = databaseColumnsCache.get(key);
if (cached != null && !cached.expired()) {
return cached.value();
}
List<String> columns = List.copyOf(mapper.findDatabaseColumns(owner, objectName));
databaseColumnsCache.put(key, new CacheEntry<>(columns, System.currentTimeMillis() + CATALOG_CACHE_MILLIS));
return columns;
}
public ProtectedObject assertEnabled(long objectId) {
@@ -46,7 +65,13 @@ public class ProtectedObjectService {
}
public List<ProtectedColumn> findColumns(long objectId) {
return mapper.findColumns(objectId);
CacheEntry<List<ProtectedColumn>> cached = protectedColumnsCache.get(objectId);
if (cached != null && !cached.expired()) {
return cached.value();
}
List<ProtectedColumn> columns = List.copyOf(mapper.findColumns(objectId));
protectedColumnsCache.put(objectId, new CacheEntry<>(columns, System.currentTimeMillis() + CATALOG_CACHE_MILLIS));
return columns;
}
@Transactional
@@ -58,18 +83,50 @@ public class ProtectedObjectService {
for (String column : splitCsv(normalized.columns())) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N");
}
protectedColumnsCache.remove(objectId);
auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
normalized.objectName()));
}
@Transactional
public ProtectedObject ensureProtectedObject(String owner, String objectName) {
ProtectedObject existing = mapper.findByOwnerAndName(owner, objectName);
String normalizedOwner = owner.trim().toUpperCase(Locale.ROOT);
String normalizedObjectName = objectName.trim().toUpperCase(Locale.ROOT);
ProtectedObject existing = mapper.findByOwnerAndName(normalizedOwner, normalizedObjectName);
if (existing != null) {
if (!existing.enabled()) {
mapper.enableObject(existing.objectId());
auditService.record(new AuditEvent("PROTECTED_OBJECT_RE_ENABLED", null, existing.objectId(), "SUCCESS", null,
null, existing.displayName()));
return mapper.findById(existing.objectId());
}
return existing;
}
throw new AppException("테이블/뷰 관리에서 실제 ORDS Path를 먼저 등록하세요: "
+ owner.toUpperCase(Locale.ROOT) + "." + objectName.toUpperCase(Locale.ROOT));
List<String> columns = findDatabaseColumns(normalizedOwner, normalizedObjectName);
if (columns.isEmpty()) {
throw new AppException("DB에서 컬럼 정보를 찾을 수 없습니다. 객체명과 스키마를 확인하세요: "
+ normalizedOwner + "." + normalizedObjectName);
}
ProtectedObjectCreateCommand command = new ProtectedObjectCreateCommand(
normalizedOwner,
normalizedObjectName,
defaultOrdsPath(normalizedOwner, normalizedObjectName),
String.join(",", columns),
""
);
long objectId = mapper.nextObjectId();
mapper.insertObject(objectId, command);
for (String column : columns) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N");
}
protectedColumnsCache.remove(objectId);
auditService.record(new AuditEvent("PROTECTED_OBJECT_AUTO_CREATED", null, objectId, "SUCCESS", null, null,
command.objectName()));
return mapper.findById(objectId);
}
private String defaultOrdsPath(String owner, String objectName) {
return owner.toLowerCase(Locale.ROOT) + "/" + objectName.toLowerCase(Locale.ROOT);
}
private ProtectedObjectCreateCommand normalizeCreateCommand(ProtectedObjectCreateCommand command) {
@@ -120,4 +177,11 @@ public class ProtectedObjectService {
.forEach(result::add);
return result;
}
private record CacheEntry<T>(T value, long expiresAt) {
boolean expired() {
return System.currentTimeMillis() > expiresAt;
}
}
}

View File

@@ -7,6 +7,8 @@ import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +20,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class PermissionController {
private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
private final PermissionService permissionService;
private final ProtectedObjectService protectedObjectService;
@@ -59,21 +62,42 @@ public class PermissionController {
@GetMapping("/permissions")
public String permissions(Model model) {
long started = System.nanoTime();
var objects = protectedObjectService.findEnabled();
model.addAttribute("roles", permissionService.findRoles());
model.addAttribute("objects", objects);
model.addAttribute("columnsByObject", objects.stream()
long objectsAt = System.nanoTime();
var roles = permissionService.findRoles();
long rolesAt = System.nanoTime();
var columnsByObject = objects.stream()
.collect(Collectors.toMap(
object -> object.objectId(),
object -> protectedObjectService.findColumns(object.objectId()).stream()
.map(column -> column.columnName())
.toList()
)));
model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects());
model.addAttribute("permissions", permissionService.findPermissionViews());
));
long columnsAt = System.nanoTime();
var dbObjects = protectedObjectService.findDatabaseObjects();
long dbObjectsAt = System.nanoTime();
var permissions = permissionService.findPermissionViews();
long permissionsAt = System.nanoTime();
log.info("permissions page timings: objects={}ms roles={}ms columns={}ms dbObjects={}ms permissions={}ms total={}ms",
elapsedMillis(started, objectsAt),
elapsedMillis(objectsAt, rolesAt),
elapsedMillis(rolesAt, columnsAt),
elapsedMillis(columnsAt, dbObjectsAt),
elapsedMillis(dbObjectsAt, permissionsAt),
elapsedMillis(started, permissionsAt));
model.addAttribute("roles", roles);
model.addAttribute("objects", objects);
model.addAttribute("columnsByObject", columnsByObject);
model.addAttribute("dbObjects", dbObjects);
model.addAttribute("permissions", permissions);
return "permissions";
}
private long elapsedMillis(long from, long to) {
return (to - from) / 1_000_000;
}
@PostMapping("/permissions")
public String save(
@RequestParam long roleId,