diff --git a/VMIS_INTEGRATION_STRATEGY_DESIGN.md b/VMIS_INTEGRATION_STRATEGY_DESIGN.md new file mode 100644 index 0000000..89799c7 --- /dev/null +++ b/VMIS_INTEGRATION_STRATEGY_DESIGN.md @@ -0,0 +1,802 @@ +# VMIS 통합 전략 설계 문서 + +**작성일**: 2025-11-06 +**목적**: 설정 기반으로 내부/외부 차량 정보 조회 방식을 선택할 수 있는 아키텍처 설계 + +--- + +## 목차 + +1. [요구사항](#1-요구사항) +2. [아키텍처 설계](#2-아키텍처-설계) +3. [구현 가이드](#3-구현-가이드) +4. [설정 예시](#4-설정-예시) +5. [테스트 방법](#5-테스트-방법) + +--- + +## 1. 요구사항 + +### 1.1 핵심 요구사항 + +**application.yml 설정에 따라 두 가지 방식 중 선택 가능**: + +1. **Internal Mode (내부 모드)** + - 신규 통합된 VMIS 모듈을 직접 호출 + - 네트워크 오버헤드 없음 + - 단일 트랜잭션 처리 가능 + - 높은 성능 + +2. **External Mode (외부 모드)** + - 기존 ExternalVehicleApiService를 통해 외부 REST API 호출 + - 독립적인 VMIS-interface 서버와 통신 + - 기존 방식 유지 (하위 호환성) + +### 1.2 전환 시나리오 + +- **개발 단계**: External 모드로 테스트 (기존 방식) +- **통합 테스트**: Internal 모드로 전환하여 성능 검증 +- **점진적 배포**: 일부 서버는 External, 일부는 Internal +- **롤백 대비**: 문제 발생 시 External 모드로 즉시 전환 + +--- + +## 2. 아키텍처 설계 + +### 2.1 전략 패턴 (Strategy Pattern) 적용 + +``` +┌─────────────────────────────────────────┐ +│ VehicleInfoServiceFacade │ (클라이언트 코드) +│ - getBasicInfo(vhrno) │ +│ - getLedgerInfo(vhrno) │ +└─────────────────┬───────────────────────┘ + │ 의존 + ↓ +┌─────────────────────────────────────────┐ +│ <> │ +│ VehicleInfoService │ (전략 인터페이스) +│ + getBasicInfo(vhrno): ResponseDTO │ +│ + getLedgerInfo(vhrno): ResponseDTO │ +└─────────────────┬───────────────────────┘ + │ + ┌────────┴─────────┐ + ↓ ↓ +┌──────────────────┐ ┌─────────────────────────┐ +│ Internal모드 │ │ External모드 │ +│ │ │ │ +│ Internal │ │ External │ +│ VehicleInfo │ │ VehicleInfo │ +│ ServiceImpl │ │ ServiceImpl │ +│ │ │ │ +│ @Conditional │ │ @Conditional │ +│ OnProperty │ │ OnProperty │ +│ (internal) │ │ (external) │ +│ │ │ │ +│ ↓ │ │ ↓ │ +│ VMIS 모듈 │ │ RestTemplate │ +│ 직접 호출 │ │ (HTTP 호출) │ +└──────────────────┘ └─────────────────────────┘ +``` + +### 2.2 컴포넌트 설계 + +#### 2.2.1 인터페이스 + +**VehicleInfoService.java** (전략 인터페이스) +```java +package go.kr.project.common.service; + +import go.kr.project.common.vo.VehicleBasicInfoResponseVO; +import go.kr.project.common.vo.VehicleLedgerResponseVO; + +public interface VehicleInfoService { + + /** + * 차량 기본정보 조회 + * @param vhrno 차량번호 + * @return 차량 기본정보 응답 + */ + VehicleBasicInfoResponseVO getBasicInfo(String vhrno); + + /** + * 차량 등록원부 조회 + * @param vhrno 차량번호 + * @return 차량 등록원부 응답 + */ + VehicleLedgerResponseVO getLedgerInfo(String vhrno); +} +``` + +#### 2.2.2 구현체 1: Internal Mode + +**InternalVehicleInfoServiceImpl.java** +```java +package go.kr.project.vmis.service; + +import go.kr.project.common.service.VehicleInfoService; +import go.kr.project.common.vo.*; +import go.kr.project.vmis.service.CarBassMatterInqireService; +import go.kr.project.vmis.service.CarLedgerFrmbkService; +import go.kr.project.vmis.model.vo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal") +public class InternalVehicleInfoServiceImpl implements VehicleInfoService { + + private final CarBassMatterInqireService carBassMatterInqireService; + private final CarLedgerFrmbkService carLedgerFrmbkService; + + @Override + public VehicleBasicInfoResponseVO getBasicInfo(String vhrno) { + log.info("[INTERNAL MODE] 차량 기본정보 조회 - 차량번호: {}", vhrno); + + try { + // 내부 VMIS 모듈 직접 호출 + CarBassMatterInqireRequestVO request = new CarBassMatterInqireRequestVO(); + request.setVhrno(vhrno); + + Envelope response = + carBassMatterInqireService.getBasicInfo(request); + + // DTO → VO 변환 + return convertToBasicInfoResponse(response); + + } catch (Exception e) { + log.error("[INTERNAL MODE] 차량 기본정보 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorResponse(vhrno, e.getMessage()); + } + } + + @Override + public VehicleLedgerResponseVO getLedgerInfo(String vhrno) { + log.info("[INTERNAL MODE] 차량 등록원부 조회 - 차량번호: {}", vhrno); + + try { + // 내부 VMIS 모듈 직접 호출 + CarLedgerFrmbkRequestVO request = new CarLedgerFrmbkRequestVO(); + request.setVhrno(vhrno); + + Envelope response = + carLedgerFrmbkService.getLedgerInfo(request); + + // DTO → VO 변환 + return convertToLedgerResponse(response); + + } catch (Exception e) { + log.error("[INTERNAL MODE] 차량 등록원부 조회 실패 - 차량번호: {}", vhrno, e); + return createLedgerErrorResponse(vhrno, e.getMessage()); + } + } + + // DTO → VO 변환 메서드들... + private VehicleBasicInfoResponseVO convertToBasicInfoResponse(Envelope envelope) { + // 구현... + } + + private VehicleLedgerResponseVO convertToLedgerResponse(Envelope envelope) { + // 구현... + } +} +``` + +#### 2.2.3 구현체 2: External Mode + +**ExternalVehicleInfoServiceImpl.java** +```java +package go.kr.project.externalApi.service; + +import go.kr.project.common.service.VehicleInfoService; +import go.kr.project.common.vo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true) +public class ExternalVehicleInfoServiceImpl implements VehicleInfoService { + + private final RestTemplate restTemplate; + + @Value("${vmis.external.api.url:http://localhost:8081/api/v1/vehicles}") + private String vmisApiUrl; + + @Override + public VehicleBasicInfoResponseVO getBasicInfo(String vhrno) { + log.info("[EXTERNAL MODE] 차량 기본정보 조회 - 차량번호: {}", vhrno); + + try { + String url = vmisApiUrl + "/basic"; + + // 기존 ExternalVehicleApiService 로직 활용 + // RestTemplate으로 외부 API 호출 + VehicleBasicRequestVO request = new VehicleBasicRequestVO(); + request.setVhrno(vhrno); + + VehicleBasicInfoResponseVO response = restTemplate.postForObject( + url, + request, + VehicleBasicInfoResponseVO.class + ); + + return response; + + } catch (Exception e) { + log.error("[EXTERNAL MODE] 차량 기본정보 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorResponse(vhrno, e.getMessage()); + } + } + + @Override + public VehicleLedgerResponseVO getLedgerInfo(String vhrno) { + log.info("[EXTERNAL MODE] 차량 등록원부 조회 - 차량번호: {}", vhrno); + + try { + String url = vmisApiUrl + "/ledger"; + + VehicleLedgerRequestVO request = new VehicleLedgerRequestVO(); + request.setVhrno(vhrno); + + VehicleLedgerResponseVO response = restTemplate.postForObject( + url, + request, + VehicleLedgerResponseVO.class + ); + + return response; + + } catch (Exception e) { + log.error("[EXTERNAL MODE] 차량 등록원부 조회 실패 - 차량번호: {}", vhrno, e); + return createLedgerErrorResponse(vhrno, e.getMessage()); + } + } +} +``` + +#### 2.2.4 설정 클래스 + +**VmisIntegrationConfig.java** +```java +package go.kr.project.vmis.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Slf4j +@Configuration +@EnableConfigurationProperties(VmisProperties.class) +public class VmisIntegrationConfig { + + @Configuration + @ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal") + static class InternalModeConfig { + @PostConstruct + public void init() { + log.info("================================================="); + log.info("VMIS Integration Mode: INTERNAL"); + log.info("차량 정보 조회: 내부 VMIS 모듈 직접 호출"); + log.info("================================================="); + } + } + + @Configuration + @ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true) + static class ExternalModeConfig { + @PostConstruct + public void init() { + log.info("================================================="); + log.info("VMIS Integration Mode: EXTERNAL"); + log.info("차량 정보 조회: 외부 REST API 호출"); + log.info("================================================="); + } + } +} +``` + +--- + +## 3. 구현 가이드 + +### 3.1 파일 구조 + +``` +go/kr/project/ +├── common/ +│ ├── service/ +│ │ └── VehicleInfoService.java (인터페이스) +│ └── vo/ +│ ├── VehicleBasicInfoResponseVO.java +│ └── VehicleLedgerResponseVO.java +│ +├── vmis/ +│ ├── config/ +│ │ ├── VmisProperties.java +│ │ └── VmisIntegrationConfig.java (설정) +│ └── service/ +│ └── InternalVehicleInfoServiceImpl.java (내부 모드 구현) +│ +└── externalApi/ + └── service/ + └── ExternalVehicleInfoServiceImpl.java (외부 모드 구현) +``` + +### 3.2 기존 코드 리팩토링 + +#### Before (기존 ExternalVehicleApiService) +```java +@Service +public class ExternalVehicleApiService { + + private final RestTemplate restTemplate; + + public VehicleApiResponseVO getBasicInfo(String vhrno) { + // RestTemplate으로 외부 API 호출 + } +} +``` + +#### After (전략 패턴 적용) +```java +// 기존 ExternalVehicleApiService는 ExternalVehicleInfoServiceImpl로 대체 +// 클라이언트 코드는 VehicleInfoService 인터페이스에 의존 + +@Service +@RequiredArgsConstructor +public class CarInspectionService { + + private final VehicleInfoService vehicleInfoService; // 인터페이스 주입 + + public void processInspection(String vhrno) { + // 설정에 따라 자동으로 내부/외부 구현체가 주입됨 + VehicleBasicInfoResponseVO info = vehicleInfoService.getBasicInfo(vhrno); + // ... + } +} +``` + +### 3.3 구현 순서 + +1. **VehicleInfoService 인터페이스 생성** + - 위치: `go/kr/project/common/service/` + - 메서드: getBasicInfo, getLedgerInfo + +2. **InternalVehicleInfoServiceImpl 구현** + - 위치: `go/kr/project/vmis/service/` + - VMIS 모듈 직접 호출 + +3. **ExternalVehicleInfoServiceImpl 구현** + - 위치: `go/kr/project/externalApi/service/` + - 기존 ExternalVehicleApiService 로직 이전 + +4. **VmisIntegrationConfig 생성** + - 설정 로깅 및 검증 + +5. **기존 클라이언트 코드 수정** + - ExternalVehicleApiService → VehicleInfoService 의존성 변경 + +--- + +## 4. 설정 예시 + +### 4.1 application.yml (공통) + +```yaml +# VMIS 통합 모드 설정 +vmis: + integration: + mode: internal # internal | external + + # Internal 모드 설정 + system: + info-sys-id: "VMIS001" + info-sys-ip: "${SERVER_IP:192.168.1.100}" + manager-id: "admin" + manager-name: "관리자" + manager-tel: "02-1234-5678" + + gpki: + enabled: false + cert-path: "${GPKI_CERT_PATH:/path/to/cert.der}" + private-key-path: "${GPKI_PRIVATE_KEY_PATH:/path/to/private.key}" + private-key-password: "${GPKI_PASSWORD:}" + + government: + host: "https://www.vemanet.com" + base-path: "/openapi" + connect-timeout: 10000 + read-timeout: 15000 + services: + basic: + path: "/carBassMatterInqire" + api-key: "${GOV_API_KEY_BASIC:}" + ledger: + path: "/carLedgerFrmbk" + api-key: "${GOV_API_KEY_LEDGER:}" + + # External 모드 설정 + external: + api: + url: "http://localhost:8081/api/v1/vehicles" + connect-timeout: 5000 + read-timeout: 10000 +``` + +### 4.2 application-local.yml (개발 환경) + +```yaml +vmis: + integration: + mode: external # 개발 중에는 외부 API 사용 + + external: + api: + url: "http://localhost:8081/api/v1/vehicles" +``` + +### 4.3 application-dev.yml (개발 서버) + +```yaml +vmis: + integration: + mode: internal # 개발 서버에서는 내부 모듈 테스트 + + gpki: + enabled: false # 개발 서버는 암호화 비활성화 +``` + +### 4.4 application-prd.yml (운영 환경) + +```yaml +vmis: + integration: + mode: internal # 운영 환경은 내부 모듈 사용 + + gpki: + enabled: true # 운영 환경은 암호화 활성화 + cert-path: "${GPKI_CERT_PATH}" + private-key-path: "${GPKI_PRIVATE_KEY_PATH}" + private-key-password: "${GPKI_PASSWORD}" +``` + +### 4.5 환경변수 설정 예시 + +```bash +# Internal 모드 (운영) +export VMIS_INTEGRATION_MODE=internal +export GOV_API_KEY_BASIC=your-api-key-basic +export GOV_API_KEY_LEDGER=your-api-key-ledger +export GPKI_PASSWORD=your-gpki-password + +# External 모드 (롤백 시) +export VMIS_INTEGRATION_MODE=external +export VMIS_EXTERNAL_API_URL=http://vmis-interface-server:8081/api/v1/vehicles +``` + +--- + +## 5. 테스트 방법 + +### 5.1 단위 테스트 + +#### InternalVehicleInfoServiceImplTest.java +```java +@SpringBootTest +@TestPropertySource(properties = { + "vmis.integration.mode=internal" +}) +class InternalVehicleInfoServiceImplTest { + + @Autowired + private VehicleInfoService vehicleInfoService; + + @Test + void testGetBasicInfo() { + // given + String vhrno = "12가3456"; + + // when + VehicleBasicInfoResponseVO response = vehicleInfoService.getBasicInfo(vhrno); + + // then + assertNotNull(response); + assertEquals(vhrno, response.getVhrno()); + assertTrue(vehicleInfoService instanceof InternalVehicleInfoServiceImpl); + } +} +``` + +#### ExternalVehicleInfoServiceImplTest.java +```java +@SpringBootTest +@TestPropertySource(properties = { + "vmis.integration.mode=external", + "vmis.external.api.url=http://localhost:8081/api/v1/vehicles" +}) +class ExternalVehicleInfoServiceImplTest { + + @Autowired + private VehicleInfoService vehicleInfoService; + + @Test + void testGetBasicInfo() { + // given + String vhrno = "12가3456"; + + // when + VehicleBasicInfoResponseVO response = vehicleInfoService.getBasicInfo(vhrno); + + // then + assertNotNull(response); + assertTrue(vehicleInfoService instanceof ExternalVehicleInfoServiceImpl); + } +} +``` + +### 5.2 통합 테스트 + +#### 1. Internal 모드 테스트 +```bash +# application-local.yml 수정 +vmis: + integration: + mode: internal + +# 애플리케이션 실행 +./gradlew bootRun --args='--spring.profiles.active=local' + +# 로그 확인 +# [INTERNAL MODE] 차량 기본정보 조회 - 차량번호: 12가3456 + +# API 호출 +curl -X POST http://localhost:8080/api/inspection \ + -H "Content-Type: application/json" \ + -d '{"vhrno":"12가3456"}' +``` + +#### 2. External 모드 테스트 +```bash +# 1. VMIS-interface 서버 실행 (8081 포트) +cd D:\workspace\git\VMIS-interface +./gradlew bootRun + +# 2. application-local.yml 수정 +vmis: + integration: + mode: external + external: + api: + url: "http://localhost:8081/api/v1/vehicles" + +# 3. VIPS 실행 +cd D:\workspace\git\VIPS +./gradlew bootRun --args='--spring.profiles.active=local' + +# 로그 확인 +# [EXTERNAL MODE] 차량 기본정보 조회 - 차량번호: 12가3456 + +# API 호출 +curl -X POST http://localhost:8080/api/inspection \ + -H "Content-Type: application/json" \ + -d '{"vhrno":"12가3456"}' +``` + +### 5.3 성능 비교 테스트 + +```java +@SpringBootTest +class VehicleInfoServicePerformanceTest { + + @Autowired + private VehicleInfoService vehicleInfoService; + + @Test + void comparePerformance() { + String vhrno = "12가3456"; + int iterations = 100; + + // Warmup + for (int i = 0; i < 10; i++) { + vehicleInfoService.getBasicInfo(vhrno); + } + + // Performance test + long startTime = System.currentTimeMillis(); + for (int i = 0; i < iterations; i++) { + vehicleInfoService.getBasicInfo(vhrno); + } + long endTime = System.currentTimeMillis(); + + long avgTime = (endTime - startTime) / iterations; + log.info("평균 응답 시간: {}ms (모드: {})", avgTime, + vehicleInfoService.getClass().getSimpleName()); + + // Internal 모드가 External 모드보다 빠를 것으로 예상 + assertTrue(avgTime < 100); // 100ms 이내 + } +} +``` + +### 5.4 장애 상황 테스트 + +#### 시나리오 1: External 모드에서 외부 API 장애 +```bash +# VMIS-interface 서버 중지 +# → External 모드는 에러 반환 +# → 설정을 Internal 모드로 변경하여 복구 +``` + +#### 시나리오 2: Internal 모드에서 DB 장애 +```bash +# DB 연결 차단 +# → Internal 모드는 에러 반환 +# → 설정을 External 모드로 변경하여 복구 +``` + +--- + +## 6. 운영 시나리오 + +### 6.1 점진적 배포 (Blue-Green) + +**1단계: 준비** +```bash +# 모든 서버: External 모드 +vmis.integration.mode=external +``` + +**2단계: 일부 서버 전환** +```bash +# 서버 A, B: External 모드 +vmis.integration.mode=external + +# 서버 C, D: Internal 모드 (테스트) +vmis.integration.mode=internal +``` + +**3단계: 모니터링 후 전체 전환** +```bash +# 모든 서버: Internal 모드 +vmis.integration.mode=internal +``` + +### 6.2 롤백 절차 + +```bash +# 1. application-prd.yml 또는 환경변수 수정 +vmis.integration.mode=external + +# 2. 애플리케이션 재시작 (또는 설정 리로드) +# 3. 로그 확인 +# [EXTERNAL MODE] ... + +# 4. VMIS-interface 외부 서버가 실행 중인지 확인 +curl http://vmis-interface-server:8081/actuator/health +``` + +### 6.3 모니터링 지표 + +**Prometheus/Grafana 메트릭**: +```java +@Component +public class VehicleInfoServiceMetrics { + + private final Counter internalCalls; + private final Counter externalCalls; + private final Timer internalTimer; + private final Timer externalTimer; + + // 메트릭 수집... +} +``` + +**로그 분석**: +```bash +# Internal 모드 호출 건수 +grep "\[INTERNAL MODE\]" application.log | wc -l + +# External 모드 호출 건수 +grep "\[EXTERNAL MODE\]" application.log | wc -l + +# 평균 응답 시간 +grep "응답시간" application.log | awk '{sum+=$NF; count++} END {print sum/count}' +``` + +--- + +## 7. 주의사항 + +### 7.1 설정 검증 + +**애플리케이션 시작 시 체크**: +```java +@Component +@RequiredArgsConstructor +public class VmisIntegrationValidator implements ApplicationRunner { + + @Value("${vmis.integration.mode}") + private String integrationMode; + + @Autowired(required = false) + private VehicleInfoService vehicleInfoService; + + @Override + public void run(ApplicationArguments args) { + if (vehicleInfoService == null) { + throw new IllegalStateException( + "VehicleInfoService 빈이 생성되지 않았습니다. " + + "vmis.integration.mode 설정을 확인하세요: " + integrationMode + ); + } + + log.info("VMIS 통합 모드 검증 완료: {} (구현체: {})", + integrationMode, + vehicleInfoService.getClass().getSimpleName()); + } +} +``` + +### 7.2 External 모드 사용 시 주의점 + +1. **외부 서버 가용성 확인** + - VMIS-interface 서버가 실행 중이어야 함 + - Health check 엔드포인트 모니터링 + +2. **네트워크 타임아웃 설정** + - connect-timeout, read-timeout 적절히 설정 + - Circuit Breaker 패턴 고려 + +3. **API 버전 호환성** + - External 서버와 클라이언트의 API 계약 일치 확인 + +### 7.3 Internal 모드 사용 시 주의점 + +1. **트랜잭션 범위** + - @Transactional 설정 주의 + - DB 연결 풀 크기 조정 + +2. **GPKI 설정** + - 운영 환경에서는 gpki.enabled=true + - 인증서 경로 및 비밀번호 확인 + +3. **API 키 보안** + - 정부 API 키는 환경변수로 관리 + - 코드에 하드코딩 금지 + +--- + +## 8. FAQ + +### Q1: 설정을 변경하면 재시작이 필요한가요? +**A**: 네, @ConditionalOnProperty는 애플리케이션 시작 시에만 평가됩니다. 설정 변경 후 재시작이 필요합니다. + +### Q2: 두 모드를 동시에 사용할 수 있나요? +**A**: 아니오, 한 번에 하나의 모드만 활성화됩니다. 설정에 따라 하나의 구현체만 빈으로 등록됩니다. + +### Q3: External 모드에서 VMIS-interface 서버가 다운되면? +**A**: RestTemplate 타임아웃이 발생하고 에러가 반환됩니다. Circuit Breaker 패턴을 추가로 구현하면 장애 전파를 방지할 수 있습니다. + +### Q4: Internal 모드가 External 모드보다 얼마나 빠른가요? +**A**: 네트워크 오버헤드가 없으므로 평균 50-100ms 정도 빠를 것으로 예상됩니다. 실제 성능은 환경에 따라 다릅니다. + +### Q5: 개발 중에는 어떤 모드를 사용하나요? +**A**: 개발 초기에는 External 모드를 사용하여 기존 방식으로 개발하고, 통합 테스트 단계에서 Internal 모드로 전환하여 검증합니다. + +--- + +**문서 버전**: 1.0 +**최종 수정**: 2025-11-06 diff --git a/VMIS_INTERFACE_INTEGRATION_ANALYSIS.md b/VMIS_INTERFACE_INTEGRATION_ANALYSIS.md index fe0bded..da2d0a6 100644 --- a/VMIS_INTERFACE_INTEGRATION_ANALYSIS.md +++ b/VMIS_INTERFACE_INTEGRATION_ANALYSIS.md @@ -665,12 +665,19 @@ dependencies { - [ ] 정부 API 연동 테스트 - [ ] 로그 저장 확인 -### Phase 7: 기존 코드 정리 +### Phase 7: 전략 패턴 구현 (내부/외부 통신 분기) +- [ ] VehicleInfoService 인터페이스 생성 +- [ ] Internal/External 모드 구현체 작성 +- [ ] VmisIntegrationConfig 설정 클래스 +- [ ] application.yml 설정 추가 (vmis.integration.mode) +- [ ] 기존 클라이언트 코드 인터페이스 의존으로 변경 + +### Phase 8: 기존 코드 정리 - [ ] ExternalVehicleApiService 코드 리팩토링 - [ ] 불필요한 REST 호출 로직 제거 - [ ] 내부 메서드 호출로 변경 -### Phase 8: 문서화 및 배포 +### Phase 9: 문서화 및 배포 - [ ] API 문서 업데이트 - [ ] 운영 가이드 작성 - [ ] 코드 리뷰 @@ -729,9 +736,10 @@ D:\workspace\git\VMIS-interface\ddl\vips\*.sql | Phase 4: 패키지 변경 | 1시간 | 일괄 변경 | | Phase 5: 데이터베이스 | 10분 | 연결 테스트 (테이블 기존재) | | Phase 6: 빌드/테스트 | 2시간 | 디버깅 포함 | -| Phase 7: 기존 코드 정리 | 1시간 | 리팩토링 | -| Phase 8: 문서화 | 1시간 | 문서 작성 | -| **총 예상 시간** | **9시간 20분** | 순수 작업 시간 | +| Phase 7: 전략 패턴 구현 | 1.5시간 | 내부/외부 통신 분기 처리 | +| Phase 8: 기존 코드 정리 | 1시간 | 리팩토링 | +| Phase 9: 문서화 | 1시간 | 문서 작성 | +| **총 예상 시간** | **10시간 50분** | 순수 작업 시간 | --- @@ -749,5 +757,9 @@ git status # 현재 브랜치 확인 --- -**문서 버전**: 1.1 +**문서 버전**: 1.2 **최종 수정**: 2025-11-06 +**주요 변경사항**: +- 브랜치 생성 관련 내용 제거 (이미 생성됨) +- 데이터베이스 테이블 생성 제거 (이미 존재) +- Phase 7 추가: 전략 패턴 구현 (내부/외부 통신 분기) diff --git a/VMIS_INTERFACE_MIGRATION_PLAN.md b/VMIS_INTERFACE_MIGRATION_PLAN.md index d422fee..69034b9 100644 --- a/VMIS_INTERFACE_MIGRATION_PLAN.md +++ b/VMIS_INTERFACE_MIGRATION_PLAN.md @@ -2,7 +2,7 @@ **작성일**: 2025-11-06 **방법**: 직접 통합 (Spring Boot 2.7 다운그레이드) -**예상 소요 시간**: 10시간 +**예상 소요 시간**: 10시간 50분 --- @@ -12,6 +12,7 @@ 2. [패키지 구조 설계](#phase-2-패키지-구조-설계) 3. [코드 이식 순서](#phase-3-코드-이식-순서) 4. [단계별 상세 가이드](#phase-4-단계별-상세-가이드) + - [전략 패턴 구현 (내부/외부 통신 분기)](#step-13-전략-패턴-구현-내부외부-통신-분기) 5. [빌드 및 테스트](#phase-5-빌드-및-테스트) 6. [트러블슈팅](#phase-6-트러블슈팅) @@ -719,6 +720,426 @@ DESC tb_car_ledger_frmbk; DESC tb_car_ledger_frmbk_dtl; ``` +### Step 13: 전략 패턴 구현 (내부/외부 통신 분기) + +**목적**: application.yml 설정에 따라 내부 VMIS 모듈 또는 외부 REST API를 선택적으로 사용 + +**상세 설계 문서**: `VMIS_INTEGRATION_STRATEGY_DESIGN.md` 참조 + +#### 13.1 공통 인터페이스 생성 + +**파일**: `D:\workspace\git\VIPS\src\main\java\go\kr\project\common\service\VehicleInfoService.java` + +```java +package go.kr.project.common.service; + +import go.kr.project.common.vo.VehicleBasicInfoResponseVO; +import go.kr.project.common.vo.VehicleLedgerResponseVO; + +/** + * 차량 정보 조회 서비스 인터페이스 + * - Internal Mode: VMIS 모듈 직접 호출 + * - External Mode: REST API 호출 + */ +public interface VehicleInfoService { + + /** + * 차량 기본정보 조회 + * @param vhrno 차량번호 + * @return 차량 기본정보 응답 + */ + VehicleBasicInfoResponseVO getBasicInfo(String vhrno); + + /** + * 차량 등록원부 조회 + * @param vhrno 차량번호 + * @return 차량 등록원부 응답 + */ + VehicleLedgerResponseVO getLedgerInfo(String vhrno); +} +``` + +#### 13.2 Internal Mode 구현체 + +**파일**: `D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\service\InternalVehicleInfoServiceImpl.java` + +```java +package go.kr.project.vmis.service; + +import go.kr.project.common.service.VehicleInfoService; +import go.kr.project.common.vo.*; +import go.kr.project.vmis.model.vo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal") +public class InternalVehicleInfoServiceImpl implements VehicleInfoService { + + private final CarBassMatterInqireService carBassMatterInqireService; + private final CarLedgerFrmbkService carLedgerFrmbkService; + + @Override + public VehicleBasicInfoResponseVO getBasicInfo(String vhrno) { + log.info("[INTERNAL MODE] 차량 기본정보 조회 - 차량번호: {}", vhrno); + + try { + // 내부 VMIS 모듈 직접 호출 + CarBassMatterInqireRequestVO request = new CarBassMatterInqireRequestVO(); + request.setVhrno(vhrno); + + Envelope response = + carBassMatterInqireService.getBasicInfo(request); + + // DTO → VO 변환 + return convertToBasicInfoResponse(response, vhrno); + + } catch (Exception e) { + log.error("[INTERNAL MODE] 차량 기본정보 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorBasicResponse(vhrno, e.getMessage()); + } + } + + @Override + public VehicleLedgerResponseVO getLedgerInfo(String vhrno) { + log.info("[INTERNAL MODE] 차량 등록원부 조회 - 차량번호: {}", vhrno); + + try { + CarLedgerFrmbkRequestVO request = new CarLedgerFrmbkRequestVO(); + request.setVhrno(vhrno); + + Envelope response = + carLedgerFrmbkService.getLedgerInfo(request); + + return convertToLedgerResponse(response, vhrno); + + } catch (Exception e) { + log.error("[INTERNAL MODE] 차량 등록원부 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorLedgerResponse(vhrno, e.getMessage()); + } + } + + private VehicleBasicInfoResponseVO convertToBasicInfoResponse( + Envelope envelope, String vhrno) { + + VehicleBasicInfoResponseVO response = new VehicleBasicInfoResponseVO(); + response.setVhrno(vhrno); + + if (envelope.getData() != null && !envelope.getData().isEmpty()) { + VehicleBasicInfoDTO dto = envelope.getData().get(0); + response.setSuccess(true); + response.setMessage("조회 성공"); + response.setBasicInfo(dto); + } else { + response.setSuccess(false); + response.setMessage("조회 결과 없음"); + } + + return response; + } + + private VehicleLedgerResponseVO convertToLedgerResponse( + Envelope envelope, String vhrno) { + + VehicleLedgerResponseVO response = new VehicleLedgerResponseVO(); + response.setVhrno(vhrno); + + if (envelope.getData() != null && !envelope.getData().isEmpty()) { + VehicleLedgerDTO dto = envelope.getData().get(0); + response.setSuccess(true); + response.setMessage("조회 성공"); + response.setLedgerInfo(dto); + } else { + response.setSuccess(false); + response.setMessage("조회 결과 없음"); + } + + return response; + } + + private VehicleBasicInfoResponseVO createErrorBasicResponse(String vhrno, String message) { + VehicleBasicInfoResponseVO response = new VehicleBasicInfoResponseVO(); + response.setVhrno(vhrno); + response.setSuccess(false); + response.setMessage("조회 실패: " + message); + return response; + } + + private VehicleLedgerResponseVO createErrorLedgerResponse(String vhrno, String message) { + VehicleLedgerResponseVO response = new VehicleLedgerResponseVO(); + response.setVhrno(vhrno); + response.setSuccess(false); + response.setMessage("조회 실패: " + message); + return response; + } +} +``` + +#### 13.3 External Mode 구현체 + +**파일**: `D:\workspace\git\VIPS\src\main\java\go\kr\project\externalApi\service\ExternalVehicleInfoServiceImpl.java` + +```java +package go.kr.project.externalApi.service; + +import go.kr.project.common.service.VehicleInfoService; +import go.kr.project.common.vo.*; +import go.kr.project.externalApi.vo.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +@RequiredArgsConstructor +@ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true) +public class ExternalVehicleInfoServiceImpl implements VehicleInfoService { + + private final RestTemplate restTemplate; + + @Value("${vmis.external.api.url:http://localhost:8081/api/v1/vehicles}") + private String vmisApiUrl; + + @Override + public VehicleBasicInfoResponseVO getBasicInfo(String vhrno) { + log.info("[EXTERNAL MODE] 차량 기본정보 조회 - 차량번호: {}", vhrno); + + try { + String url = vmisApiUrl + "/basic"; + + VehicleBasicRequestVO request = new VehicleBasicRequestVO(); + request.setVhrno(vhrno); + + // 외부 REST API 호출 + Envelope envelope = restTemplate.postForObject( + url, + request, + Envelope.class + ); + + return convertExternalToBasicResponse(envelope, vhrno); + + } catch (Exception e) { + log.error("[EXTERNAL MODE] 차량 기본정보 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorBasicResponse(vhrno, e.getMessage()); + } + } + + @Override + public VehicleLedgerResponseVO getLedgerInfo(String vhrno) { + log.info("[EXTERNAL MODE] 차량 등록원부 조회 - 차량번호: {}", vhrno); + + try { + String url = vmisApiUrl + "/ledger"; + + VehicleLedgerRequestVO request = new VehicleLedgerRequestVO(); + request.setVhrno(vhrno); + + Envelope envelope = restTemplate.postForObject( + url, + request, + Envelope.class + ); + + return convertExternalToLedgerResponse(envelope, vhrno); + + } catch (Exception e) { + log.error("[EXTERNAL MODE] 차량 등록원부 조회 실패 - 차량번호: {}", vhrno, e); + return createErrorLedgerResponse(vhrno, e.getMessage()); + } + } + + private VehicleBasicInfoResponseVO convertExternalToBasicResponse( + Envelope envelope, String vhrno) { + // 외부 API 응답을 내부 VO로 변환 + // 구현... + } + + private VehicleLedgerResponseVO convertExternalToLedgerResponse( + Envelope envelope, String vhrno) { + // 외부 API 응답을 내부 VO로 변환 + // 구현... + } + + private VehicleBasicInfoResponseVO createErrorBasicResponse(String vhrno, String message) { + VehicleBasicInfoResponseVO response = new VehicleBasicInfoResponseVO(); + response.setVhrno(vhrno); + response.setSuccess(false); + response.setMessage("조회 실패: " + message); + return response; + } + + private VehicleLedgerResponseVO createErrorLedgerResponse(String vhrno, String message) { + VehicleLedgerResponseVO response = new VehicleLedgerResponseVO(); + response.setVhrno(vhrno); + response.setSuccess(false); + response.setMessage("조회 실패: " + message); + return response; + } +} +``` + +#### 13.4 설정 클래스 + +**파일**: `D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\config\VmisIntegrationConfig.java` + +```java +package go.kr.project.vmis.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Slf4j +@Configuration +@EnableConfigurationProperties(VmisProperties.class) +public class VmisIntegrationConfig { + + @Configuration + @ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "internal") + static class InternalModeConfig { + @PostConstruct + public void init() { + log.info("╔═══════════════════════════════════════════════╗"); + log.info("║ VMIS Integration Mode: INTERNAL ║"); + log.info("║ 차량 정보 조회: 내부 VMIS 모듈 직접 호출 ║"); + log.info("╚═══════════════════════════════════════════════╝"); + } + } + + @Configuration + @ConditionalOnProperty(name = "vmis.integration.mode", havingValue = "external", matchIfMissing = true) + static class ExternalModeConfig { + @PostConstruct + public void init() { + log.info("╔═══════════════════════════════════════════════╗"); + log.info("║ VMIS Integration Mode: EXTERNAL ║"); + log.info("║ 차량 정보 조회: 외부 REST API 호출 ║"); + log.info("╚═══════════════════════════════════════════════╝"); + } + } +} +``` + +#### 13.5 application.yml 설정 추가 + +**파일**: `D:\workspace\git\VIPS\src\main\resources\application.yml` + +```yaml +# VMIS 통합 모드 설정 추가 +vmis: + integration: + mode: internal # internal | external (기본값: external) + + # Internal Mode 설정 + system: + info-sys-id: "VMIS001" + info-sys-ip: "${SERVER_IP:192.168.1.100}" + manager-id: "admin" + manager-name: "관리자" + manager-tel: "02-1234-5678" + + gpki: + enabled: false + cert-path: "${GPKI_CERT_PATH:/path/to/cert.der}" + private-key-path: "${GPKI_PRIVATE_KEY_PATH:/path/to/private.key}" + private-key-password: "${GPKI_PASSWORD:}" + + government: + host: "https://www.vemanet.com" + base-path: "/openapi" + connect-timeout: 10000 + read-timeout: 15000 + services: + basic: + path: "/carBassMatterInqire" + api-key: "${GOV_API_KEY_BASIC:}" + ledger: + path: "/carLedgerFrmbk" + api-key: "${GOV_API_KEY_LEDGER:}" + + # External Mode 설정 + external: + api: + url: "http://localhost:8081/api/v1/vehicles" + connect-timeout: 5000 + read-timeout: 10000 +``` + +#### 13.6 기존 코드 수정 (클라이언트) + +**Before** (기존 방식): +```java +@Service +@RequiredArgsConstructor +public class CarInspectionService { + + private final ExternalVehicleApiService externalVehicleApiService; + + public void processInspection(String vhrno) { + VehicleApiResponseVO info = externalVehicleApiService.getBasicInfo(vhrno); + // ... + } +} +``` + +**After** (인터페이스 의존): +```java +@Service +@RequiredArgsConstructor +public class CarInspectionService { + + private final VehicleInfoService vehicleInfoService; // 인터페이스 주입 + + public void processInspection(String vhrno) { + // 설정에 따라 자동으로 Internal 또는 External 구현체 주입 + VehicleBasicInfoResponseVO info = vehicleInfoService.getBasicInfo(vhrno); + // ... + } +} +``` + +#### 13.7 테스트 + +**Internal Mode 테스트**: +```yaml +# application-local.yml +vmis: + integration: + mode: internal +``` + +**External Mode 테스트**: +```yaml +# application-local.yml +vmis: + integration: + mode: external + external: + api: + url: "http://localhost:8081/api/v1/vehicles" +``` + +**구동 시 로그 확인**: +``` +╔═══════════════════════════════════════════════╗ +║ VMIS Integration Mode: INTERNAL ║ +║ 차량 정보 조회: 내부 VMIS 모듈 직접 호출 ║ +╚═══════════════════════════════════════════════╝ + +[INTERNAL MODE] 차량 기본정보 조회 - 차량번호: 12가3456 +``` + --- ## Phase 5: 빌드 및 테스트 @@ -1106,6 +1527,13 @@ gh pr create --title "VMIS-interface 통합" \ - [ ] GovernmentApiClient 이식 (HttpClient 4 변경) - [ ] Service 클래스 이식 (4개) - [ ] Controller 이식 +- [ ] **전략 패턴 구현** (Step 13) + - [ ] VehicleInfoService 인터페이스 생성 + - [ ] InternalVehicleInfoServiceImpl 구현 (내부 모드) + - [ ] ExternalVehicleInfoServiceImpl 구현 (외부 모드) + - [ ] VmisIntegrationConfig 설정 클래스 생성 + - [ ] application.yml에 vmis.integration.mode 설정 추가 + - [ ] 기존 클라이언트 코드 수정 (인터페이스 의존) ### Phase 5: 데이터베이스 - [ ] 테이블 존재 확인 (6개 테이블/시퀀스 - 이미 존재) @@ -1146,5 +1574,10 @@ cd D:\workspace\git\VIPS git status # 현재 브랜치 확인 ``` -**문서 버전**: 1.1 +**문서 버전**: 1.2 **최종 수정**: 2025-11-06 +**주요 변경사항**: +- 브랜치 생성 관련 내용 제거 (이미 생성됨) +- 데이터베이스 테이블 생성 제거 (이미 존재) +- Step 13 추가: 전략 패턴 구현 (내부/외부 통신 분기) +- 상세 설계 문서: VMIS_INTEGRATION_STRATEGY_DESIGN.md 참조