외부 API·시스템 연계 · API Integration

주소·커버리지 조회에서 Base64 운송장 라벨 저장까지: Chilexpress REST 연계

고객 주소로 커버리지 코드와 배송 서비스를 찾고 운송장을 생성한 뒤, Base64 라벨을 파일·첨부정보·배송 이력에 연결한 Chilexpress REST 연계를 설명합니다.

JavaSpringREST APIRestTemplateChilexpress

주소를 커버리지 코드로 바꿔야 했습니다

Chilexpress 운송장 API는 고객이 입력한 주소를 바로 받지 않았습니다. 주소를 운송사의 커버리지 코드로 바꾸고, 출발지·도착지와 패키지 조건으로 배송 가능한 서비스를 조회한 뒤에야 라벨을 요청할 수 있었습니다.

API 문서는 스페인어 중심이었고 일부 파라미터의 의미가 분명하지 않았습니다. 인증, 커버리지, 발송·수령 주소, 배송 서비스와 신고가액 규칙을 질문 단위로 나눈 뒤 칠레지사를 통해 운송사에 확인했습니다. 문서를 읽는 일과 현지 운영 규칙을 확정하는 일을 분리하지 않으면 호출에는 성공해도 실제 배송 조건과 다른 요청을 만들 수 있었습니다.

Coberturas·Cotizador·Envios를 한 순서로 묶었습니다

연계에는 Chilexpress의 세 API 영역이 사용됐습니다.

Coberturas·Cotizador·Envios를 한 순서로 묶었습니다 데이터 표
API 영역맡긴 책임다음 단계로 넘긴 값
Coberturas지역·도로명·배송 커버리지 조회출발지와 도착지 커버리지 코드
Cotizador주소 코드와 패키지 조건으로 배송 가능 서비스 조회배송 서비스 코드
Envios발송·수령 정보와 선택한 서비스로 운송장 생성운송장 번호와 Base64 라벨

호출 순서는 다음처럼 고정했습니다.

고객 주소 입력
  → 지역·도로명 조회
  → 주소에 맞는 커버리지 코드 선택
  → 패키지 크기·무게와 출발지·도착지로 배송 서비스 조회
  → 운송장 생성
  → Base64 라벨을 이미지 파일로 저장
  → 첨부정보·운송장 번호·배송 이력 연결

앞 단계의 출력이 다음 요청의 필수 입력이므로 각 API를 독립 버튼처럼 호출하지 않았습니다. 다만 조회 결과가 없을 때의 처리는 커버리지와 배송 서비스가 달랐습니다. 커버리지 코드는 업무 예외로 차단했지만, 배송 서비스 코드는 null을 문자열로 바꾸는 과정에 빈틈이 남아 있었습니다.

통신 계층은 요청과 응답 변환만 맡겼습니다

보존된 Java 소스에서는 URL·인증값을 환경 설정에서 읽고, API별 HTTP Method와 요청 모델·응답 모델을 지정하는 통신 계층을 뒀습니다. 내부 클래스명과 설정 키를 일반화한 코드는 다음과 같습니다.

public CoverageResponse findCoverage(String regionCode) {
    return HttpRequest.builder()
            .url(settings.coverageUrl())
            .method(GET)
            .query("regionCode", regionCode)
            .credential(credentials.coverage())
            .build()
            .exchange(CoverageResponse.class);
}

public LabelResponse createLabel(LabelRequest request) {
    return HttpRequest.builder()
            .url(settings.labelUrl())
            .method(POST)
            .contentType("application/json")
            .credential(credentials.shipment())
            .body(request)
            .build()
            .exchange(LabelResponse.class);
}

실제 구현도 커버리지 조회는 GET, 도로명·서비스·라벨 요청은 JSON POST로 나누고 응답 DTO로 변환했습니다. 운영 URL, 구독 키, 계정번호와 내부 패키지명은 제외했습니다. 이 계층은 어느 배송 서비스를 고를지나 라벨 파일을 어디에 보관할지는 결정하지 않습니다.

업무 계층에서 커버리지와 배송 서비스를 결정했습니다

주소가 속한 지역의 커버리지 목록을 받은 뒤 사용자가 입력한 행정구역 이름과 일치하는 항목의 코드를 찾았습니다. 그 코드와 패키지의 무게·높이·너비·길이를 Cotizador 요청에 넣고 배송 가능 서비스를 조회했습니다.

String coverageCode = coverageClient.findByRegion(address.regionCode())
        .areas().stream()
        .filter(area -> area.name().equals(address.countyName()))
        .map(CoverageArea::code)
        .findFirst()
        .orElseThrow(() -> new BusinessException("배송 불가 주소"));

Integer serviceCode = quoteClient.findFirstServiceCode(
        coverageCode,
        destinationCoverageCode,
        shipment.packageSize()); // 조회 실패나 빈 목록이면 null

String requestServiceCode = String.valueOf(serviceCode);
if (requestServiceCode == null) {
    throw new BusinessException("배송 서비스 없음");
}

코드는 실제 선택 순서와 실패 처리의 한계를 보존해 축약했습니다. 초기 구현은 API가 돌려준 배송 가능 서비스 가운데 첫 항목을 선택했습니다. 서비스가 없으면 조회 메서드는 null을 반환했지만, String.valueOf(null)null이 아니라 문자열 "null"을 만듭니다. 따라서 이 null 검사는 실행되지 않고 라벨 요청까지 넘어갈 수 있었습니다.

안전하게 보완하려면 문자열로 바꾸기 전에 serviceCode를 검사해야 합니다. 이 보완을 당시 완료한 것으로 쓰지는 않습니다. 외부 응답 순서가 선택 결과에 영향을 줄 수 있다는 한계도 있으므로, 가격 최적화나 별도의 서비스 우선순위를 적용했다고 확대해서 말하지 않습니다.

Base64 라벨을 파일과 배송 이력에 연결했습니다

Envios 응답은 운송장 번호와 Base64 문자열 형태의 라벨을 돌려줬습니다. 응답을 화면에만 표시하지 않고 이미지 파일로 저장한 뒤 기존 첨부파일 관리와 배송 이력에 연결했습니다.

public ShipmentResult generateShipment(Shipment shipment) {
    LabelData label = labelClient.create(buildLabelRequest(shipment));
    FileReference labelFile = saveLabel(label);

    shipment.setTrackingNumber(label.transportOrderNumber());
    shipment.setCarrier(CHILEXPRESS);
    shipment.setLabelFileId(labelFile.id());
    shipmentHistory.save(shipment);

    return new ShipmentResult(labelFile.id());
}

private FileReference saveLabel(LabelData label) {
    byte[] image = decodeBase64(label.imageData());
    return attachments.save(label.barcode() + ".png", image);
}

실제 소스에서는 날짜별 디렉터리를 만들고 Base64 데이터를 파일에 쓴 다음 첨부정보를 등록했습니다. 이후 운송장 번호, 운송사와 라벨 첨부 식별자를 배송 이력에 저장했습니다. 위 코드는 그 실행 순서를 유지하면서 파일 경로·테이블·식별자를 일반화한 예시입니다.

외부 운송장 발급, 로컬 파일 생성과 DB 저장은 하나의 분산 트랜잭션으로 묶여 있지 않았습니다. 라벨 API의 응답 객체가 null이거나 파일 생성·쓰기·첨부 등록이 실패하면 업무 예외로 중단했습니다. 다만 응답 내부의 상세 목록과 라벨 값이 비어 있는 경우까지 일관되게 검사한 코드는 확인되지 않았습니다. 외부 발급 이후의 재시도와 중복 방지 정책도 이 글의 완료 범위로 주장하지 않습니다.

국가별 운송사를 화면 단계에서도 나눴습니다

칠레 고객에게는 Chilexpress를, 페루 고객에게는 FedEx만 선택할 수 있도록 국가별 운송사 노출을 나눴습니다. 영어·스페인어 배송 안내 문구도 이 분기에 맞춰 적용했습니다. Chilexpress 연계를 만들었다고 기존 FedEx 기능 전체를 제가 개발한 것은 아닙니다.

화면에서는 지역·도로명 조회와 필수 입력 검증을 제공했습니다. 서버도 커버리지 목록과 배송 서비스 응답을 다시 조회했습니다. 다만 위에서 본 문자열 변환 문제 때문에 유효하지 않은 배송 서비스가 라벨 요청 전에 항상 차단됐다고 말할 수는 없습니다. 화면 선택값만 신뢰하지 않고 서버에서 다시 확인하려는 방향은 맞았지만, 실패값을 다음 요청으로 넘기지 않는 방어까지 완결되지는 않았습니다.

테스트와 운영 반영 범위를 시점으로 나눴습니다

최초 구축에서는 국가별 운송사 노출, 배송 안내, 운송장 생성, 수령 주소와 페루 고객의 FedEx 흐름을 테스트 케이스로 확인했습니다. 개발·QA·운영 환경의 API 계정과 라벨 생성 결과를 대조했고, 현지 사용자 피드백도 반영했습니다. 등록된 테스트 항목에는 완료와 미완료가 함께 있었으므로 모든 항목이 한 번에 성공했다고 쓰지는 않습니다.

2023년 1월 31일에는 Chilexpress 운송장 정상 생성과 국가별 운송사 분기의 운영 반영을 확인했습니다. 제가 맡은 범위는 최초 REST API 연계 개발, QA와 오픈 지원입니다. 칠레지사 LIMS·COMS 전체 오픈을 단독으로 수행한 것으로 넓히지 않습니다.

2024년 7~8월에는 후속 재연계를 위해 소스를 수정하고 제한적인 내부 확인을 진행했습니다. 그러나 사용자 검증과 최종 운영 반영을 확인할 기록은 없습니다. 따라서 이 후속 작업은 재연계 완료가 아니라 내부 확인까지 진행한 별도 시도로 남깁니다.

같은 배송 연계에서 다른 경계를 보는 글

FedEx ETD 연계 흐름은 통관 문서를 먼저 올리고 받은 documentId를 운송장 요청에 연결하는 순서를 다룹니다. 외부 API 전환의 롤백 사례는 코드 밖의 운영 계정 관계가 실패했을 때 복구 기준선을 되찾는 과정을 설명합니다.

Chilexpress 사례의 중심은 주소 → 커버리지 → 배송 서비스 → 운송장 → 라벨 파일·배송 이력입니다. 외부 API 연계에서는 HTTP 호출 성공만 보지 않고, 앞 단계의 응답이 다음 업무 상태와 로컬 저장에 어떻게 이어지는지를 함께 설계해야 합니다.