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:
HeungTak Lee 2026-02-07 07:23:44 +09:00
부모 e9d5d36928
커밋 e729316edf
5개의 변경된 파일110개의 추가작업 그리고 10개의 파일을 삭제

파일 보기

@ -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 | 커밋 메시지 | 해시 |
@ -180,3 +204,7 @@
| 2026-02-06 | 2.2 | refactor: 쿼리 생명주기 관리 서비스 finally 블록으로 단일화 | 28908e1 |
| 2026-02-06 | 3 | perf: 백프레셔 고도화 - 정확한 버퍼 추적 및 적응형 지연 | 7b7e283 |
| 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;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
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.SignalType;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
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
@Service
@ -25,19 +33,74 @@ public class IntegrationVesselService {
private final DataSource queryDataSource;
@Value("${vessel.integration.enabled:true}")
@Value("${vessel.integration.enabled:false}")
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")
private final Map<String, IntegrationVessel> integrationCache = new ConcurrentHashMap<>();
// 캐시 로드 상태
private final AtomicBoolean cacheLoaded = new AtomicBoolean(false);
// 전용 DataSource (별도 DB 사용 )
private DataSource integrationDataSource;
private boolean dedicatedDataSource = false;
public IntegrationVesselService(@Qualifier("queryDataSource") DataSource 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("loaded", cacheLoaded.get());
status.put("size", integrationCache.size());
status.put("dedicatedDataSource", dedicatedDataSource);
status.put("tableName", integrationTableName);
return status;
}
@ -161,14 +226,11 @@ public class IntegrationVesselService {
long startTime = System.currentTimeMillis();
try {
JdbcTemplate jdbcTemplate = new JdbcTemplate(queryDataSource);
JdbcTemplate jdbcTemplate = new JdbcTemplate(integrationDataSource);
String sql = """
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,
integration_ship_ty
FROM signal.t_ship_integration_sub
""";
String sql = "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," +
" integration_ship_ty FROM " + integrationTableName;
List<IntegrationVessel> vessels = jdbcTemplate.query(sql, (rs, rowNum) ->
IntegrationVessel.builder()

파일 보기

@ -104,7 +104,7 @@ logging:
vessel: # spring 하위가 아닌 최상위 레벨
# 통합선박 설정
integration:
enabled: true # 통합선박 기능 활성화 여부
enabled: true # queryDB signal.t_ship_integration_sub 사용 (기본값)
batch:
# Area Statistics 처리를 위한 별도 설정
area-statistics:

파일 보기

@ -105,7 +105,12 @@ logging:
vessel: # spring 하위가 아닌 최상위 레벨
# 통합선박 설정
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:
# Area Statistics 처리를 위한 별도 설정
area-statistics:

파일 보기

@ -136,6 +136,11 @@ vessel:
enabled: ${INTEGRATION_ENABLED:false} # 통합선박 기능 활성화 여부
cache:
refresh-cron: "0 0 6 * * ?" # 매일 06:00 갱신
datasource:
jdbc-url: "" # 빈 값: queryDataSource 폴백, 별도 DB 사용 시 프로파일에서 오버라이드
username: ""
password: ""
table-name: signal.t_ship_integration_sub # 기본: queryDB의 signal 스키마
batch:
chunk-size: ${BATCH_CHUNK_SIZE:10000}