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_INTERFACE_MIGRATION_PL...

45 KiB

VMIS-interface → VIPS 통합 마이그레이션 실행 계획

작성일: 2025-11-06 방법: 직접 통합 (Spring Boot 2.7 다운그레이드) 예상 소요 시간: 10시간 50분


목차

  1. 사전 준비
  2. 패키지 구조 설계
  3. 코드 이식 순서
  4. 단계별 상세 가이드
  5. 빌드 및 테스트
  6. 트러블슈팅

Phase 1: 사전 준비

참고: 브랜치는 이미 생성되어 진행 중입니다.

1.1 현재 상태 확인

cd D:\workspace\git\VIPS

# 현재 브랜치 및 상태 확인
git status
git branch

1.2 디렉토리 구조 준비

# VIPS 프로젝트에 새 패키지 생성 (Windows PowerShell)
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\config"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\controller"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\service"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\client"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\model\entity"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\model\vo"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\model\dto"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\mapper"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\util"
New-Item -ItemType Directory -Force -Path "src\main\java\go\kr\project\vmis\enricher"

# MyBatis 매퍼 디렉토리
New-Item -ItemType Directory -Force -Path "src\main\resources\mybatis\mapper\vmis"

# GPKI 라이브러리 디렉토리 (이미 존재하면 스킵)
New-Item -ItemType Directory -Force -Path "lib"

1.3 GPKI 라이브러리 복사

# Windows CMD
copy "D:\workspace\git\VMIS-interface\lib\libgpkiapi_jni_1.5.jar" "D:\workspace\git\VIPS\lib\"

Phase 2: 패키지 구조 설계

2.1 최종 패키지 구조

go.kr.project.vmis/
├── config/                           (설정)
│   ├── VmisProperties.java           (설정 속성)
│   ├── MyBatisConfig.java            (MyBatis 설정)
│   └── OpenApiConfig.java            (Open API 설정)
│
├── controller/                       (컨트롤러)
│   └── VehicleInterfaceController.java
│
├── service/                          (서비스)
│   ├── CarBassMatterInqireService.java
│   ├── CarLedgerFrmbkService.java
│   ├── CarBassMatterInqireLogService.java
│   └── CarLedgerFrmbkLogService.java
│
├── client/                           (외부 API 클라이언트)
│   └── GovernmentApiClient.java
│
├── enricher/                         (데이터 보강)
│   └── RequestEnricher.java
│
├── model/                            (모델)
│   ├── entity/                       (DB 엔티티)
│   │   ├── CarBassMatterInqire.java
│   │   ├── CarBassMatterInqireLog.java
│   │   ├── CarLedgerFrmbk.java
│   │   ├── CarLedgerFrmbkDtl.java
│   │   ├── CarLedgerFrmbkLog.java
│   │   ├── VehicleBasicInfo.java
│   │   └── VehicleLedger.java
│   ├── vo/                           (VO)
│   │   ├── CarBassMatterInqireRequestVO.java
│   │   ├── CarLedgerFrmbkRequestVO.java
│   │   ├── Envelope.java
│   │   ├── GovApiBasicRequest.java
│   │   ├── GovApiBasicResponse.java
│   │   ├── GovApiLedgerRequest.java
│   │   └── GovApiLedgerResponse.java
│   └── dto/                          (DTO)
│       ├── VehicleBasicInfoDTO.java
│       └── VehicleLedgerDTO.java
│
├── mapper/                           (MyBatis Mapper)
│   ├── CarBassMatterInqireLogMapper.java
│   └── CarLedgerFrmbkLogMapper.java
│
└── util/                             (유틸리티)
    ├── EncryptionUtil.java
    └── TransactionIdGenerator.java

Phase 3: 코드 이식 순서

3.1 이식 우선순위 (의존성 순서)

1. util (의존성 없음)
   └── TransactionIdGenerator.java
   └── EncryptionUtil.java

2. model (util에만 의존)
   └── entity/ (7개)
   └── vo/ (7개)
   └── dto/ (2개)

3. config (model에 의존)
   └── VmisProperties.java (가장 먼저)
   └── MyBatisConfig.java
   └── OpenApiConfig.java

4. mapper (model에 의존)
   └── CarBassMatterInqireLogMapper.java
   └── CarLedgerFrmbkLogMapper.java

5. enricher (config, util에 의존)
   └── RequestEnricher.java

6. client (config, model, util, enricher에 의존)
   └── GovernmentApiClient.java

7. service (모든 계층에 의존)
   └── CarBassMatterInqireLogService.java
   └── CarLedgerFrmbkLogService.java
   └── CarBassMatterInqireService.java
   └── CarLedgerFrmbkService.java

8. controller (service에 의존)
   └── VehicleInterfaceController.java

Phase 4: 단계별 상세 가이드

Step 1: build.gradle 의존성 추가

파일: D:\workspace\git\VIPS\build.gradle

dependencies {
    // 기존 의존성...

    // VMIS-interface 관련 추가 의존성
    implementation 'org.apache.httpcomponents:httpclient:4.5.14'  // Apache HttpClient 4
    implementation 'com.fasterxml.jackson.core:jackson-databind'  // JSON 처리 (이미 있을 가능성 높음)

    // GPKI 라이브러리
    implementation files('lib/libgpkiapi_jni_1.5.jar')
}

주의:

  • Spring Boot 2.7.18에는 기본적으로 HttpClient 4가 포함되어 있으므로 충돌 확인 필요
  • Jackson은 Spring Boot Starter에 포함되어 있음

Step 2: VmisProperties 이식 및 수정

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\config\VmisProperties.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\config\VmisProperties.java

변경 사항:

// Before
package com.vmis.interfaceapp.config;

// After
package go.kr.project.vmis.config;

전체 코드 예시:

package go.kr.project.vmis.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "vmis")
public class VmisProperties {

    private SystemProps system = new SystemProps();
    private GpkiProps gpki = new GpkiProps();
    private GovProps government = new GovProps();

    @Getter
    @Setter
    public static class SystemProps {
        private String infoSysId;
        private String infoSysIp;
        private String managerId;
        private String managerName;
        private String managerTel;
    }

    @Getter
    @Setter
    public static class GpkiProps {
        private boolean enabled;
        private String certPath;
        private String privateKeyPath;
        private String privateKeyPassword;
    }

    @Getter
    @Setter
    public static class GovProps {
        private String host;
        private String basePath;
        private int connectTimeout;
        private int readTimeout;
        private Services services = new Services();

        @Getter
        @Setter
        public static class Services {
            private ServiceConfig basic = new ServiceConfig();
            private ServiceConfig ledger = new ServiceConfig();

            @Getter
            @Setter
            public static class ServiceConfig {
                private String path;
                private String apiKey;
            }
        }
    }
}

Step 3: application.yml 설정 통합

파일: D:\workspace\git\VIPS\src\main\resources\application.yml

추가할 내용:

# VMIS 정부 API 연동 설정
vmis:
  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  # 개발환경에서는 false, 운영환경에서는 true
    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:your-api-key-basic}"
      ledger:
        path: "/carLedgerFrmbk"
        api-key: "${GOV_API_KEY_LEDGER:your-api-key-ledger}"

# MyBatis 설정 업데이트 (기존 설정에 추가)
mybatis:
  mapper-locations:
    - classpath:mybatis/mapper/**/*_${Globals.DbType}.xml
    - classpath:mybatis/mapper/vmis/**/*_maria.xml  # VMIS 매퍼 추가

환경별 설정 (application-local.yml, application-dev.yml, application-prd.yml):

# application-prd.yml (운영환경)
vmis:
  gpki:
    enabled: true  # 운영환경에서는 암호화 활성화

Step 4: 유틸리티 클래스 이식

4.1 TransactionIdGenerator.java

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\util\TransactionIdGenerator.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\util\TransactionIdGenerator.java

변경 사항:

// Before
package com.vmis.interfaceapp.util;

// After
package go.kr.project.vmis.util;

// import 문 변경 없음 (Java 표준 라이브러리만 사용)

4.2 EncryptionUtil.java

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\util\EncryptionUtil.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\util\EncryptionUtil.java

주요 변경 사항:

// Before
package com.vmis.interfaceapp.util;

import com.vmis.interfaceapp.config.VmisProperties;

// After
package go.kr.project.vmis.util;

import go.kr.project.vmis.config.VmisProperties;

Step 5: 모델 클래스 이식

5.1 Entity 클래스 (7개)

패키지 변경:

// Before
package com.vmis.interfaceapp.model.entity;

// After
package go.kr.project.vmis.model.entity;

파일 목록:

  1. CarBassMatterInqire.java
  2. CarBassMatterInqireLog.java
  3. CarLedgerFrmbk.java
  4. CarLedgerFrmbkDtl.java
  5. CarLedgerFrmbkLog.java
  6. VehicleBasicInfo.java
  7. VehicleLedger.java

주의사항:

  • Lombok 어노테이션 유지 (@Getter, @Setter, @Builder 등)
  • java.time.* 클래스 사용 가능 (Java 8+)
  • jakarta.validation.*javax.validation.* 변경

5.2 VO 클래스 (7개)

변경 예시:

// Before
package com.vmis.interfaceapp.model.vo;

import jakarta.validation.constraints.NotBlank;

// After
package go.kr.project.vmis.model.vo;

import javax.validation.constraints.NotBlank;

파일 목록:

  1. CarBassMatterInqireRequestVO.java
  2. CarLedgerFrmbkRequestVO.java
  3. Envelope.java
  4. GovApiBasicRequest.java
  5. GovApiBasicResponse.java
  6. GovApiLedgerRequest.java
  7. GovApiLedgerResponse.java

5.3 DTO 클래스 (2개)

패키지 변경:

// Before
package com.vmis.interfaceapp.model.dto;

// After
package go.kr.project.vmis.model.dto;

파일 목록:

  1. VehicleBasicInfoDTO.java
  2. VehicleLedgerDTO.java

Step 6: Config 클래스 이식

6.1 MyBatisConfig.java

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\config\MyBatisConfig.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\config\VmisMyBatisConfig.java (이름 변경)

변경 사항:

// Before
package com.vmis.interfaceapp.config;

import org.mybatis.spring.annotation.MapperScan;

@Configuration
@MapperScan("com.vmis.interfaceapp.mapper")
public class MyBatisConfig {
    // ...
}

// After
package go.kr.project.vmis.config;

import org.mybatis.spring.annotation.MapperScan;

@Configuration
@MapperScan("go.kr.project.vmis.mapper")
public class VmisMyBatisConfig {
    // ...
}

주의: VIPS에 이미 MyBatis 설정이 있으므로 클래스명을 변경하여 충돌 방지

6.2 OpenApiConfig.java

변경 사항:

// Before
package com.vmis.interfaceapp.config;

// After
package go.kr.project.vmis.config;

// import 패키지 변경
import go.kr.project.vmis.model.vo.*;

Step 7: Mapper 이식

7.1 Mapper 인터페이스

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\mapper\ 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\mapper\

변경 사항:

// Before
package com.vmis.interfaceapp.mapper;

import com.vmis.interfaceapp.model.entity.CarBassMatterInqireLog;

// After
package go.kr.project.vmis.mapper;

import go.kr.project.vmis.model.entity.CarBassMatterInqireLog;

파일 목록:

  1. CarBassMatterInqireLogMapper.java
  2. CarLedgerFrmbkLogMapper.java

7.2 Mapper XML

소스: D:\workspace\git\VMIS-interface\src\main\resources\mybatis\mapper\ 대상: D:\workspace\git\VIPS\src\main\resources\mybatis\mapper\vmis\

CarBassMatterInqireLogMapper_maria.xml 변경 예시:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- Before -->
<mapper namespace="com.vmis.interfaceapp.mapper.CarBassMatterInqireLogMapper">
    <resultMap id="CarBassMatterInqireLogResultMap"
               type="com.vmis.interfaceapp.model.entity.CarBassMatterInqireLog">

<!-- After -->
<mapper namespace="go.kr.project.vmis.mapper.CarBassMatterInqireLogMapper">
    <resultMap id="CarBassMatterInqireLogResultMap"
               type="go.kr.project.vmis.model.entity.CarBassMatterInqireLog">

파일 목록:

  1. CarBassMatterInqireLogMapper_maria.xml
  2. CarLedgerFrmbkLogMapper_maria.xml

Step 8: Enricher 이식

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\enricher\RequestEnricher.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\enricher\RequestEnricher.java

변경 사항:

// Before
package com.vmis.interfaceapp.enricher;

import com.vmis.interfaceapp.config.VmisProperties;
import com.vmis.interfaceapp.util.TransactionIdGenerator;

// After
package go.kr.project.vmis.enricher;

import go.kr.project.vmis.config.VmisProperties;
import go.kr.project.vmis.util.TransactionIdGenerator;

Step 9: GovernmentApiClient 이식 (가장 복잡)

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\client\GovernmentApiClient.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\client\GovernmentApiClient.java

HttpClient 5 → 4 변경:

// Before (HttpClient 5)
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.ContentType;

// After (HttpClient 4)
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.ContentType;

주요 API 변경:

// Before (HttpClient 5)
StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);

try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
    String responseBody = EntityUtils.toString(response.getEntity());
}

// After (HttpClient 4)
StringEntity entity = new StringEntity(jsonBody, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);

CloseableHttpResponse response = null;
try {
    response = httpClient.execute(httpPost);
    String responseBody = EntityUtils.toString(response.getEntity());
} finally {
    if (response != null) {
        response.close();
    }
}

전체 변경 템플릿:

package go.kr.project.vmis.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import go.kr.project.vmis.config.VmisProperties;
import go.kr.project.vmis.enricher.RequestEnricher;
import go.kr.project.vmis.model.vo.*;
import go.kr.project.vmis.util.EncryptionUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

@Slf4j
@Component
@RequiredArgsConstructor
public class GovernmentApiClient {

    private final VmisProperties vmisProperties;
    private final RequestEnricher requestEnricher;
    private final EncryptionUtil encryptionUtil;
    private final ObjectMapper objectMapper;

    public GovApiBasicResponse callBasicInfoApi(String vhrno) {
        // 기존 로직 유지, HttpClient 4 API로 변경
        // ...
    }

    public GovApiLedgerResponse callLedgerInfoApi(String vhrno) {
        // 기존 로직 유지, HttpClient 4 API로 변경
        // ...
    }

    private CloseableHttpClient createHttpClient() {
        return HttpClientBuilder.create()
            .setConnectionTimeToLive(vmisProperties.getGovernment().getConnectTimeout(),
                                     java.util.concurrent.TimeUnit.MILLISECONDS)
            .build();
    }
}

Step 10: Service 계층 이식

패키지 변경:

// Before
package com.vmis.interfaceapp.service;

import com.vmis.interfaceapp.client.GovernmentApiClient;
import com.vmis.interfaceapp.mapper.*;
import com.vmis.interfaceapp.model.*;

// After
package go.kr.project.vmis.service;

import go.kr.project.vmis.client.GovernmentApiClient;
import go.kr.project.vmis.mapper.*;
import go.kr.project.vmis.model.*;

파일 목록:

  1. CarBassMatterInqireLogService.java
  2. CarLedgerFrmbkLogService.java
  3. CarBassMatterInqireService.java
  4. CarLedgerFrmbkService.java

주의사항:

  • @Service 어노테이션 유지
  • @Transactional 설정 확인

Step 11: Controller 이식

소스: D:\workspace\git\VMIS-interface\src\main\java\com\vmis\interfaceapp\controller\VehicleInterfaceController.java 대상: D:\workspace\git\VIPS\src\main\java\go\kr\project\vmis\controller\VehicleInterfaceController.java

주요 변경 사항:

// Before
package com.vmis.interfaceapp.controller;

import com.vmis.interfaceapp.service.*;
import com.vmis.interfaceapp.model.vo.*;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/v1/vehicles")

// After
package go.kr.project.vmis.controller;

import go.kr.project.vmis.service.*;
import go.kr.project.vmis.model.vo.*;
import javax.validation.Valid;

@RestController
@RequestMapping("/vmis/api/v1/vehicles")  // 경로 변경 고려

API 경로 충돌 방지:

  • 기존 VIPS: /api/v1/vehicles (ExternalVehicleApiService가 사용)
  • 새로운 VMIS: /vmis/api/v1/vehicles 또는 /internal/vehicles

Step 12: 데이터베이스 확인

데이터베이스 테이블은 이미 존재합니다 (VIPS와 VMIS-interface가 동일한 DB 사용)

테이블 존재 확인:

-- 1. 테이블 존재 확인
SHOW TABLES LIKE 'tb_car_bass_matter_inqire%';
SHOW TABLES LIKE 'tb_car_ledger_frmbk%';

-- 2. 시퀀스 존재 확인
SHOW TABLES LIKE 'seq_car_%';

-- 예상 결과: 6개 항목
-- tb_car_bass_matter_inqire_log
-- seq_car_bass_matter_inqire
-- tb_car_ledger_frmbk
-- seq_car_ledger_frmbk
-- tb_car_ledger_frmbk_dtl
-- seq_car_ledger_frmbk_dtl

테이블 구조 확인 (선택사항):

DESC tb_car_bass_matter_inqire_log;
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

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

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

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

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

# 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 (기존 방식):

@Service
@RequiredArgsConstructor
public class CarInspectionService {

    private final ExternalVehicleApiService externalVehicleApiService;

    public void processInspection(String vhrno) {
        VehicleApiResponseVO info = externalVehicleApiService.getBasicInfo(vhrno);
        // ...
    }
}

After (인터페이스 의존):

@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 테스트:

# application-local.yml
vmis:
  integration:
    mode: internal

External Mode 테스트:

# 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: 빌드 및 테스트

5.1 Gradle 빌드

cd D:\workspace\git\VIPS

# Clean 빌드
gradlew clean build

# 빌드 성공 확인
# BUILD SUCCESSFUL 메시지 확인

5.2 애플리케이션 구동

# Spring Boot 실행
gradlew bootRun

# 또는
java -jar build/libs/VIPS-BOOT.war

5.3 로그 확인

로그 파일 위치 확인 (logback-spring.xml 참조)
- 구동 시 에러 로그 확인
- Bean 생성 확인 (VmisProperties, GovernmentApiClient 등)

5.4 API 테스트

Swagger UI 접속

http://localhost:8080/swagger-ui/index.html

직접 API 호출 (PowerShell)

# 기본사항 조회 테스트
Invoke-RestMethod -Uri "http://localhost:8080/vmis/api/v1/vehicles/basic" `
    -Method POST `
    -ContentType "application/json" `
    -Body '{"vhrno":"12가3456"}' | ConvertTo-Json -Depth 10

# 등록원부 조회 테스트
Invoke-RestMethod -Uri "http://localhost:8080/vmis/api/v1/vehicles/ledger" `
    -Method POST `
    -ContentType "application/json" `
    -Body '{"vhrno":"12가3456"}' | ConvertTo-Json -Depth 10

cURL (Git Bash)

# 기본사항 조회
curl -X POST http://localhost:8080/vmis/api/v1/vehicles/basic \
  -H "Content-Type: application/json" \
  -d '{"vhrno":"12가3456"}'

# 등록원부 조회
curl -X POST http://localhost:8080/vmis/api/v1/vehicles/ledger \
  -H "Content-Type: application/json" \
  -d '{"vhrno":"12가3456"}'

5.5 데이터베이스 확인

-- 로그 테이블 확인
SELECT * FROM tb_car_bass_matter_inqire_log ORDER BY reg_dt DESC LIMIT 10;
SELECT * FROM tb_car_ledger_frmbk_log ORDER BY reg_dt DESC LIMIT 10;

-- 데이터 저장 확인
SELECT * FROM tb_car_ledger_frmbk ORDER BY reg_dt DESC LIMIT 10;
SELECT * FROM tb_car_ledger_frmbk_dtl ORDER BY reg_dt DESC LIMIT 10;

Phase 6: 트러블슈팅

6.1 빌드 오류

오류 1: Package jakarta.* does not exist

원인: jakarta 패키지 미변경
해결: 전체 프로젝트에서 jakarta → javax 일괄 변경

# IntelliJ IDEA
Ctrl+Shift+R → "jakarta." → "javax."

오류 2: Cannot resolve symbol 'HttpClients'

원인: HttpClient 5 import 문 사용
해결: org.apache.hc.client5.* → org.apache.http.* 변경

오류 3: Bean creation error - VmisProperties

원인: @ConfigurationProperties 스캔 누락
해결: @Component 어노테이션 추가 또는
      @EnableConfigurationProperties(VmisProperties.class) 추가

6.2 런타임 오류

오류 1: Mapper not found

원인: MyBatis mapper-locations 설정 누락
해결: application.yml에 vmis 매퍼 경로 추가
mybatis:
  mapper-locations:
    - classpath:mybatis/mapper/**/*_maria.xml
    - classpath:mybatis/mapper/vmis/**/*_maria.xml

오류 2: GPKI 라이브러리 로드 실패

원인: libgpkiapi_jni_1.5.jar 경로 문제
해결:
1. lib 디렉토리 확인
2. build.gradle에 files('lib/...') 추가 확인
3. 개발환경에서는 vmis.gpki.enabled=false 설정

오류 3: API 호출 타임아웃

원인: 정부 API 호스트 연결 불가
해결:
1. 네트워크 연결 확인
2. 방화벽 설정 확인
3. API 키 유효성 확인
4. vmis.government.connect-timeout 값 증가

6.3 데이터베이스 오류

오류 1: Table doesn't exist

원인: DDL 미실행
해결: ddl/vips/*.sql 파일 실행

오류 2: Sequence not found

원인: 시퀀스 미생성
해결: seq_*.sql 파일 먼저 실행

6.4 API 응답 오류

오류 1: 401 Unauthorized

원인: API 키 오류
해결: application.yml의 GOV_API_KEY_* 환경변수 확인

오류 2: 500 Internal Server Error

원인: 암호화 실패
해결:
1. 개발환경에서는 vmis.gpki.enabled=false 설정
2. 운영환경에서는 인증서 경로 확인

Phase 7: 기존 코드 리팩토링

7.1 ExternalVehicleApiService 수정

파일: D:\workspace\git\VIPS\src\main\java\go\kr\project\externalApi\service\ExternalVehicleApiService.java

변경 전 (REST API 호출):

@Service
public class ExternalVehicleApiService {

    private final RestTemplate restTemplate;
    private final String VMIS_API_URL = "http://localhost:8081/api/v1/vehicles";

    public VehicleApiResponseVO getBasicInfo(String vhrno) {
        String url = VMIS_API_URL + "/basic";
        // RestTemplate으로 HTTP 호출...
    }
}

변경 후 (내부 메서드 호출):

@Service
@RequiredArgsConstructor
public class ExternalVehicleApiService {

    private final CarBassMatterInqireService carBassMatterInqireService;
    private final CarLedgerFrmbkService carLedgerFrmbkService;

    public VehicleApiResponseVO getBasicInfo(String vhrno) {
        // 내부 서비스 직접 호출
        CarBassMatterInqireRequestVO request = new CarBassMatterInqireRequestVO();
        request.setVhrno(vhrno);

        Envelope<VehicleBasicInfoDTO> response =
            carBassMatterInqireService.getBasicInfo(request);

        // DTO → VO 변환 로직...
        return convertToApiResponse(response);
    }

    public VehicleApiResponseVO getLedgerInfo(String vhrno) {
        CarLedgerFrmbkRequestVO request = new CarLedgerFrmbkRequestVO();
        request.setVhrno(vhrno);

        Envelope<VehicleLedgerDTO> response =
            carLedgerFrmbkService.getLedgerInfo(request);

        return convertToApiResponse(response);
    }

    private VehicleApiResponseVO convertToApiResponse(Envelope<?> envelope) {
        // 변환 로직...
    }
}

7.2 RestTemplate 설정 정리

옵션 1: RestTemplate 설정 유지 (향후 다른 외부 API 사용 가능) 옵션 2: 미사용 시 주석 처리

// RestTemplateConfig.java
// @Configuration  // 주석 처리
public class RestTemplateConfig {
    // ...
}

Phase 8: 문서화 및 배포

8.1 API 문서 업데이트

Swagger 어노테이션 추가 (VehicleInterfaceController.java):

@RestController
@RequestMapping("/vmis/api/v1/vehicles")
@Tag(name = "VMIS 차량 정보 조회", description = "도로교통공단 API 연동")
public class VehicleInterfaceController {

    @PostMapping("/basic")
    @Operation(summary = "자동차 기본사항 조회",
               description = "차량번호로 기본사항 조회")
    public ResponseEntity<Envelope<VehicleBasicInfoDTO>> getBasicInfo(
        @Valid @RequestBody CarBassMatterInqireRequestVO request) {
        // ...
    }
}

8.2 환경변수 설정 가이드

application-prd.yml 예시:

vmis:
  system:
    info-sys-id: "${VMIS_SYSTEM_ID}"
    info-sys-ip: "${SERVER_IP}"
    manager-id: "${VMIS_MANAGER_ID}"
    manager-name: "${VMIS_MANAGER_NAME}"
    manager-tel: "${VMIS_MANAGER_TEL}"

  gpki:
    enabled: true
    cert-path: "${GPKI_CERT_PATH}"
    private-key-path: "${GPKI_PRIVATE_KEY_PATH}"
    private-key-password: "${GPKI_PASSWORD}"

  government:
    services:
      basic:
        api-key: "${GOV_API_KEY_BASIC}"
      ledger:
        api-key: "${GOV_API_KEY_LEDGER}"

환경변수 설정 스크립트 (setenv.bat):

@echo off
REM VMIS 환경변수 설정

SET VMIS_SYSTEM_ID=VMIS001
SET SERVER_IP=192.168.1.100
SET VMIS_MANAGER_ID=admin
SET VMIS_MANAGER_NAME=관리자
SET VMIS_MANAGER_TEL=02-1234-5678

SET GPKI_CERT_PATH=C:\gpki\cert.der
SET GPKI_PRIVATE_KEY_PATH=C:\gpki\private.key
SET GPKI_PASSWORD=your-password

SET GOV_API_KEY_BASIC=your-api-key-basic
SET GOV_API_KEY_LEDGER=your-api-key-ledger

echo VMIS 환경변수 설정 완료

8.3 Git Commit

# 모든 변경사항 스테이징
git add .

# 커밋
git commit -m "VMIS-interface 통합: 도로교통공단 API 연동 모듈 내재화

- VMIS-interface 전체 코드 이식 (34개 파일)
- Spring Boot 3 → 2.7 다운그레이드
- jakarta → javax 패키지 변경
- HttpClient 5 → 4 변경
- 패키지 구조: go.kr.project.vmis.*
- 데이터베이스 테이블 추가 (6개)
- ExternalVehicleApiService 내부 호출로 리팩토링

Co-Authored-By: Claude <noreply@anthropic.com>"

# 푸시
git push origin feature/vmis-integration

8.4 Pull Request 생성

# GitHub CLI 사용
gh pr create --title "VMIS-interface 통합" \
  --body "## 개요
도로교통공단 API 연동 모듈(VMIS-interface)을 VIPS 내부로 통합

## 주요 변경사항
- Spring Boot 2.7 호환 코드로 변경
- 34개 Java 파일 이식
- 6개 데이터베이스 테이블 추가
- 내부 메서드 호출로 성능 개선

## 테스트
- [x] 빌드 성공
- [x] API 호출 테스트
- [x] 데이터베이스 저장 확인

## 체크리스트
- [x] 코드 리뷰 완료
- [x] 테스트 통과
- [x] 문서 업데이트
"

체크리스트

Phase 1: 사전 준비

  • 현재 브랜치 상태 확인 (이미 생성됨)
  • 디렉토리 구조 생성
  • GPKI 라이브러리 복사

Phase 2: 설정

  • build.gradle 의존성 추가
  • application.yml 설정 통합
  • VmisProperties 이식

Phase 3: 유틸리티 및 모델

  • TransactionIdGenerator 이식
  • EncryptionUtil 이식
  • Entity 클래스 이식 (7개)
  • VO 클래스 이식 (7개)
  • DTO 클래스 이식 (2개)

Phase 4: 핵심 로직

  • Config 클래스 이식 (2개)
  • Mapper 인터페이스 이식 (2개)
  • Mapper XML 이식 (2개)
  • RequestEnricher 이식
  • GovernmentApiClient 이식 (HttpClient 4 변경)
  • Service 클래스 이식 (4개)
  • Controller 이식
  • 전략 패턴 구현 (Step 13)
    • VehicleInfoService 인터페이스 생성
    • InternalVehicleInfoServiceImpl 구현 (내부 모드)
    • ExternalVehicleInfoServiceImpl 구현 (외부 모드)
    • VmisIntegrationConfig 설정 클래스 생성
    • application.yml에 vmis.integration.mode 설정 추가
    • 기존 클라이언트 코드 수정 (인터페이스 의존)

Phase 5: 데이터베이스

  • 테이블 존재 확인 (6개 테이블/시퀀스 - 이미 존재)

Phase 6: 빌드 및 테스트

  • Gradle 빌드 성공
  • 애플리케이션 구동 확인
  • Swagger UI 확인
  • API 호출 테스트 (/basic)
  • API 호출 테스트 (/ledger)
  • 데이터베이스 저장 확인

Phase 7: 리팩토링

  • ExternalVehicleApiService 수정
  • 불필요한 코드 제거

Phase 8: 마무리

  • API 문서 업데이트
  • 환경변수 가이드 작성
  • Git 커밋
  • Pull Request 생성

참고 자료


작업 진행 방법: 브랜치는 이미 생성되어 있으므로, Phase 1의 디렉토리 구조 준비부터 시작하세요.

cd D:\workspace\git\VIPS
git status  # 현재 브랜치 확인

문서 버전: 1.2 최종 수정: 2025-11-06 주요 변경사항:

  • 브랜치 생성 관련 내용 제거 (이미 생성됨)
  • 데이터베이스 테이블 생성 제거 (이미 존재)
  • Step 13 추가: 전략 패턴 구현 (내부/외부 통신 분기)
  • 상세 설계 문서: VMIS_INTEGRATION_STRATEGY_DESIGN.md 참조