From 0a48273b6da788d35a04cc50e64258c0b651d967 Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 25 Jun 2026 14:05:53 +0900 Subject: [PATCH] [Developer] #424 speed up permission object selection --- .../vpdbackoffice/config/DbPoolWarmup.java | 50 ++++++++++++ .../mapper/ProtectedObjectMapper.java | 2 + .../service/ProtectedObjectService.java | 76 +++++++++++++++++-- .../web/PermissionController.java | 36 +++++++-- src/main/resources/application.yml | 6 +- .../mapper/ProtectedObjectMapper.xml | 13 ++++ 6 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java b/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java new file mode 100644 index 0000000..0c7840b --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java @@ -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); + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java index 36996ef..e4b89c3 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java @@ -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); } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java index 4bbb830..aa82f47 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java @@ -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> databaseObjectsCache; + private final Map>> databaseColumnsCache = new ConcurrentHashMap<>(); + private final Map>> 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 findDatabaseObjects() { - return mapper.findDatabaseObjects(); + CacheEntry> cached = databaseObjectsCache; + if (cached != null && !cached.expired()) { + return cached.value(); + } + List objects = List.copyOf(mapper.findDatabaseObjects()); + databaseObjectsCache = new CacheEntry<>(objects, System.currentTimeMillis() + CATALOG_CACHE_MILLIS); + return objects; } public List findDatabaseColumns(String owner, String objectName) { - return mapper.findDatabaseColumns(owner, objectName); + String key = owner.trim().toUpperCase(Locale.ROOT) + "." + objectName.trim().toUpperCase(Locale.ROOT); + CacheEntry> cached = databaseColumnsCache.get(key); + if (cached != null && !cached.expired()) { + return cached.value(); + } + List 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 findColumns(long objectId) { - return mapper.findColumns(objectId); + CacheEntry> cached = protectedColumnsCache.get(objectId); + if (cached != null && !cached.expired()) { + return cached.value(); + } + List 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 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 value, long expiresAt) { + + boolean expired() { + return System.currentTimeMillis() > expiresAt; + } + } } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java index c018e94..798133f 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java @@ -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, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8e92217..ca94781 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,9 +8,11 @@ spring: driver-class-name: oracle.jdbc.OracleDriver hikari: pool-name: vpd-backoffice-pool - maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:5} - minimum-idle: ${BACKOFFICE_DB_POOL_MIN:1} + maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:10} + minimum-idle: ${BACKOFFICE_DB_POOL_MIN:2} connection-timeout: 10000 + idle-timeout: ${BACKOFFICE_DB_POOL_IDLE_TIMEOUT_MS:300000} + keepalive-time: ${BACKOFFICE_DB_POOL_KEEPALIVE_MS:120000} thymeleaf: cache: false diff --git a/src/main/resources/mapper/ProtectedObjectMapper.xml b/src/main/resources/mapper/ProtectedObjectMapper.xml index 67e4fb2..0ff07f3 100644 --- a/src/main/resources/mapper/ProtectedObjectMapper.xml +++ b/src/main/resources/mapper/ProtectedObjectMapper.xml @@ -28,6 +28,13 @@ WHERE object_type IN ('TABLE', 'VIEW') AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER') AND object_name NOT LIKE 'BIN$%' + AND NOT EXISTS ( + SELECT 1 + FROM cb_protected_object po + WHERE po.owner = all_objects.owner + AND po.object_name = all_objects.object_name + AND po.enabled_yn = 'Y' + ) ORDER BY owner, object_type, object_name FETCH FIRST 500 ROWS ONLY @@ -77,6 +84,12 @@ WHERE object_id = #{objectId,jdbcType=NUMERIC} + + UPDATE cb_protected_object + SET enabled_yn = 'Y' + WHERE object_id = #{objectId} + + UPDATE cb_protected_object SET enabled_yn = 'N'