[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 updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
int enableObject(@Param("objectId") long objectId);
int disableObject(@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.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -19,6 +21,10 @@ public class ProtectedObjectService {
private final ProtectedObjectMapper mapper; private final ProtectedObjectMapper mapper;
private final AuditService auditService; 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) { public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
this.mapper = mapper; this.mapper = mapper;
@@ -30,11 +36,24 @@ public class ProtectedObjectService {
} }
public List<DatabaseObjectOption> findDatabaseObjects() { 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) { 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) { public ProtectedObject assertEnabled(long objectId) {
@@ -46,7 +65,13 @@ public class ProtectedObjectService {
} }
public List<ProtectedColumn> findColumns(long objectId) { 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 @Transactional
@@ -58,18 +83,50 @@ public class ProtectedObjectService {
for (String column : splitCsv(normalized.columns())) { for (String column : splitCsv(normalized.columns())) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N"); 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, auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
normalized.objectName())); normalized.objectName()));
} }
@Transactional @Transactional
public ProtectedObject ensureProtectedObject(String owner, String objectName) { 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 != 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; return existing;
} }
throw new AppException("테이블/뷰 관리에서 실제 ORDS Path를 먼저 등록하세요: " List<String> columns = findDatabaseColumns(normalizedOwner, normalizedObjectName);
+ owner.toUpperCase(Locale.ROOT) + "." + objectName.toUpperCase(Locale.ROOT)); 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) { private ProtectedObjectCreateCommand normalizeCreateCommand(ProtectedObjectCreateCommand command) {
@@ -120,4 +177,11 @@ public class ProtectedObjectService {
.forEach(result::add); .forEach(result::add);
return result; 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.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +20,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller @Controller
public class PermissionController { public class PermissionController {
private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
private final PermissionService permissionService; private final PermissionService permissionService;
private final ProtectedObjectService protectedObjectService; private final ProtectedObjectService protectedObjectService;
@@ -59,21 +62,42 @@ public class PermissionController {
@GetMapping("/permissions") @GetMapping("/permissions")
public String permissions(Model model) { public String permissions(Model model) {
long started = System.nanoTime();
var objects = protectedObjectService.findEnabled(); var objects = protectedObjectService.findEnabled();
model.addAttribute("roles", permissionService.findRoles()); long objectsAt = System.nanoTime();
model.addAttribute("objects", objects); var roles = permissionService.findRoles();
model.addAttribute("columnsByObject", objects.stream() long rolesAt = System.nanoTime();
var columnsByObject = objects.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
object -> object.objectId(), object -> object.objectId(),
object -> protectedObjectService.findColumns(object.objectId()).stream() object -> protectedObjectService.findColumns(object.objectId()).stream()
.map(column -> column.columnName()) .map(column -> column.columnName())
.toList() .toList()
))); ));
model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects()); long columnsAt = System.nanoTime();
model.addAttribute("permissions", permissionService.findPermissionViews()); 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"; return "permissions";
} }
private long elapsedMillis(long from, long to) {
return (to - from) / 1_000_000;
}
@PostMapping("/permissions") @PostMapping("/permissions")
public String save( public String save(
@RequestParam long roleId, @RequestParam long roleId,

View File

@@ -8,9 +8,11 @@ spring:
driver-class-name: oracle.jdbc.OracleDriver driver-class-name: oracle.jdbc.OracleDriver
hikari: hikari:
pool-name: vpd-backoffice-pool pool-name: vpd-backoffice-pool
maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:5} maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:10}
minimum-idle: ${BACKOFFICE_DB_POOL_MIN:1} minimum-idle: ${BACKOFFICE_DB_POOL_MIN:2}
connection-timeout: 10000 connection-timeout: 10000
idle-timeout: ${BACKOFFICE_DB_POOL_IDLE_TIMEOUT_MS:300000}
keepalive-time: ${BACKOFFICE_DB_POOL_KEEPALIVE_MS:120000}
thymeleaf: thymeleaf:
cache: false cache: false

View File

@@ -28,6 +28,13 @@
WHERE object_type IN ('TABLE', 'VIEW') WHERE object_type IN ('TABLE', 'VIEW')
AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER') AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER')
AND object_name NOT LIKE 'BIN$%' 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 ORDER BY owner, object_type, object_name
FETCH FIRST 500 ROWS ONLY FETCH FIRST 500 ROWS ONLY
</select> </select>
@@ -77,6 +84,12 @@
WHERE object_id = #{objectId,jdbcType=NUMERIC} WHERE object_id = #{objectId,jdbcType=NUMERIC}
</update> </update>
<update id="enableObject">
UPDATE cb_protected_object
SET enabled_yn = 'Y'
WHERE object_id = #{objectId}
</update>
<update id="disableObject"> <update id="disableObject">
UPDATE cb_protected_object UPDATE cb_protected_object
SET enabled_yn = 'N' SET enabled_yn = 'N'