인덱스 API 통계 부분 실 데이터 적용

This commit is contained in:
Rinjae
2025-12-23 16:27:49 +09:00
parent 980f96859a
commit 2b9a0e8f4f
4 changed files with 197 additions and 29 deletions
@@ -1,33 +1,22 @@
package com.eactive.apim.portal.apps.main.controller;
import com.eactive.apim.portal.apps.apis.dto.ApiSpecInfoDto;
import com.eactive.apim.portal.apps.apis.service.ApiSearchFacade;
import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceApiDTO;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO;
import com.eactive.apim.portal.apps.community.notice.service.PortalNoticeFacade;
import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO;
import com.eactive.apim.portal.apps.main.service.IndexStatisticsService;
import com.eactive.apim.portal.apps.main.service.MainApiFacade;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import java.util.*;
import java.util.stream.Collectors;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
public class IndexController {
private final ApiSearchFacade apiSearchFacade;
private final MainApiFacade mainApiFacade;
private final PortalPropertyService portalPropertyService;
private final IndexStatisticsService indexStatisticsService;
/**
* 메인 페이지 API는 Portal Property 에 main.service.list 에 등록된 그룹을 기준으로 API를 조회함.
@@ -45,29 +34,23 @@ public class IndexController {
List<ApiServiceDTO> services = mainApiFacade.getOpenApiServices();
List<String> hashTags = mainApiFacade.getHashTags();
ApiGroupSearch search = new ApiGroupSearch();
Map<String, Object> searchResult = apiSearchFacade.searchApis(search);
// 인덱스 페이지 하단 통계 조회 (캐싱 적용)
IndexStatisticsDTO statistics = indexStatisticsService.getStatistics();
int selectedApiCount = (int) searchResult.get("selectedApiCount");
int serviceCount = ((List<?>) searchResult.get("services")).size();
// 승인된 기업 건수
int approvedOrgCount = mainApiFacade.getApprovedOrgCount();
addAttributesToModel(model, services, hashTags, serviceCount, approvedOrgCount, selectedApiCount);
addAttributesToModel(model, services, hashTags, statistics);
return "apps/main/index";
}
private void addAttributesToModel(Model model, List<ApiServiceDTO> apiServices, List<String> hashTags, int serviceCount, int approvedOrgCount, int selectedApiCount) {
private void addAttributesToModel(Model model, List<ApiServiceDTO> apiServices, List<String> hashTags, IndexStatisticsDTO statistics) {
model.addAttribute("services", apiServices);
model.addAttribute("hashtags", hashTags);
model.addAttribute("serviceCount", serviceCount);
model.addAttribute("approvedOrgCount", approvedOrgCount);
model.addAttribute("selectedApiCount", selectedApiCount);
// 인덱스 페이지 하단 통계
model.addAttribute("approvedOrgCount", statistics.getApprovedOrgCount());
model.addAttribute("serviceCount", statistics.getActiveAppCount());
model.addAttribute("totalApiCount", statistics.getTotalApiCount());
}
}
@@ -0,0 +1,37 @@
package com.eactive.apim.portal.apps.main.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 인덱스 페이지 하단 통계 DTO
* - API 활용 기업: 법인으로 등록된 수의 합계 (정상 상태)
* - 서비스 이용 수: 전체 법인이 생성한 앱의 합계 (이용 가능 상태)
* - API 이용 건수: 전체 법인이 생성한 앱의 API 수의 합계 (이용 가능 상태)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IndexStatisticsDTO {
/**
* API 활용 기업 수
* 법인으로 등록되고 정상 상태인 기업의 수
*/
private int approvedOrgCount;
/**
* 서비스 이용 수
* 정상 상태 법인이 생성한 앱 중 이용 가능 상태인 앱의 수
*/
private int activeAppCount;
/**
* API 이용 건수
* 정상 상태 법인이 생성한 이용 가능 앱에 연결된 API의 총 수
*/
private int totalApiCount;
}
@@ -0,0 +1,137 @@
package com.eactive.apim.portal.apps.main.service;
import com.eactive.apim.portal.app.entity.Credential;
import com.eactive.apim.portal.app.repository.CredentialRepository;
import com.eactive.apim.portal.apps.main.dto.IndexStatisticsDTO;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.ApprovalStatus;
import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums.OrgStatus;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 인덱스 페이지 통계 서비스
* 메모리 캐싱을 통해 DB 조회를 최소화 (1시간 TTL)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IndexStatisticsService {
private static final long CACHE_TTL_HOURS = 1;
private static final String APP_STATUS_ACTIVE = "1";
private final PortalOrgRepository portalOrgRepository;
private final CredentialRepository credentialRepository;
// 캐시된 통계 데이터
private volatile IndexStatisticsDTO cachedStatistics;
// 마지막 집계 시간
private volatile LocalDateTime lastAggregatedTime;
/**
* 인덱스 페이지 통계 조회
* 캐시가 없거나 1시간이 경과한 경우 재집계
*/
@Transactional(readOnly = true)
public IndexStatisticsDTO getStatistics() {
if (shouldRefreshCache()) {
synchronized (this) {
// Double-check locking
if (shouldRefreshCache()) {
refreshStatistics();
}
}
}
return cachedStatistics;
}
/**
* 캐시 갱신이 필요한지 확인
*/
private boolean shouldRefreshCache() {
if (cachedStatistics == null || lastAggregatedTime == null) {
return true;
}
return LocalDateTime.now().isAfter(lastAggregatedTime.plusHours(CACHE_TTL_HOURS));
}
/**
* 통계 재집계
*/
private void refreshStatistics() {
log.info("인덱스 페이지 통계 집계 시작");
long startTime = System.currentTimeMillis();
try {
// 1. API 활용 기업 수: 정상 상태이고 승인 완료된 기관 수
int approvedOrgCount = (int) portalOrgRepository.countByOrgStatusAndApprovalStatus(
OrgStatus.ACTIVE, ApprovalStatus.COMPLETED);
// 2. 정상 상태 기관의 ID 목록 조회
List<String> activeOrgIds = portalOrgRepository.findIdsByOrgStatusAndApprovalStatus(
OrgStatus.ACTIVE, ApprovalStatus.COMPLETED);
int activeAppCount = 0;
int totalApiCount = 0;
if (!activeOrgIds.isEmpty()) {
// 3. 서비스 이용 수: 정상 기관의 이용 가능 앱 수
activeAppCount = (int) credentialRepository.countActiveAppsByOrgIds(activeOrgIds);
// 4. API 이용 건수: 정상 기관의 이용 가능 앱에 연결된 API 수
List<Credential> activeApps = credentialRepository.findActiveAppsByOrgIds(activeOrgIds);
totalApiCount = activeApps.stream()
.mapToInt(credential -> credential.getApiList() != null ? credential.getApiList().size() : 0)
.sum();
}
cachedStatistics = IndexStatisticsDTO.builder()
.approvedOrgCount(approvedOrgCount)
.activeAppCount(activeAppCount)
.totalApiCount(totalApiCount)
.build();
lastAggregatedTime = LocalDateTime.now();
long elapsed = System.currentTimeMillis() - startTime;
log.info("인덱스 페이지 통계 집계 완료 - 소요시간: {}ms, 기업: {}, 앱: {}, API: {}",
elapsed, approvedOrgCount, activeAppCount, totalApiCount);
} catch (Exception e) {
log.error("인덱스 페이지 통계 집계 실패", e);
// 기존 캐시가 없는 경우 기본값 설정
if (cachedStatistics == null) {
cachedStatistics = IndexStatisticsDTO.builder()
.approvedOrgCount(0)
.activeAppCount(0)
.totalApiCount(0)
.build();
}
}
}
/**
* 캐시 강제 갱신 (관리자용)
*/
public void forceRefresh() {
synchronized (this) {
lastAggregatedTime = null;
refreshStatistics();
}
}
/**
* 캐시 만료 시간 조회
*/
public LocalDateTime getCacheExpireTime() {
if (lastAggregatedTime == null) {
return null;
}
return lastAggregatedTime.plusHours(CACHE_TTL_HOURS);
}
}
@@ -5,7 +5,10 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrgEnums;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@EMSDataSource
public interface PortalOrgRepository extends BaseRepository<PortalOrg, String> {
@@ -20,5 +23,13 @@ public interface PortalOrgRepository extends BaseRepository<PortalOrg, String> {
long countByOrgStatusAndApprovalStatus(PortalOrgEnums.OrgStatus status, PortalOrgEnums.ApprovalStatus approvalStatus);
Optional<PortalOrg> findById(String orgId);
/**
* 정상 상태이고 승인 완료된 기관의 ID 목록 조회
*/
@Query("SELECT o.id FROM PortalOrg o WHERE o.orgStatus = :status AND o.approvalStatus = :approvalStatus")
List<String> findIdsByOrgStatusAndApprovalStatus(
@Param("status") PortalOrgEnums.OrgStatus status,
@Param("approvalStatus") PortalOrgEnums.ApprovalStatus approvalStatus);
}