← 프로젝트 목록

Backend Project · 03

DeployHub

배포 패키지 자동화 백엔드 · 안랩클라우드메이트 현장실습 · 단독 개발 · 2026.07–08 · main 84 + test 28 클래스

Java 17Spring Boot 3.5JPAFlywayMySQLskopeo / OCIMicrosoft GraphTestcontainersDocker
Overview

폐쇄망 고객사에 제품을 반입하려면 컨테이너 이미지를 버전 단위로 모아 파일로 만들어 전달해야 한다. 그동안 담당자가 로컬 PC에서 수동으로 이미지를 받아 압축하고 공유 드라이브에 올리던 과정을, 메인버전을 고르면 매니페스트를 확정하고 이미지를 병렬 수집해 OCI 아카이브로 묶은 뒤 SharePoint에 업로드하고 전달 링크까지 내주는 백엔드로 자동화했다. 백엔드 API와 Swagger 명세가 담당 범위이며 화면은 별도 주체가 맡았다.

Problem

수동 수집은 담당자 로컬 PC의 디스크·네트워크에 묶이고, 고객사별로 어떤 버전을 반입했는지 이력이 문서에 흩어져 있었다. 게다가 반입물은 사내에서 검증한 것과 바이트 단위로 같아야 하는데, 그 동일성을 보장할 근거가 없었다.

My Role & Technical Decisions

단독 개발 — 버전·매니페스트 도메인, 레지스트리 연동(이미지 존재 검증·크기 산정), skopeo 기반 아카이브 수집, Microsoft Graph 업로드·링크 발급, Job 오케스트레이션(진행률·재시도·정리 배치), 오류 코드 노출 등급 체계, Flyway 스키마와 Testcontainers 통합 테스트까지 설계·구현했다.

Architecture
DeployHub 시스템 아키텍처온라인 빌드 환경과 폐쇄망 설치 환경의 경계를 중심으로 패키징·검증 흐름을 표현했습니다.

01 · 반입 패키지 생성

의존성을 수집해 재현 가능한 오프라인 번들로 고정

  1. 01Source Repo설정 · 스크립트
  2. collect
  3. 02Online Builder이미지 · 패키지 수집
  4. package
  5. 03Offline Bundlechecksum · manifest
  6. verify
  7. 04Secure Transfer승인된 반입 매체
  8. install
  9. 05Air-gapped Host무중단 설치

02 · 설치 검증·복구

설치 전후 상태를 기록해 실패 시 원복

  1. 01Preflight CheckOS · port · disk
  2. record
  3. 02State Store설치 단계 · 백업
  4. inspect
  5. 03Health Check서비스 응답 검증
  6. on failure
  7. 04Rollback실패 단계 복구
  • 사용자·클라이언트
  • 애플리케이션
  • 데이터
  • 비동기 처리
  • 배포
  • 관측
  • 보안·검증
  • 외부 시스템
Key API Endpoints
MethodPathDescription
POST/api/main-versions/{v}/package-job매니페스트 확정 · 패키징 Job 생성
GET/api/package-jobs/{v}Job 상세 · 진행률 폴링
POST/api/package-jobs/{v}/retry실패 항목만 재시도
GET/api/package-jobs/{v}/files폴더 공유 링크 · 파일별 URL 발급
GET/api/health/registry레지스트리·자격증명 기동 점검
Troubleshooting & Evidence

반입한 이미지가 고객사 환경에서 이름으로 실행되지 않음↗

원인: 아카이브에 기록되는 이미지 참조를 저장소명만으로 적었는데, 컨테이너 런타임은 조회 시 레지스트리 호스트를 붙여 정규화한다 — 기록된 이름과 조회 이름이 영영 달라져 적재는 성공하는데 실행·조회·삭제가 전부 실패하고 이미지 목록에 같은 항목이 두 번 표시됐다

해결: 목적지 참조를 호스트까지 붙인 완전 수식 참조로 고정. 네임스페이스가 없는 저장소는 정규형이 한 단계 더 들어가는 것을 7MB 테스트 이미지로 분리 확인해 별도 처리했다. 반입물에 사내 레지스트리 주소가 실리면 안 되므로 호스트는 공개 기본값을 명시해 정규화 결과와 일치시켰다

배운 점: 컨테이너 이미지 이름은 저장 시점과 조회 시점의 정규화 규칙이 같아야 한다 — '적재 성공'은 사용 가능을 뜻하지 않는다. 그리고 증상이 겹쳐 보일 때는 변수를 하나만 바꾼 최소 이미지로 원인을 분리해야 엉뚱한 것을 고치지 않는다

레지스트리에 분명히 있는 이미지가 '없음'으로 판정↗

원인: 매니페스트 조회 Accept 헤더에 단일 매니페스트 형식만 넣고 멀티아키텍처 인덱스 형식을 빠뜨렸다. 레지스트리는 사유를 명시한 404를 주는데, 클라이언트가 404를 '없음'으로 뭉뚱그려 처리해 존재하는 이미지가 조용히 누락됐다

해결: Accept에 인덱스 2종을 추가하고, 404의 사유를 구분해 '확인 불가'와 '확실히 없음'을 분리했다. 실측 저장소 12개 중 3개가 인덱스였다

배운 점: 외부 API의 404를 전부 '없음'으로 접으면, 협상 실패처럼 다른 이유로 온 404가 도메인 사실로 둔갑한다 — 응답 사유를 읽고 분기해야 한다

반입 아카이브가 원본보다 최대 3배 이상 커지고 무결성 대조가 항상 실패↗

원인: 아카이브 포맷이 레이어를 다시 풀어 담아 압축이 사라졌고, 멀티아키텍처 인덱스를 플랫폼 하나로 평탄화해 담으면서 아카이브 digest가 레지스트리 digest와 달라졌다

해결: 압축과 digest를 그대로 보존하는 OCI 아카이브 포맷으로 바꾸고, 원본 형식을 유지하는 옵션과 인덱스를 통째로 보존하는 옵션을 함께 적용. 포맷을 특정 버전으로 강제하면 빌드 어테스테이션에서 죽는 것도 확인해 강제를 걷어냈다

배운 점: '같은 이미지'의 기준은 실행 가능 여부가 아니라 digest 동일성이다 — 포맷을 손대는 순간 그 기준이 깨지므로, 전송 계층은 원본을 변형하지 않는 선택지를 먼저 찾아야 한다

업로드 API가 같은 코드인데 어떤 날은 되고 어떤 날은 전건 실패↗

원인: 요청 본문을 불변 Map 리터럴로 만들었는데, 이 자료구조는 순회 순서가 JVM 기동마다 무작위로 바뀐다. 외부 API가 특정 키를 다른 키보다 먼저 요구해서, 순서가 어긋난 기동에서만 400이 났다 — 재기동 뽑기가 된 것이다

해결: 순서가 보장되는 본문 생성기로 교체하고, 직렬화된 본문 바이트 자체를 테스트로 고정했다

배운 점: 순서를 보장하지 않는 자료구조는 '보통 순서대로 나온다'가 아니라 '언제든 바뀔 수 있다'로 읽어야 한다. 재현이 안 되는 간헐 실패일수록 실행마다 달라지는 값이 어디 있는지부터 찾는다

이미 적용된 마이그레이션 파일에 주석을 고쳤다가 서버가 기동 불가↗

원인: 마이그레이션 도구가 파일 내용으로 체크섬을 내므로 주석 한 줄도 불일치가 된다. 통합 테스트는 매번 빈 DB로 시작해 이 부류를 구조적으로 잡지 못했다

해결: 마이그레이션을 한 파일로 통합하고 기존 DB는 재기준선으로 넘겼다. 그리고 마이그레이션 파일의 CRC32를 고정하는 테스트를 추가해, 적용된 파일이 수정되면 CI에서 먼저 깨지게 했다

배운 점: '빈 DB에서 잘 된다'가 검증하지 못하는 영역이 있다 — 기존 DB의 상태에 의존하는 결함은 테스트 전략 자체를 바꿔야 잡힌다

실패 사유가 인증 없는 조회 응답에 그대로 실려 서버 내부 정보가 새어 나감↗

원인: 항목별 실패 메시지를 자유 문자열로 저장했는데, 외부 API 예외 메시지에 업스트림 응답 본문이 통째로 들어 있었다. 이 값이 무인증 조회 응답에 노출됐다

해결: 오류를 enum으로만 정의하고 각 코드에 노출 등급(응답 노출 / 로그 전용)을 부여. 실패 기록 API가 문자열을 아예 받지 못하게 막고, 응답에 나가는 문구에 경로·호스트 패턴이 없는지 검사하는 테스트를 세워 3중으로 강제했다

배운 점: 민감 정보 유출은 규칙 문서로 막히지 않는다 — 위반이 컴파일 또는 테스트에서 실패하도록 타입과 검사로 강제해야 다음 사람도 지키게 된다

Metrics
3.37× → 1×
반입 아카이브 크기 (포맷 교체로 원본 크기 유지)
digest 일치
레지스트리 원본과 반입물 무결성 대조
2개월 단독
현장실습 (main 84 · test 28 클래스)
Infra & Deploy

Java 17 · Spring Boot 3.5 · JPA · MySQL 8(Flyway 스키마 관리) · Docker Compose. 컨테이너 레지스트리는 REST로 이미지 존재·크기를 확인하고, 실제 수집은 skopeo를 외부 프로세스로 실행해 OCI 아카이브로 받는다. 수집·업로드 모두 상한이 있는 병렬 처리로 돌리며 Job 단위로 진행률·재시도·보존 정리를 관리한다. 산출물은 Microsoft Graph로 SharePoint 문서 라이브러리에 분할 업로드하고 공유 링크를 발급한다. 실제 바이너리가 필요한 경로는 Testcontainers로 레지스트리를 띄워 실물 검증한다.

Next Step

프로젝트에 대해 더 이야기해 보세요.

연락하기