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:
htlee 2026-03-01 22:54:19 +09:00
부모 a90662b2f0
커밋 0249a1fb90
6개의 변경된 파일72개의 추가작업 그리고 11개의 파일을 삭제

파일 보기

@ -1,5 +1,9 @@
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.Resource;
import org.springframework.http.HttpHeaders;
@ -17,16 +21,20 @@ import java.nio.file.Paths;
@RestController
@RequestMapping("/api/tiles")
@Tag(name = "지도 타일 API", description = "WebP 형식의 배경 지도 타일 서비스 (World/ENC)")
public class MapTileController {
private static final String TILE_BASE_PATH = "/devdata/MAPS/WORLD_webp";
private static final String TILE_ENC_PATH = "/devdata/MAPS/ENC_RAS_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(
@PathVariable int z,
@PathVariable int x,
@PathVariable int y) {
@Parameter(description = "줌 레벨 (0~18)") @PathVariable int z,
@Parameter(description = "X 좌표 (열)") @PathVariable int x,
@Parameter(description = "Y 좌표 (행)") @PathVariable int y) {
try {
// 안전한 경로 생성
@ -59,10 +67,13 @@ public class MapTileController {
}
@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(
@PathVariable int z,
@PathVariable int x,
@PathVariable int y) {
@Parameter(description = "줌 레벨 (0~18)") @PathVariable int z,
@Parameter(description = "X 좌표 (열)") @PathVariable int x,
@Parameter(description = "Y 좌표 (행)") @PathVariable int y) {
try {
// 안전한 경로 생성
@ -95,6 +106,7 @@ public class MapTileController {
}
@GetMapping("/health")
@Operation(summary = "타일 서비스 상태 확인", description = "타일 디렉토리 존재 여부로 서비스 상태를 확인합니다")
public ResponseEntity<String> checkTileService() {
File baseDir = new File(TILE_BASE_PATH);
if (baseDir.exists() && baseDir.isDirectory()) {

파일 보기

@ -12,6 +12,8 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@ -155,10 +157,10 @@ public class VesselPositionService {
return RecentVesselPositionDto.builder()
.mmsi(mmsi)
.imo(imo > 0 ? imo : null)
.lon(rs.getDouble("lon"))
.lat(rs.getDouble("lat"))
.sog(rs.getBigDecimal("sog"))
.cog(rs.getBigDecimal("cog"))
.lon(Math.round(rs.getDouble("lon") * 1_000_000) / 1_000_000.0)
.lat(Math.round(rs.getDouble("lat") * 1_000_000) / 1_000_000.0)
.sog(scaleDecimal(rs.getBigDecimal("sog"), 1))
.cog(scaleDecimal(rs.getBigDecimal("cog"), 1))
.shipNm(rs.getString("ship_nm"))
.shipTy(shipTy)
.shipKindCode(shipKindCode)
@ -168,4 +170,8 @@ public class VesselPositionService {
.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;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@ -36,6 +37,7 @@ public class JacksonConfig {
return Jackson2ObjectMapperBuilder.json()
.modules(javaTimeModule)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}
}

파일 보기

@ -3,6 +3,10 @@ package gc.mda.signal_batch.monitoring.controller;
import gc.mda.signal_batch.monitoring.service.BatchMetadataCleanupService;
import io.swagger.v3.oas.annotations.Operation;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -641,7 +645,35 @@ public class BatchAdminController {
* 프론트엔드에서 Stacked Bar (처리 건수) + Duration Line (소요시간) 차트 표시.
*/
@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() {
try {
long start = System.currentTimeMillis();

파일 보기

@ -1,6 +1,8 @@
package gc.mda.signal_batch.monitoring.controller;
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 org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@ -13,11 +15,13 @@ import java.util.Map;
@RestController
@RequestMapping("/admin/locks")
@RequiredArgsConstructor
@Tag(name = "Lock 모니터링 API", description = "배치 동시성 Lock 상태 및 Deadlock 정보 조회")
public class LockMonitorController {
private final ConcurrentUpdateManager concurrentUpdateManager;
@GetMapping("/statistics")
@Operation(summary = "Lock 통계 조회", description = "현재 활성 Lock 및 Lock 사용 통계를 조회합니다")
public ResponseEntity<Map<String, Object>> getLockStatistics() {
Map<String, Object> response = new HashMap<>();
response.put("lockStats", concurrentUpdateManager.getLockStatistics());
@ -28,6 +32,7 @@ public class LockMonitorController {
}
@GetMapping("/deadlocks")
@Operation(summary = "Deadlock 정보 조회", description = "감지된 Deadlock 이력 정보를 조회합니다")
public ResponseEntity<Map<String, Object>> getDeadlockInfo() {
Map<String, Object> response = new HashMap<>();
response.put("deadlocks", concurrentUpdateManager.getDeadlockInfo());

파일 보기

@ -2,6 +2,10 @@ server:
shutdown: graceful
servlet:
context-path: /signal-batch
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/xml,text/plain
min-response-size: 1024
spring:
application: