You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
VIPS/VMIS_INTEGRATION_STRATEGY_D...

23 KiB

VMIS 통합 전략 설계 문서

작성일: 2025-11-06 목적: 설정 기반으로 내부/외부 차량 정보 조회 방식을 선택할 수 있는 아키텍처 설계


목차

  1. 요구사항
  2. 아키텍처 설계
  3. 구현 가이드
  4. 설정 예시
  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 (전략 인터페이스)

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 구현 순서

  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 (공통)

# 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 모드 사용 시 주의점

  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