23 KiB
VMIS 통합 전략 설계 문서
작성일: 2025-11-06 목적: 설정 기반으로 내부/외부 차량 정보 조회 방식을 선택할 수 있는 아키텍처 설계
목차
1. 요구사항
1.1 핵심 요구사항
application.yml 설정에 따라 두 가지 방식 중 선택 가능:
-
Internal Mode (내부 모드)
- 신규 통합된 VMIS 모듈을 직접 호출
- 네트워크 오버헤드 없음
- 단일 트랜잭션 처리 가능
- 높은 성능
-
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 (전략 인터페이스)
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
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
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
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)
@Service
public class ExternalVehicleApiService {
private final RestTemplate restTemplate;
public VehicleApiResponseVO getBasicInfo(String vhrno) {
// RestTemplate으로 외부 API 호출
}
}
After (전략 패턴 적용)
// 기존 ExternalVehicleApiService는 ExternalVehicleInfoServiceImpl로 대체
// 클라이언트 코드는 VehicleInfoService 인터페이스에 의존
@Service
@RequiredArgsConstructor
public class CarInspectionService {
private final VehicleInfoService vehicleInfoService; // 인터페이스 주입
public void processInspection(String vhrno) {
// 설정에 따라 자동으로 내부/외부 구현체가 주입됨
VehicleBasicInfoResponseVO info = vehicleInfoService.getBasicInfo(vhrno);
// ...
}
}
3.3 구현 순서
-
VehicleInfoService 인터페이스 생성
- 위치:
go/kr/project/common/service/ - 메서드: getBasicInfo, getLedgerInfo
- 위치:
-
InternalVehicleInfoServiceImpl 구현
- 위치:
go/kr/project/vmis/service/ - VMIS 모듈 직접 호출
- 위치:
-
ExternalVehicleInfoServiceImpl 구현
- 위치:
go/kr/project/externalApi/service/ - 기존 ExternalVehicleApiService 로직 이전
- 위치:
-
VmisIntegrationConfig 생성
- 설정 로깅 및 검증
-
기존 클라이언트 코드 수정
- ExternalVehicleApiService → VehicleInfoService 의존성 변경
4. 설정 예시
4.1 application.yml (공통)
# 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 (개발 환경)
vmis:
integration:
mode: external # 개발 중에는 외부 API 사용
external:
api:
url: "http://localhost:8081/api/v1/vehicles"
4.3 application-dev.yml (개발 서버)
vmis:
integration:
mode: internal # 개발 서버에서는 내부 모듈 테스트
gpki:
enabled: false # 개발 서버는 암호화 비활성화
4.4 application-prd.yml (운영 환경)
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 환경변수 설정 예시
# 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
@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
@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 모드 테스트
# 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 모드 테스트
# 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 성능 비교 테스트
@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 장애
# VMIS-interface 서버 중지
# → External 모드는 에러 반환
# → 설정을 Internal 모드로 변경하여 복구
시나리오 2: Internal 모드에서 DB 장애
# DB 연결 차단
# → Internal 모드는 에러 반환
# → 설정을 External 모드로 변경하여 복구
6. 운영 시나리오
6.1 점진적 배포 (Blue-Green)
1단계: 준비
# 모든 서버: External 모드
vmis.integration.mode=external
2단계: 일부 서버 전환
# 서버 A, B: External 모드
vmis.integration.mode=external
# 서버 C, D: Internal 모드 (테스트)
vmis.integration.mode=internal
3단계: 모니터링 후 전체 전환
# 모든 서버: Internal 모드
vmis.integration.mode=internal
6.2 롤백 절차
# 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 메트릭:
@Component
public class VehicleInfoServiceMetrics {
private final Counter internalCalls;
private final Counter externalCalls;
private final Timer internalTimer;
private final Timer externalTimer;
// 메트릭 수집...
}
로그 분석:
# 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 설정 검증
애플리케이션 시작 시 체크:
@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 모드 사용 시 주의점
-
외부 서버 가용성 확인
- VMIS-interface 서버가 실행 중이어야 함
- Health check 엔드포인트 모니터링
-
네트워크 타임아웃 설정
- connect-timeout, read-timeout 적절히 설정
- Circuit Breaker 패턴 고려
-
API 버전 호환성
- External 서버와 클라이언트의 API 계약 일치 확인
7.3 Internal 모드 사용 시 주의점
-
트랜잭션 범위
- @Transactional 설정 주의
- DB 연결 풀 크기 조정
-
GPKI 설정
- 운영 환경에서는 gpki.enabled=true
- 인증서 경로 및 비밀번호 확인
-
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