[Spring Boot] Swagger(Springdoc)에서 제네릭 공통 응답 스키마가 꼬이는 문제 해결기

2026. 1. 22. 23:26·Backend/Trouble Shooting

 

1. 문제 상황 

프로젝트에 공통 응답 포맷인 ApiResponseDto<T>를 도입한 이후, Swagger UI에서 예상치 못한 문제가 발생했다.

  • 현상
    모든 API의 응답 데이터(data 필드)가 특정 DTO(예: UserResponseDto)로 고정되어 표시됨
  • 영향
    일기(Diary), 날씨(Weather) 등 서로 다른 도메인의 API 문서에서도 응답 예시가 전부 유저 정보로 출력되어

공통 응답 래퍼를 쓰는 구조 자체는 문제가 없어 보였지만, 문서 자동화 도구(Springdoc) 와 결합되면서 문제가 표면화되었다.

2. 원인 분석

이 문제의 핵심 원인은
Springdoc의 스키마 생성 방식과 제네릭 타입 정보가 문서화 과정에서 처리되는 방식에 있었다.

2-1. Springdoc은 “스키마 이름”을 기준으로 동작한다

Springdoc(OpenAPI 3 기반)은 내부적으로
#/components/schemas/{SchemaName} 을 MAP 형태로 관리한다.

즉,

  • 스키마 이름이 같으면 → 같은 스키마
  • 한 번 등록된 이름은 → 덮어쓰지 않음

이라는 전제가 있다.

그런데 ApiResponseDto 클래스에 다음과 같이 선언되어 있었다.

@Schema(name = "ApiResponseDto")
public class ApiResponseDto<T> { ... }

이 설정은 Springdoc에게 다음과 같은 의미를 준다.

“이 클래스는 제네릭 타입이 무엇이든, 항상 ApiResponseDto라는 단 하나의 스키마로 취급해라.”

 

2-2. 제네릭 타입은 런타임에 소실되고, 문서화 시점엔 더 취약하다

자바의 제네릭은 컴파일 타임 개념이며,
런타임에는 타입 정보가 소거(Type Erasure)된다.

Springdoc은 이를 보완하기 위해
컨트롤러 메서드의 반환 타입 시그니처를 분석해 제네릭 타입을 유추하지만,

  • 스키마 이름이 고정되어 있으면
  • 제네릭 인자(T)가 달라도

👉 같은 스키마로 병합되어 버린다.

2-3. “먼저 본 놈이 임자” 현상

Springdoc은 애플리케이션 기동 시 컨트롤러를 스캔하며 스키마를 생성합니다.

이때,

  1. 가장 먼저 발견된 응답 타입이
    ApiResponseDto<UserResponseDto> 였고
  2. 이 구조가 ApiResponseDto 스키마로 등록됨
  3. 이후 등장하는
    ApiResponseDto<DiaryResponseDto>,
    ApiResponseDto<WeatherResponseDto> 는
    이미 존재하는 스키마를 재사용

결과적으로 모든 응답의 data 필드가
UserResponseDto 구조로 고정되어 보이게 된 것이다.

2-4. 커스텀 에러 응답 로직이 문제를 증폭시켰다

추가로, 커스텀 에러 응답 처리기에서 다음과 같이 하드코딩된 참조가 있었다.

#/components/schemas/ApiResponseDto

이로 인해,

  • 성공 응답용 스키마
  • 에러 응답용 스키마

가 같은 이름 아래에서 뒤섞이며,
Swagger UI 상에서는 더 예측 불가능한 형태로 보이게 되었다.

3. 해결 과정 

문제를 해결하는 핵심 전략은 다음 두 가지였다.

  1. Springdoc이 제네릭 타입을 구분할 수 있게 만들어준다
  2. 성공 응답과 에러 응답의 스키마 책임을 명확히 분리한다

Step 1. 고정된 스키마 이름 제거

가장 먼저 ApiResponseDto에 지정된 name 속성을 제거했다.

@Schema(description = "공통 응답 형식") // name 속성 제거
public class ApiResponseDto<T> { ... }

이제 Springdoc은 다음과 같이 동작합니다.

  • ApiResponseDto<UserResponseDto>
    → ApiResponseDtoUserResponseDto
  • ApiResponseDto<DiaryResponseDto>
    → ApiResponseDtoDiaryResponseDto

즉, 제네릭 인자까지 포함한 고유 스키마를 자동 생성하게 된다.

Step 2. 에러 응답 전용 스키마를 명시적으로 등록

에러 응답의 특징은 명확하다.

  • data는 항상 null
  • 구조는 고정됨
  • 성공 응답과 섞일 이유가 없음

그래서 에러 응답의 표준을
ApiResponseDto<Void> 로 정의했다.

다만 Void 타입은 Springdoc 자동 스캔 대상에서 누락될 수 있기 때문에,
OpenApiCustomizer를 사용해 명시적으로 스키마를 등록했다.

@Bean
public OpenApiCustomizer apiResponseDtoVoidSchemaCustomizer() {
    return openApi -> {
        Schema<?> voidSchema = new Schema<>()
            .type("object")
            .description("에러 응답용 포맷");
        // code, message, data(nullable) 필드 정의
        openApi.getComponents().addSchemas("ApiResponseDtoVoid", voidSchema);
    };
}

이로써 에러 응답은 항상
ApiResponseDtoVoid 스키마를 참조하게 된다.

Step 3. 커스텀 에러 처리기의 참조 대상 수정

기존에는 에러 응답에서도 성공 응답과 동일한 스키마를 참조하고 있었다.

  • 기존
  • #/components/schemas/ApiResponseDto
  • 변경
  • #/components/schemas/ApiResponseDtoVoid

이 수정으로 성공/실패 응답 간의 스키마 오염 경로를 완전히 차단할 수 있었다.

Step 4. 컨트롤러 선언 방식 표준화

Springdoc의 장점을 살리기 위해
컨트롤러 레벨에서의 선언 규칙도 정리했습니다.

  • 성공 응답
    • @ApiResponse에서 content를 비워둠
    • → 메서드 반환 타입을 기준으로 Springdoc이 자동 스키마 생성
  • 에러 응답
    • @ApiErrorCodeExamples 사용
    • content = @Content 유지
    • → 커스텀 예시(JSON)가 정확히 주입되도록 설정

이렇게 역할을 분리하니
설정도 단순해지고, 문서 일관성도 유지되었습니다.

4. 결과 및 교훈 

결과

  • 각 API 엔드포인트마다 실제 반환 DTO 구조가 정확히 Swagger UI에 노출
  • 성공 응답과 에러 응답이 명확히 분리된 스키마로 표현
  • 프론트엔드 협업 시 “이 API 응답 구조가 뭐냐”는 질문이 사라짐

교훈

  1. 제네릭 공통 응답 DTO에 고정된 @Schema(name)는 매우 위험하다
  2. Springdoc의 자동 스키마 생성은 강력하지만,
    Void, 에러 응답처럼 특수한 케이스는 명시적으로 관리해야 한다
  3. Swagger 문제의 대부분은 “도구의 버그”가 아니라
    도구가 의도한 방식과 코드 설계가 어긋난 결과인 경우가 많다

 

'Backend > Trouble Shooting' 카테고리의 다른 글

JPA 자동 스키마 관리에서 Flyway 기반 DB 마이그레이션  (0) 2026.02.03
[SpringBoot] UTC 시간 처리, 코드가 아니라 설정으로 정리하기 (Hibernate 6 & Jackson)  (0) 2026.01.21
[SpringBoot] 검증 로직의 위치(@Valid vs Service) : 테스트 코드로 드러난 설계 문제  (0) 2026.01.14
[SpringBoot] 날짜와 시간 처리를 UTC로 표준화하기  (0) 2026.01.10
[SpringBoot] 프로젝트에 SonarCloud 연동하기: Gradle + GitHub Actions  (0) 2026.01.05
'Backend/Trouble Shooting' 카테고리의 다른 글
  • JPA 자동 스키마 관리에서 Flyway 기반 DB 마이그레이션
  • [SpringBoot] UTC 시간 처리, 코드가 아니라 설정으로 정리하기 (Hibernate 6 & Jackson)
  • [SpringBoot] 검증 로직의 위치(@Valid vs Service) : 테스트 코드로 드러난 설계 문제
  • [SpringBoot] 날짜와 시간 처리를 UTC로 표준화하기
wlals916
wlals916
  • wlals916
    JM.devlog
    wlals916
    GitHub
  • 전체
    오늘
    어제
    • 분류 전체보기 (28)
      • Algorithms (20)
        • Problem Solving (14)
        • Language & Concept (6)
      • Backend (7)
        • Study Notes (1)
        • Trouble Shooting (6)
        • Development (0)
      • AI (0)
      • Review (1)
      • Dev Log (0)
  • 관리자

    • 글쓰기
    • 관리 페이지
    • 티스토리 홈
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로그래머스
    코딩테스트
    코테
    회고
    김영한
    백준
    DevOps
    SpringBoot
    백엔드
    C++
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.6
wlals916
[Spring Boot] Swagger(Springdoc)에서 제네릭 공통 응답 스키마가 꼬이는 문제 해결기
상단으로

티스토리툴바