API 문서를 작성하는 도구로 Spring REST Docs와 Swagger를 고려했습니다.
그래서 Swagger를 선택했지만 문제가 있었습니다. 저희는 도메인별로 Enum을 만들어 에러 코드를 관리하고 있었는데 Swagger는 이 Enum에 정의된 수많은 에러 응답들을 자동으로 문서에 반영해주지 못했습니다.
물론 @ApiResponse 어노테이션을 컨트롤러 메서드마다 일일이 붙여 수동으로 작성할 수도 있었지만 이는 보일러플레이트 코드를 양산하고, 에러 코드가 추가/변경될 때마다 문서도 함께 수정해야 하는 유지보수의 어려움을 만들었습니다.
@ApiResponse 예시
@Operation(summary = "템플릿 승인 요청", description = "특정 템플릿에 대해 승인 요청을 보낸다.")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "템플릿 승인 요청 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResult.class),
examples = @ExampleObject(
name = "승인 요청 성공",
value = """
{
"success": true,
"data": {
"templateId": 1,
"status": "PENDING",
"requestedBy": "user_123"
},
"error": null
}
"""
)
)
),
@ApiResponse(
responseCode = "403",
description = "권한 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResult.class),
examples = @ExampleObject(
name = "승인 권한 없음 (TEMPLATE_403_001)",
value = """
{
"success": false,
"data": null,
"error": {
"code": "FORBIDDEN_TEMPLATE",
"message": "권한이 없는 템플릿입니다."
}
}
"""
)
)
),
@ApiResponse(
responseCode = "404",
description = "템플릿을 찾을 수 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResult.class),
examples = @ExampleObject(
name = "템플릿 없음 (TEMPLATE_404_001)",
value = """
{
"success": false,
"data": null,
"error": {
"code": "TEMPLATE_NOT_FOUND",
"message": "템플릿을 찾을 수 없습니다."
}
}
"""
)
)
),
// ... TemplateErrorCode에 정의된 다른 모든 에러 ...
})
@PostMapping("/{id}/approve-request")
public ApiResult<TemplateApproveResponse> approveTemplate(
@PathVariable Long id,
@AuthenticationPrincipal CustomUserPrincipal principal
) {
System.out.println(principal.getId());
TemplateApproveResponse response = templateService.approveTemplate(id, principal.getId());
return ApiResult.ok(response);
}
저의 목표는 다음과 같았습니다.
Enum에 정의된 에러 코드가 그대로 API 문서에 반영되어야 한다.이 문제를 해결하기 위해 OperationCustomizer를 활용한 커스텀 어노테이션을 만들기로 했습니다.
구현은 크게 두 단계로 나뉩니다.
@ApiErrorCodeExample 정의SwaggerConfig 작성먼저, 컨트롤러의 각 메서드가 어떤 에러 코드 Enum을 참조해야 하는지 알려주는 어노테이션을 만듭니다.
ApiErrorCodeExample.java
package com.example.final_projects.config.swagger;
import com.example.final_projects.exception.code.BaseErrorCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Swagger 문서에 API에서 발생 가능한 에러 코드 예시를 동적으로 추가하기 위한
* 커스텀 어노테이션입니다.
*
* BaseErrorCode를 구현한 Enum 클래스를 값으로 받습니다.
*/
@Target(ElementType.METHOD) // 메서드에 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임에 이 어노테이션 정보를 참조할 수 있도록 설정
public @interface ApiErrorCodeExample {
Class<? extends BaseErrorCode> value(); // BaseErrorCode를 상속받는 Enum 클래스 타입을 받음
}
@Target(ElementType.METHOD): 이 어노테이션을 메서드에만 붙일 수 있도록 지정합니다.@Retention(RetentionPolicy.RUNTIME): Springdoc이 런타임에 어노테이션을 읽고 문서를 동적으로 생성해야 하므로 RUNTIME으로 설정합니다.value(): TemplateErrorCode.class 처럼 에러 코드가 정의된 Enum 클래스를 넘겨받기 위해 사용합니다. (이때 BaseErrorCode라는 공통 인터페이스를 구현하도록 강제했습니다.)다음으로, Springdoc이 Swagger 문서를 생성하는 과정에 개입하여 @ApiErrorCodeExample 어노테이션을 처리하는 로직을 추가합니다. springdoc-core가 제공하는 OperationCustomizer를 Bean으로 등록하면 이 작업을 수행할 수 있습니다.
SwaggerConfig.java
@Configuration
public class SwaggerConfig {
// ... (OpenAPI 기본 설정 Bean: Jober API 타이틀, JWT 인증 설정 등) ...
/**
* @ApiErrorCodeExample 어노테이션을 처리하여
* Swagger 문서에 동적으로 Error Response 예시를 추가하는 OperationCustomizer
*/
@Bean
public OperationCustomizer errorCodeCustomizer() {
return (operation, handlerMethod) -> {
// 1. 핸들러 메서드(컨트롤러)에서 @ApiErrorCodeExample 어노테이션 찾기
ApiErrorCodeExample annotation = handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
if (annotation != null) {
// 2. 어노테이션이 존재하면, value()로 지정된 Enum 클래스를 가져옴
Class<? extends BaseErrorCode> errorCodeClass = annotation.value();
// 3. 해당 Enum의 상수들(에러 코드)을 가져와 Swagger 문서에 예시 추가
addErrorCodeExamples(operation.getResponses(), errorCodeClass);
}
return operation;
};
}
/**
* BaseErrorCode Enum 클래스를 바탕으로 ApiResponses에 에러 응답 예시(Example)를 추가
*/
private void addErrorCodeExamples(ApiResponses responses, Class<? extends BaseErrorCode> errorCodeClass) {
// 1. Enum에 정의된 모든 에러 코드를 가져옴
BaseErrorCode[] errorCodes = errorCodeClass.getEnumConstants();
// 2. 에러 코드들을 HTTP Status Code별로 그룹화
// (e.g., 400 - [BAD_REQUEST_001, BAD_REQUEST_002], 404 - [NOT_FOUND_001])
Map<Integer, List<BaseErrorCode>> groupedByStatus = Arrays.stream(errorCodes)
.collect(Collectors.groupingBy(ec -> ec.getErrorReason().getStatus()));
// 3. 그룹화된 맵을 순회하며 HTTP Status별로 ApiResponse 생성
groupedByStatus.forEach((status, list) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse().content(content.addMediaType("application/json", mediaType));
// 4. 같은 HTTP Status를 가진 에러 코드들을 Example로 추가
for (BaseErrorCode ec : list) {
Example example = new Example();
example.setSummary(ec.getErrorReason().getMessage()); // 예시 이름 (Summary)
// 예시 값 (JSON). ApiResult는 공통 응답 포맷
example.setValue(ApiResult.error(
ec.getErrorReason().getCode(),
ec.getErrorReason().getMessage()
));
// (e.g., "USER_400_001")
mediaType.addExamples(ec.getErrorReason().getCode(), example);
}
// 5. 생성된 ApiResponse를 Operation의 ApiResponses에 추가
responses.addApiResponse(String.valueOf(status), apiResponse);
});
}
}
핵심 로직:
errorCodeCustomizer Bean이 Springdoc에 의해 등록됩니다.Operation)를 스캔할 때마다 errorCodeCustomizer가 동작합니다.handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class)를 통해 메서드에 @ApiErrorCodeExample이 붙어있는지 확인합니다.annotation.value()로 TemplateErrorCode.class 같은 Enum 클래스 정보를 가져옵니다.addErrorCodeExamples 헬퍼 메서드를 호출합니다.addErrorCodeExamples에서는:Enum의 모든 상수(getEnumConstants())를 가져옵니다.groupingBy를 사용해 맵으로 만듭니다. (Swagger는 HTTP Status 기준으로 응답을 묶어 보여주기 때문입니다.)ApiResponse를 생성합니다.BaseErrorCode ec)에 대해 Example 객체를 만듭니다.example.setSummary(): 예시의 요약 (e.g., "입력값이 유효하지 않습니다.")example.setValue(): 실제 반환될 JSON 응답 본문 (ApiResult.error(...))mediaType.addExamples(): 생성된 예시를 MediaType에 추가합니다. 이때 **키(Key)로 에러 코드 문자열(e.g., "INVALID_INPUT")**을 사용하면, Swagger UI에서 드롭다운 형태로 여러 예시를 선택해 볼 수 있습니다.ApiResponse를 responses.addApiResponse(String.valueOf(status), ...)를 통해 Swagger 문서에 추가합니다.이제 컨트롤러에서 이 어노테이션을 사용하기만 하면 됩니다.
컨트롤러 예시:

Swagger UI 결과:

@ApiErrorCodeExample 커스텀 어노테이션과 OperationCustomizer를 도입함으로써 다음을 달성할 수 있었습니다.
Enum이 곧 문서가 되었습니다. Enum만 수정하면 별도 작업 없이 Swagger 문서가 자동으로 갱신됩니다.@ApiResponse 작성에 비해 문서화에 드는 공수가 줄었습니다.