hyunsu blog

홈노트

모듈 단독 테스트는 통과했는데 전체 기동에서 panic — OpenAPI 스키마 이름은 전역이다

목차 · 5
  1. 01블로그 모듈 첫 배포가 기동 직후 panic 으로 되돌려졌다
  2. 02huma 는 왜 duplicate name 으로 panic 하나
  3. 03타입에 모듈 접두를 붙이고 전 모듈 마운트 테스트를 더했다
  4. 04격리가 깨지는 지점은 테스트가 직접 밟게 한다
  5. 05자주 묻는 질문

huma 로 만든 Go API 가 기동하자마자 panic: duplicate name 으로 죽는다면, 서로 다른 패키지에 이름이 같은 요청·응답 타입이 있는 것이다. huma 의 기본 이름 규칙은 패키지 이름을 버리고 타입 이름만 쓰는데 스키마 레지스트리는 API 하나에 하나라서, 모듈을 하나씩 올리는 테스트로는 잡히지 않는다. 2026년 9월 우리는 타입에 모듈 접두를 붙이고, 모든 모듈을 한 API 에 올리는 테스트를 더해 고쳤다.

01 블로그 모듈 첫 배포가 기동 직후 panic 으로 되돌려졌다

API 는 Go 로 만든 모듈형 모놀리스다. 모듈 하나가 패키지 하나, 논리 DB 하나, 경로 접두사 하나를 가진다. 2026년 9월, 이 블로그를 받치는 blog 모듈을 처음 붙여 배포했더니 헬스체크가 실패했고 배포 스크립트가 이전 이미지로 자동 롤백했다. 컨테이너 로그의 마지막 줄은 이랬다.

text
panic: duplicate name: AuditDTO, new type: blog.AuditDTO, existing type: admin.AuditDTO

모듈 테스트는 전부 통과한 상태였고, OpenAPI 계약 테스트까지 있었다. 그래도 로그 한 줄이 부딪힌 두 타입을 이미 짚고 있었다. 자동 롤백 덕에 이 실수의 비용은 5분으로 끝났다.

02 huma 는 왜 duplicate name 으로 panic 하나

스키마 이름이 Go 타입 이름만으로 정해지는데, 그 이름을 담는 레지스트리는 API 하나에 하나뿐이어서다. huma 는 요청·응답 본문의 Go 타입을 반영해 OpenAPI 스키마를 만든다. 기본 이름 함수 DefaultSchemaNamer 는 문서 표현대로 패키지 이름을 무시하고 타입 이름을 쓴다(우리가 쓰는 v2.39 기준). 그래서 blog.AuditDTO 와 admin.AuditDTO 는 둘 다 AuditDTO 가 되고, 두 번째 등록 때 레지스트리가 "같은 이름, 다른 타입" 으로 보고 panic 을 낸다.

모듈 테스트가 못 잡은 이유도 같다. 테스트는 모듈 하나만 레지스트리에 올리니 부딪힐 상대가 없다. 이름 충돌은 모든 모듈이 한 레지스트리를 나눠 쓰는 실제 기동에서만 드러난다.

이름 없는 타입도 비켜 가지 못한다. 출력 구조체 안의 익명 Body 는 감싼 타입 이름에 필드 이름을 붙인 힌트로 이름이 정해진다. 그래서 두 모듈에 ListAuditOutput 이 있으면 둘 다 ListAuditOutputBody 가 되어 똑같이 부딪힌다.

03 타입에 모듈 접두를 붙이고 전 모듈 마운트 테스트를 더했다

  1. blog 모듈에서 스키마가 되는 타입 전부에 모듈 접두를 붙였다. AuditDTO 는 BlogAuditDTO 가 됐고, 출력 래퍼 이름도 함께 바꿨다.
  2. 기동 경로와 같은 huma 구성으로 모든 모듈을 한 API 에 올리는 테스트를 더했다. DB 없이 돌게 모듈마다 메모리 저장소를 주고, Redis 자리에는 메모리 구현(miniredis)을 넣었다.
  3. 고치기 전 코드로 이 테스트가 같은 panic 으로 실패하는 것을 먼저 보고, 고친 뒤 통과를 확인했다.
  4. 테스트의 모듈 표가 실제 기동 목록과 다르면 실패하게 했다. 새 모듈을 붙이고 테스트에 넣는 일을 잊지 않게 하려는 장치다.

이 테스트는 지금 모든 모듈을 올리고도 1초 안팎에 끝난다(2026년 10월 go test 로 잰 값). huma 문서는 패키지가 다른 같은 이름 타입이 문제를 낼 수 있으니 직접 만든 이름 함수를 쓰라고도 안내한다. 우리는 이름 함수를 바꾸는 대신 타입 이름에 접두를 붙였고, 덕분에 OpenAPI 문서의 스키마 이름만 보고도 어느 모듈 것인지 알 수 있다. 모듈 규칙 문서에는 "응답·요청 타입 이름에는 모듈 접두를 붙인다" 한 줄을 더했다.

04 격리가 깨지는 지점은 테스트가 직접 밟게 한다

모듈이 서로 import 하지 않아도, 프레임워크 수준의 이름 공간은 공유될 수 있다. 격리를 믿는 만큼 격리가 깨지는 지점을 테스트가 밟게 해야 한다. 이번에는 그 일을 운영의 헬스체크와 자동 롤백이 대신했다. 롤백이 배포 절차 안에 들어 있었기에 비용이 5분에서 멈췄고, 그래서 롤백은 배포의 일부여야 한다고 본다.

05 자주 묻는 질문

모듈 테스트는 통과했는데 왜 실제 기동에서만 panic 이 나나?

모듈 테스트는 모듈 하나만 레지스트리에 올리기 때문이다. 이름 충돌은 모든 모듈이 한 레지스트리를 쓰는 실제 기동에서만 드러난다.

이름 없는 Body 구조체도 이름을 바꿔야 하나?

예. 익명 타입은 감싼 출력 타입 이름에 필드 이름을 붙여 스키마 이름이 정해진다. 래퍼 이름이 겹치면 같은 panic 이 난다.

타입 이름을 바꾸지 않고 풀 수는 없나?

예. huma 문서는 이런 경우 직접 만든 이름 함수를 쓰라고 안내한다. 우리는 타입에 모듈 접두를 붙이는 쪽을 골랐다.