VMIS-interface → VIPS 통합 작업 분석, 계획 문서

수정
internalApi
박성영 1 month ago
parent b160476ddc
commit 6c677f7f69

@ -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) │
└─────────────────┬───────────────────────┘
│ 의존
┌─────────────────────────────────────────┐
<<interface>> │
│ 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<VehicleBasicInfoDTO> 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<VehicleLedgerDTO> 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<VehicleBasicInfoDTO> envelope) {
// 구현...
}
private VehicleLedgerResponseVO convertToLedgerResponse(Envelope<VehicleLedgerDTO> 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

@ -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 추가: 전략 패턴 구현 (내부/외부 통신 분기)

@ -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<VehicleBasicInfoDTO> 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<VehicleLedgerDTO> 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<VehicleBasicInfoDTO> 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<VehicleLedgerDTO> 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<VehicleApiResponseVO> 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<VehicleApiResponseVO> 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<VehicleApiResponseVO> envelope, String vhrno) {
// 외부 API 응답을 내부 VO로 변환
// 구현...
}
private VehicleLedgerResponseVO convertExternalToLedgerResponse(
Envelope<VehicleApiResponseVO> 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 참조

Loading…
Cancel
Save