perf: API 응답 크기 최적화 — gzip 압축, NON_NULL, 정밀도 제한, Swagger 최신화
- application.yml: gzip 압축 활성화 (1KB 이상 JSON 자동 압축, 70~85% 감소) - JacksonConfig: NON_NULL 전역 설정 (null 필드 직렬화 제거, 5~15% 감소) - VesselPositionService: sog/cog 소수점 1자리, lon/lat 6자리 제한 (3~5% 감소) - MapTileController: @Tag, @Operation, @Parameter Swagger 문서 추가 - LockMonitorController: @Tag, @Operation Swagger 문서 추가 - BatchAdminController daily-stats: @ApiResponse 응답 스키마 예시 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
a90662b2f0
커밋
0249a1fb90
@ -1,5 +1,9 @@
|
|||||||
package gc.mda.signal_batch.domain.gis.controller;
|
package gc.mda.signal_batch.domain.gis.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@ -17,16 +21,20 @@ import java.nio.file.Paths;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/tiles")
|
@RequestMapping("/api/tiles")
|
||||||
|
@Tag(name = "지도 타일 API", description = "WebP 형식의 배경 지도 타일 서비스 (World/ENC)")
|
||||||
public class MapTileController {
|
public class MapTileController {
|
||||||
|
|
||||||
private static final String TILE_BASE_PATH = "/devdata/MAPS/WORLD_webp";
|
private static final String TILE_BASE_PATH = "/devdata/MAPS/WORLD_webp";
|
||||||
private static final String TILE_ENC_PATH = "/devdata/MAPS/ENC_RAS_webp";
|
private static final String TILE_ENC_PATH = "/devdata/MAPS/ENC_RAS_webp";
|
||||||
|
|
||||||
@GetMapping("/world/{z}/{x}/{y}.webp")
|
@GetMapping("/world/{z}/{x}/{y}.webp")
|
||||||
|
@Operation(summary = "세계지도 타일 조회", description = "ZXY 좌표 기반 세계지도 WebP 타일을 반환합니다")
|
||||||
|
@ApiResponse(responseCode = "200", description = "WebP 이미지 타일")
|
||||||
|
@ApiResponse(responseCode = "404", description = "타일 없음")
|
||||||
public ResponseEntity<Resource> getWorldTile(
|
public ResponseEntity<Resource> getWorldTile(
|
||||||
@PathVariable int z,
|
@Parameter(description = "줌 레벨 (0~18)") @PathVariable int z,
|
||||||
@PathVariable int x,
|
@Parameter(description = "X 좌표 (열)") @PathVariable int x,
|
||||||
@PathVariable int y) {
|
@Parameter(description = "Y 좌표 (행)") @PathVariable int y) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 안전한 경로 생성
|
// 안전한 경로 생성
|
||||||
@ -59,10 +67,13 @@ public class MapTileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/enc/{z}/{x}/{y}.webp")
|
@GetMapping("/enc/{z}/{x}/{y}.webp")
|
||||||
|
@Operation(summary = "ENC 해도 타일 조회", description = "ZXY 좌표 기반 전자해도(ENC) WebP 타일을 반환합니다")
|
||||||
|
@ApiResponse(responseCode = "200", description = "WebP 이미지 타일")
|
||||||
|
@ApiResponse(responseCode = "404", description = "타일 없음")
|
||||||
public ResponseEntity<Resource> getEncTile(
|
public ResponseEntity<Resource> getEncTile(
|
||||||
@PathVariable int z,
|
@Parameter(description = "줌 레벨 (0~18)") @PathVariable int z,
|
||||||
@PathVariable int x,
|
@Parameter(description = "X 좌표 (열)") @PathVariable int x,
|
||||||
@PathVariable int y) {
|
@Parameter(description = "Y 좌표 (행)") @PathVariable int y) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 안전한 경로 생성
|
// 안전한 경로 생성
|
||||||
@ -95,6 +106,7 @@ public class MapTileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/health")
|
@GetMapping("/health")
|
||||||
|
@Operation(summary = "타일 서비스 상태 확인", description = "타일 디렉토리 존재 여부로 서비스 상태를 확인합니다")
|
||||||
public ResponseEntity<String> checkTileService() {
|
public ResponseEntity<String> checkTileService() {
|
||||||
File baseDir = new File(TILE_BASE_PATH);
|
File baseDir = new File(TILE_BASE_PATH);
|
||||||
if (baseDir.exists() && baseDir.isDirectory()) {
|
if (baseDir.exists() && baseDir.isDirectory()) {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
|||||||
import org.springframework.jdbc.core.RowMapper;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -155,10 +157,10 @@ public class VesselPositionService {
|
|||||||
return RecentVesselPositionDto.builder()
|
return RecentVesselPositionDto.builder()
|
||||||
.mmsi(mmsi)
|
.mmsi(mmsi)
|
||||||
.imo(imo > 0 ? imo : null)
|
.imo(imo > 0 ? imo : null)
|
||||||
.lon(rs.getDouble("lon"))
|
.lon(Math.round(rs.getDouble("lon") * 1_000_000) / 1_000_000.0)
|
||||||
.lat(rs.getDouble("lat"))
|
.lat(Math.round(rs.getDouble("lat") * 1_000_000) / 1_000_000.0)
|
||||||
.sog(rs.getBigDecimal("sog"))
|
.sog(scaleDecimal(rs.getBigDecimal("sog"), 1))
|
||||||
.cog(rs.getBigDecimal("cog"))
|
.cog(scaleDecimal(rs.getBigDecimal("cog"), 1))
|
||||||
.shipNm(rs.getString("ship_nm"))
|
.shipNm(rs.getString("ship_nm"))
|
||||||
.shipTy(shipTy)
|
.shipTy(shipTy)
|
||||||
.shipKindCode(shipKindCode)
|
.shipKindCode(shipKindCode)
|
||||||
@ -168,4 +170,8 @@ public class VesselPositionService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BigDecimal scaleDecimal(BigDecimal value, int scale) {
|
||||||
|
return value != null ? value.setScale(scale, RoundingMode.HALF_UP) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package gc.mda.signal_batch.global.config;
|
package gc.mda.signal_batch.global.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
@ -36,6 +37,7 @@ public class JacksonConfig {
|
|||||||
return Jackson2ObjectMapperBuilder.json()
|
return Jackson2ObjectMapperBuilder.json()
|
||||||
.modules(javaTimeModule)
|
.modules(javaTimeModule)
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||||
|
.serializationInclusion(JsonInclude.Include.NON_NULL)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,6 +3,10 @@ package gc.mda.signal_batch.monitoring.controller;
|
|||||||
import gc.mda.signal_batch.monitoring.service.BatchMetadataCleanupService;
|
import gc.mda.signal_batch.monitoring.service.BatchMetadataCleanupService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -641,7 +645,35 @@ public class BatchAdminController {
|
|||||||
* 프론트엔드에서 Stacked Bar (처리 건수) + Duration Line (소요시간) 차트 표시.
|
* 프론트엔드에서 Stacked Bar (처리 건수) + Duration Line (소요시간) 차트 표시.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/daily-stats")
|
@GetMapping("/daily-stats")
|
||||||
@Operation(summary = "일별 처리 통계", description = "최근 7일간 일별 Job별 배치 처리 통계를 조회합니다 (대시보드 차트용)")
|
@Operation(
|
||||||
|
summary = "일별 처리 통계",
|
||||||
|
description = "최근 7일간 일별 Job별 배치 처리 통계를 조회합니다 (대시보드 차트용)"
|
||||||
|
)
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "일별 통계 + 24시간 상태 요약",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = Object.class),
|
||||||
|
examples = @ExampleObject(value = """
|
||||||
|
{
|
||||||
|
"dailyStats": [
|
||||||
|
{
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"totalWrite": 285000,
|
||||||
|
"jobs": {
|
||||||
|
"incrementalVesselJob": { "writeCount": 120000, "execCount": 288, "avgDuration": 45 },
|
||||||
|
"trackBuildJob": { "writeCount": 95000, "execCount": 288, "avgDuration": 30 },
|
||||||
|
"hourlyAggregationJob": { "writeCount": 50000, "execCount": 24, "avgDuration": 15 },
|
||||||
|
"dailyAggregationJob": { "writeCount": 20000, "execCount": 1, "avgDuration": 40 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statusSummary": { "completed": 601, "failed": 0, "stopped": 0 }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
)
|
||||||
public ResponseEntity<Map<String, Object>> getDailyStatistics() {
|
public ResponseEntity<Map<String, Object>> getDailyStatistics() {
|
||||||
try {
|
try {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package gc.mda.signal_batch.monitoring.controller;
|
package gc.mda.signal_batch.monitoring.controller;
|
||||||
|
|
||||||
import gc.mda.signal_batch.global.util.ConcurrentUpdateManager;
|
import gc.mda.signal_batch.global.util.ConcurrentUpdateManager;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@ -13,11 +15,13 @@ import java.util.Map;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/admin/locks")
|
@RequestMapping("/admin/locks")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Tag(name = "Lock 모니터링 API", description = "배치 동시성 Lock 상태 및 Deadlock 정보 조회")
|
||||||
public class LockMonitorController {
|
public class LockMonitorController {
|
||||||
|
|
||||||
private final ConcurrentUpdateManager concurrentUpdateManager;
|
private final ConcurrentUpdateManager concurrentUpdateManager;
|
||||||
|
|
||||||
@GetMapping("/statistics")
|
@GetMapping("/statistics")
|
||||||
|
@Operation(summary = "Lock 통계 조회", description = "현재 활성 Lock 및 Lock 사용 통계를 조회합니다")
|
||||||
public ResponseEntity<Map<String, Object>> getLockStatistics() {
|
public ResponseEntity<Map<String, Object>> getLockStatistics() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("lockStats", concurrentUpdateManager.getLockStatistics());
|
response.put("lockStats", concurrentUpdateManager.getLockStatistics());
|
||||||
@ -28,6 +32,7 @@ public class LockMonitorController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/deadlocks")
|
@GetMapping("/deadlocks")
|
||||||
|
@Operation(summary = "Deadlock 정보 조회", description = "감지된 Deadlock 이력 정보를 조회합니다")
|
||||||
public ResponseEntity<Map<String, Object>> getDeadlockInfo() {
|
public ResponseEntity<Map<String, Object>> getDeadlockInfo() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("deadlocks", concurrentUpdateManager.getDeadlockInfo());
|
response.put("deadlocks", concurrentUpdateManager.getDeadlockInfo());
|
||||||
|
|||||||
@ -2,6 +2,10 @@ server:
|
|||||||
shutdown: graceful
|
shutdown: graceful
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /signal-batch
|
context-path: /signal-batch
|
||||||
|
compression:
|
||||||
|
enabled: true
|
||||||
|
mime-types: application/json,application/xml,text/html,text/xml,text/plain
|
||||||
|
min-response-size: 1024
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user