feat: IntegrationVessel 전용 DataSource 지원 및 프로파일 호환성 보장
- IntegrationVesselService에 전용 DataSource 자체 생성/관리 추가 - vessel.integration.datasource.* 설정으로 별도 DB 연결 가능 - @PostConstruct에서 경량 HikariDataSource 생성 (max 3, min 1) - 미설정 시 queryDataSource 자동 폴백 (기존 동작 유지) - prod: 별도 DB (mdadb2 gis.t_ship_integration_sub) → 전용 DataSource - dev/prod-mpr: queryDB signal 스키마 → queryDataSource 폴백 - 기존 DataSourceConfig 4개 파일 수정 없이 프로파일 완벽 호환 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
e9d5d36928
커밋
e729316edf
@ -169,6 +169,30 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 7 — 통합선박(IntegrationVessel) 전용 DataSource
|
||||||
|
|
||||||
|
- [x] **7.1** IntegrationVesselService 전용 DataSource 지원
|
||||||
|
- `vessel.integration.datasource.*` 설정으로 별도 DB 연결 가능
|
||||||
|
- `@PostConstruct`에서 전용 HikariDataSource 생성 (max 3, min 1)
|
||||||
|
- 미설정 시 queryDataSource 자동 폴백 (기존 동작 유지)
|
||||||
|
- `@PreDestroy`에서 전용 DataSource 정리
|
||||||
|
- 상태: **완료** (2026-02-07)
|
||||||
|
|
||||||
|
- [x] **7.2** 설정 기반 테이블명
|
||||||
|
- `vessel.integration.table-name` 프로파일별 분리
|
||||||
|
- prod: `t_ship_integration_sub` (gis 스키마는 jdbc-url currentSchema)
|
||||||
|
- dev/prod-mpr/local/query: `signal.t_ship_integration_sub` (기본값)
|
||||||
|
- 상태: **완료** (2026-02-07)
|
||||||
|
|
||||||
|
- [x] **7.3** 프로파일 호환성 유지
|
||||||
|
- prod: 별도 DB (10.188.141.230:5432/mdadb2 gis) → 전용 DataSource
|
||||||
|
- dev/prod-mpr: queryDB signal 스키마 → queryDataSource 폴백
|
||||||
|
- local/query: integration 비활성화 (기본값)
|
||||||
|
- 기존 DataSourceConfig 4개 파일 수정 없음
|
||||||
|
- 상태: **완료** (2026-02-07)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 커밋 이력
|
## 커밋 이력
|
||||||
|
|
||||||
| 날짜 | Phase | 커밋 메시지 | 해시 |
|
| 날짜 | Phase | 커밋 메시지 | 해시 |
|
||||||
@ -180,3 +204,7 @@
|
|||||||
| 2026-02-06 | 2.2 | refactor: 쿼리 생명주기 관리 서비스 finally 블록으로 단일화 | 28908e1 |
|
| 2026-02-06 | 2.2 | refactor: 쿼리 생명주기 관리 서비스 finally 블록으로 단일화 | 28908e1 |
|
||||||
| 2026-02-06 | 3 | perf: 백프레셔 고도화 - 정확한 버퍼 추적 및 적응형 지연 | 7b7e283 |
|
| 2026-02-06 | 3 | perf: 백프레셔 고도화 - 정확한 버퍼 추적 및 적응형 지연 | 7b7e283 |
|
||||||
| 2026-02-06 | 4 | feat: WebSocket 설정 외부화 및 부하 제어 모니터링 엔드포인트 | c92bf0e |
|
| 2026-02-06 | 4 | feat: WebSocket 설정 외부화 및 부하 제어 모니터링 엔드포인트 | c92bf0e |
|
||||||
|
| 2026-02-06 | 5 | feat: 대기열 기반 쿼리 관리 및 타임아웃 최적화 | 7bd7bf5 |
|
||||||
|
| 2026-02-06 | 6 | feat: 일일 데이터 인메모리 캐시 구현 | 03b14e6 |
|
||||||
|
| 2026-02-06 | 5~6 | docs: Phase 5~6 구현 진행 문서 및 성능 보고서 업데이트 | dc586dd |
|
||||||
|
| 2026-02-06 | 6 | fix: 캐시-DB 하이브리드 조회 시 뷰포트 2-pass 필터링 정합성 보장 | e9d5d36 |
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
package gc.mda.signal_batch.domain.vessel.service;
|
package gc.mda.signal_batch.domain.vessel.service;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
import gc.mda.signal_batch.domain.vessel.dto.IntegrationVessel;
|
import gc.mda.signal_batch.domain.vessel.dto.IntegrationVessel;
|
||||||
import gc.mda.signal_batch.global.util.IntegrationSignalConstants;
|
import gc.mda.signal_batch.global.util.IntegrationSignalConstants;
|
||||||
import gc.mda.signal_batch.global.util.IntegrationSignalConstants.SignalType;
|
import gc.mda.signal_batch.global.util.IntegrationSignalConstants.SignalType;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@ -18,6 +22,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
/**
|
/**
|
||||||
* 통합선박 정보 서비스
|
* 통합선박 정보 서비스
|
||||||
* 글로벌 캐시를 통한 통합선박 정보 관리
|
* 글로벌 캐시를 통한 통합선박 정보 관리
|
||||||
|
*
|
||||||
|
* 전용 DataSource 설정 (vessel.integration.datasource.*):
|
||||||
|
* - jdbc-url이 설정되면 별도 DB에서 통합선박 정보를 로드
|
||||||
|
* - 미설정 시 queryDataSource를 폴백으로 사용
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -25,19 +33,74 @@ public class IntegrationVesselService {
|
|||||||
|
|
||||||
private final DataSource queryDataSource;
|
private final DataSource queryDataSource;
|
||||||
|
|
||||||
@Value("${vessel.integration.enabled:true}")
|
@Value("${vessel.integration.enabled:false}")
|
||||||
private boolean integrationEnabled;
|
private boolean integrationEnabled;
|
||||||
|
|
||||||
|
@Value("${vessel.integration.datasource.jdbc-url:}")
|
||||||
|
private String integrationJdbcUrl;
|
||||||
|
|
||||||
|
@Value("${vessel.integration.datasource.username:}")
|
||||||
|
private String integrationUsername;
|
||||||
|
|
||||||
|
@Value("${vessel.integration.datasource.password:}")
|
||||||
|
private String integrationPassword;
|
||||||
|
|
||||||
|
@Value("${vessel.integration.datasource.table-name:signal.t_ship_integration_sub}")
|
||||||
|
private String integrationTableName;
|
||||||
|
|
||||||
// 글로벌 캐시 (키: "sig_src_cd_target_id")
|
// 글로벌 캐시 (키: "sig_src_cd_target_id")
|
||||||
private final Map<String, IntegrationVessel> integrationCache = new ConcurrentHashMap<>();
|
private final Map<String, IntegrationVessel> integrationCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// 캐시 로드 상태
|
// 캐시 로드 상태
|
||||||
private final AtomicBoolean cacheLoaded = new AtomicBoolean(false);
|
private final AtomicBoolean cacheLoaded = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
// 전용 DataSource (별도 DB 사용 시)
|
||||||
|
private DataSource integrationDataSource;
|
||||||
|
private boolean dedicatedDataSource = false;
|
||||||
|
|
||||||
public IntegrationVesselService(@Qualifier("queryDataSource") DataSource queryDataSource) {
|
public IntegrationVesselService(@Qualifier("queryDataSource") DataSource queryDataSource) {
|
||||||
this.queryDataSource = queryDataSource;
|
this.queryDataSource = queryDataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (integrationEnabled && integrationJdbcUrl != null && !integrationJdbcUrl.isBlank()) {
|
||||||
|
try {
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setJdbcUrl(integrationJdbcUrl);
|
||||||
|
config.setUsername(integrationUsername);
|
||||||
|
config.setPassword(integrationPassword);
|
||||||
|
config.setDriverClassName("org.postgresql.Driver");
|
||||||
|
config.setMaximumPoolSize(3);
|
||||||
|
config.setMinimumIdle(1);
|
||||||
|
config.setPoolName("IntegrationHikariPool");
|
||||||
|
config.setConnectionTimeout(10000);
|
||||||
|
config.setIdleTimeout(300000);
|
||||||
|
config.setMaxLifetime(600000);
|
||||||
|
config.setConnectionTestQuery("SELECT 1");
|
||||||
|
this.integrationDataSource = new HikariDataSource(config);
|
||||||
|
this.dedicatedDataSource = true;
|
||||||
|
log.info("Integration dedicated DataSource created: {}", integrationJdbcUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to create integration DataSource, falling back to queryDataSource: {}", e.getMessage());
|
||||||
|
this.integrationDataSource = queryDataSource;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.integrationDataSource = queryDataSource;
|
||||||
|
if (integrationEnabled) {
|
||||||
|
log.info("Integration using queryDataSource (no dedicated datasource configured)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
if (dedicatedDataSource && integrationDataSource instanceof HikariDataSource hikari) {
|
||||||
|
hikari.close();
|
||||||
|
log.info("Integration dedicated DataSource closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 통합선박 기능 활성화 여부
|
* 통합선박 기능 활성화 여부
|
||||||
*/
|
*/
|
||||||
@ -137,6 +200,8 @@ public class IntegrationVesselService {
|
|||||||
status.put("enabled", integrationEnabled);
|
status.put("enabled", integrationEnabled);
|
||||||
status.put("loaded", cacheLoaded.get());
|
status.put("loaded", cacheLoaded.get());
|
||||||
status.put("size", integrationCache.size());
|
status.put("size", integrationCache.size());
|
||||||
|
status.put("dedicatedDataSource", dedicatedDataSource);
|
||||||
|
status.put("tableName", integrationTableName);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,14 +226,11 @@ public class IntegrationVesselService {
|
|||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(queryDataSource);
|
JdbcTemplate jdbcTemplate = new JdbcTemplate(integrationDataSource);
|
||||||
|
|
||||||
String sql = """
|
String sql = "SELECT intgr_seq, ais, enav, vpass, vts_ais, d_mf_hf," +
|
||||||
SELECT intgr_seq, ais, enav, vpass, vts_ais, d_mf_hf,
|
" ais_ship_nm, enav_ship_nm, vpass_ship_nm, vts_ais_ship_nm, d_mf_hf_ship_nm," +
|
||||||
ais_ship_nm, enav_ship_nm, vpass_ship_nm, vts_ais_ship_nm, d_mf_hf_ship_nm,
|
" integration_ship_ty FROM " + integrationTableName;
|
||||||
integration_ship_ty
|
|
||||||
FROM signal.t_ship_integration_sub
|
|
||||||
""";
|
|
||||||
|
|
||||||
List<IntegrationVessel> vessels = jdbcTemplate.query(sql, (rs, rowNum) ->
|
List<IntegrationVessel> vessels = jdbcTemplate.query(sql, (rs, rowNum) ->
|
||||||
IntegrationVessel.builder()
|
IntegrationVessel.builder()
|
||||||
|
|||||||
@ -104,7 +104,7 @@ logging:
|
|||||||
vessel: # spring 하위가 아닌 최상위 레벨
|
vessel: # spring 하위가 아닌 최상위 레벨
|
||||||
# 통합선박 설정
|
# 통합선박 설정
|
||||||
integration:
|
integration:
|
||||||
enabled: true # 통합선박 기능 활성화 여부
|
enabled: true # queryDB signal.t_ship_integration_sub 사용 (기본값)
|
||||||
batch:
|
batch:
|
||||||
# Area Statistics 처리를 위한 별도 설정
|
# Area Statistics 처리를 위한 별도 설정
|
||||||
area-statistics:
|
area-statistics:
|
||||||
|
|||||||
@ -105,7 +105,12 @@ logging:
|
|||||||
vessel: # spring 하위가 아닌 최상위 레벨
|
vessel: # spring 하위가 아닌 최상위 레벨
|
||||||
# 통합선박 설정
|
# 통합선박 설정
|
||||||
integration:
|
integration:
|
||||||
enabled: true # 통합선박 기능 활성화 여부
|
enabled: true
|
||||||
|
datasource:
|
||||||
|
jdbc-url: jdbc:postgresql://10.188.141.230:5432/mdadb2?currentSchema=gis&options=-csearch_path=gis,public&TimeZone=Asia/Seoul
|
||||||
|
username: mda
|
||||||
|
password: mda#8932
|
||||||
|
table-name: t_ship_integration_sub # gis 스키마는 jdbc-url의 currentSchema로 지정
|
||||||
batch:
|
batch:
|
||||||
# Area Statistics 처리를 위한 별도 설정
|
# Area Statistics 처리를 위한 별도 설정
|
||||||
area-statistics:
|
area-statistics:
|
||||||
|
|||||||
@ -136,6 +136,11 @@ vessel:
|
|||||||
enabled: ${INTEGRATION_ENABLED:false} # 통합선박 기능 활성화 여부
|
enabled: ${INTEGRATION_ENABLED:false} # 통합선박 기능 활성화 여부
|
||||||
cache:
|
cache:
|
||||||
refresh-cron: "0 0 6 * * ?" # 매일 06:00 갱신
|
refresh-cron: "0 0 6 * * ?" # 매일 06:00 갱신
|
||||||
|
datasource:
|
||||||
|
jdbc-url: "" # 빈 값: queryDataSource 폴백, 별도 DB 사용 시 프로파일에서 오버라이드
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
table-name: signal.t_ship_integration_sub # 기본: queryDB의 signal 스키마
|
||||||
|
|
||||||
batch:
|
batch:
|
||||||
chunk-size: ${BATCH_CHUNK_SIZE:10000}
|
chunk-size: ${BATCH_CHUNK_SIZE:10000}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user