스웨거 @ApiResponses를 커스텀 어노테이션으로 대체해보기
1.문제점
현재는 ErrorCode를 인터페이스로 정의하여, 상황에 맞는 다양한 에러 코드를 enum으로 커스텀하여 구현하고 있습니다.
이를 통해 각 도메인 에러 처리를 객체 지향적으로 작성하고 관리하려고 시도했습니다.
public interface ErrorCode {
HttpStatus getHttpStatus();
String getErrorCode();
}
package com.linkmoa.source.domain.site.error;
import com.linkmoa.source.global.error.code.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public enum SiteErrorCode implements ErrorCode {
SITE_NOT_FOUND(HttpStatus.NOT_FOUND,"Site를 찾을 수 없습니다."),
SITE_ERRORCODE_TEST(HttpStatus.NOT_FOUND,"site error code 테스트");
private HttpStatus httpStatus;
private String errorMessage;
SiteErrorCode(HttpStatus httpStatus, String errorMessage) {
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}
@Override
public HttpStatus getHttpStatus() {
return httpStatus;
}
@Override
public String getErrorMessage(){
return errorMessage;
}
}
하지만 이렇게 ErrorCode를 인터페이스로 정의하고 각 도메인 error를 enum을 통해 에러 코드를 관리한 후, Swagger를 사용해 API
문서화를 진행하면서 문제가 발생했습니다.
Swagger에서 @ApiResponses와 @ApiResponse를 사용하는 과정에서, 애노테이션의 속성들은 상수 값만 허용됩니다. 즉, httpStatus나 errorMessage 같은 값을 동적으로 가져와서 사용할 수 없습니다. 이로 인해 ErrorCode에서 정의한 값들을 자동으로 참조할 수 없고, 개발자가 직접 하나하나 수동으로 옮겨 적어야 하는 불편함이 생겼습니다.
@Tag(name="Post",description = "사이트 관련 API")
@Operation(summary = "사이트 저장",description = "사이트를 저장합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "400",
description = "실패",
content = @Content(
schema = @Schema(implementation = ResponseError.class))),
@ApiResponse(
responseCode = "200",
description = "성공",
content = @Content(
schema = @Schema(implementation = ResponseError.class)))
})
@PostMapping
ResponseEntity<ApiSiteResponse<Long>> saveSite(
@RequestBody @Validated SiteCreateRequestDto siteCreateRequestDto,
@AuthenticationPrincipal PrincipalDetails principalDetails
);
위와 같이 Swagger에서 에러 관련 응답을 정의할 때, ErrorCode 클래스에 있는 HTTP 상태 코드나 메시지를 자동으로 참조하지 못하고, 매번 일일이 복사해서 사용해야 하는 문제가 발생했습니다.
2. 해결 방법: 에러 응답 값을 출력할 수 있는 커스텀 어노테이션을 구현
현재 문제를 해결하기 위해, ErrorCode를 활용하여 코드와 예시 값을 자동으로 참조할 수 있도록 커스텀 어노테이션을 만들어보겠습니다. 이 어노테이션을 사용하면 Swagger 문서화 시에 에러 코드와 메시지를 일일이 수동으로 입력할 필요 없이, ErrorCode에서 정의한 값들을 간편하게 API 응답에 반영할 수 있습니다.
1.Swagger 문서화에 사용할 어노테이션 생성
어노테이션을 생성합니다.
@Target
- 어토네이션을 적용할 수 있는 위치(타입)을 지정함. 예를 들어, 메소드 , 클래스 필드 등 특정 요소에만 붙일 수 있게 제약하는 것
- 아래의 코드의 경우 ElementType.METHOD는 메소드에만 붙을 수 있는 어노테이션이라는 것을 의미합니다.
@Retention
- 어노테이션은 메타 정보가 유지되는 기간을 정의함. 즉, 어노테이션의 라이플 사이클 (어노테이션이 언제까지 살아있느냐)를 결정
- 아래의 코드의 경우 RetentionPolicy.RUNTIME는 런타임 동안에도 유지되며, 프로그램 실행 중에 해당 어노테이션의 정보를 읽을 수 있음을 의미함. 즉, 코드가 실행되는 도중에도 어노텡션의 내용을 참조하거나 조작할 수 있음.
- 또한, RUNTIME은 Reflection API로 접근도 가능
Reflection API란?
기본적으로 Class의 객체를 생성해서 본 클래스를 인스턴스화,메소드 호출 등을 진행할 수 있습니다. 하지만, 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메소드, 타입 ,변수 등)에 접근할 수 있게 해주는 자바 기법을 Reflection API라고 합니다.
Class<? extends ErrorCode> value();
저는 ErrorCode 인터페이스를 구현한 특정 enum의 타입들을 사용하여 에러 코드를 관리하고 있습니다. 그렇기 때문에, 해당 API 메소드에서 사용할 에러 코드(enum 타입)을 전달받을 수 있게 작성했습니다.
package com.linkmoa.source.global.swagger;
import com.linkmoa.source.global.error.code.ErrorCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExamples {
Class<? extends ErrorCode> value(); // ErrorCode를 구현한 enum 클래스 타입을 배열로 받음
}
2.SwaggerConfig 작성
한번에 보기 힘드니 메소드 별로 나눠서 알아보겠습니다.
//SwaggerConfig.java
@Component
@RequiredArgsConstructor
public class SwaggerConfig {
@Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
ApiErrorCodeExamples apiErrorCodeExample =
handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class);
// ApiErrorCodeExample 어노테이션 단 메소드 적용
if (apiErrorCodeExample != null) {
generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
}
return operation;
};
}
private void generateErrorCodeResponseExample(Operation operation, Class<? extends ErrorCode> value) {
ApiResponses responses =operation.getResponses();
ErrorCode[] errorCodes= value.getEnumConstants();
Map<Integer, List<ExampleHolder>> statusWithExampleHolders = Arrays.stream(errorCodes)
.map(
errorCode -> ExampleHolder.builder()
.holder(getSwaggerExample((Enum<?>) errorCode))
.name(((Enum<?>) errorCode).name()) // 직접 name() 메서드 사용
.code(errorCode.getHttpStatus().value())
.build()
)
.collect(Collectors.groupingBy(ExampleHolder::getCode));
// ExampleHolders를 ApiResponses에 추가
addExamplesToResponses(responses, statusWithExampleHolders);
}
// exampleHolder를 ApiResponses에 추가
private void addExamplesToResponses(ApiResponses responses,
Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {
statusWithExampleHolders.forEach(
(status, v) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();
v.forEach(
exampleHolder -> mediaType.addExamples(
exampleHolder.getName(),
exampleHolder.getHolder()
)
);
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
responses.addApiResponse(String.valueOf(status), apiResponse);
}
);
}
private Example getSwaggerExample(Enum<?> errorCodeEnum) {
// Enum을 ErrorCode로 캐스팅
ErrorCode errorCode = (ErrorCode) errorCodeEnum;
// ResponseError 객체 생성
ResponseError responseError = ResponseError.builder()
.httpStatusCode(errorCode.getHttpStatus())
.errorMessage(errorCode.getErrorMessage())
.build();
Example example =new Example();
example.setValue(responseError);
return example;
}
customize() 메서드
@Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
ApiErrorCodeExamples apiErrorCodeExample =
handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class);
// ApiErrorCodeExample 어노테이션 단 메소드 적용
if (apiErrorCodeExample != null) {
generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
}
return operation;
};
}
OperationCustomizer 빈은 리플렉션을 사용하여 특정 어노테이션이 달린 메소드 정보를 동적으로 탐색하고, 스웨거(Swagger) 문서에 자동으로 에러 응답을 처리하고 있습니다. 자세한 동작 과정은 아래와 같습니다.
1.리플렉션을 통한 어노테이션 탐색
- handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class)를 호출하여, 현재 처리 중인 메소드에 @ApiErrorCodeExamples 어노테이션이 붙어 있는지 확인
- getMethodAnnotation 메소드는 HandlerMethod 객체를 사용하여, 이 메소드에 특정 어노테이션이 붙어 있는지 확인합니다. 이를 통해 메소드에 @ApiErrorCodeExamples와 같은 어노테이션이 달려 있는지 런타임 동안에 동적으로 확인함. 이는 리플렉션을 활용한 방식으로 컴파일 타임이 아닌 런타임 중에 메타데이터를 검사할 수 있음.
2.어노테이션이 존재하는 경우 처리
- @ApiErrorCodeExamples 어노테이션이 메소드에 존재하면, 해당 어노테이션의 value() 메소드를 호출하여 에러 코드(enum 타입) 정보를 가져옴.
- value() 메소드로 얻은 정보는 해당 메소드가 반환할 수 있는 에러 응답의 세부 정보를 포함
3.스웨거 예시 응답 생성
- 가져온 에러 코드(enum 타입) 정보를 바탕으로 generateErrorCodeResponseExample 메소드를 호출하여 스웨거 문서에 자동으로 에러 응답 예시 값을 추가함.
- generateErrorCodeResponseExample는 에러 코드의 HTTP 상태 코드와 에러 메시지를 스웨거 문서에 반영
4.API 문서에 반영
- 최종적으로 수정된 Operation 객체가 Swagger 문서에 반영됨.
1.Operation이란
Operation은 Swagger의 각 API 엔드포인트에 대한 정보를 담고 있는 객체입니다.
OperationCustomizer를 사용해 Operation 객체를 수정하고 있으므로, 해당 API의 커스텀 정보나 에러 응답 예시를 추가하는 데 사용됩니다.
- Swagger에서 API의 설명, 응답 코드, 요청/응답 스키마 등을 정의할 때 사용하는 클래스입니다.
- 이 객체는 각 API가 어떻게 동작하는지에 대한 정보를 스웨거 문서에 표시하는 역할을 합니다.
2.HandlerMethod이란
HandlerMethod는 스프링에서 사용되는 객체로, 특정 HTTP 요청에 매핑된 컨트롤러의 메소드 정보를 담고 있습니다.
- 예를 들어, @PostMapping, @GetMapping 같은 어노테이션이 달린 메소드가 실제로 어떤 메소드인지, 그 메소드의 파라미터와
리턴 타입은 무엇인지 등의 정보를 포함
generateErrorCodeResponseExample 메서드
private void generateErrorCodeResponseExample(Operation operation, Class<? extends ErrorCode> value) {
ApiResponses responses =operation.getResponses();
ErrorCode[] errorCodes= value.getEnumConstants();
Map<Integer, List<ExampleHolder>> statusWithExampleHolders = Arrays.stream(errorCodes)
.map(
errorCode -> ExampleHolder.builder()
.holder(getSwaggerExample((Enum<?>) errorCode))
.name(((Enum<?>) errorCode).name()) // 직접 name() 메서드 사용
.code(errorCode.getHttpStatus().value())
.build()
)
.collect(Collectors.groupingBy(ExampleHolder::getCode));
// ExampleHolders를 ApiResponses에 추가
addExamplesToResponses(responses, statusWithExampleHolders);
}
// ExampleHolder
@Getter
@Builder
public class ExampleHolder {
private Example holder; // 스웨거 Example 객체
private String name; // 에러 코드 이름
private int code; // http 상태코드
}
//ErrorCode
public interface ErrorCode {
HttpStatus getHttpStatus();
//String getErrorCode();
String getErrorMessage();
}
//SiteErrorCode 예시
@Getter
public enum SiteErrorCode implements ErrorCode {
SITE_NOT_FOUND(HttpStatus.NOT_FOUND,"Site를 찾을 수 없습니다."),
SITE_ERRORCODE_TEST(HttpStatus.NOT_FOUND,"site error code 테스트");
private HttpStatus httpStatus;
private String errorMessage;
SiteErrorCode(HttpStatus httpStatus, String errorMessage) {
this.httpStatus = httpStatus;
this.errorMessage = errorMessage;
}
}
generateErrorCodeResponseExample 메서드는 ErrorCode enum의 HTTP 상태 코드와 에러 코드의 이름을 가져와서 에러 응답 예시인 ExampleHolder 객체를 생성합니다. 그리고 ExampleHolder 객체를 ApiResponse에 추가합니다.
간단히 말해, 이 메서드는:
- ErrorCode enum에서 모든 오류 코드를 가져옴
- 각 오류 코드에 대해 예시(ExampleHolder 객체)를 생성함
- 오류 코드에 해당하는 HTTP 상태 코드별로 그룹화
- 이 정보를 ApiResponse 객체에 추가하여, Swagger 문서에 해당 오류 응답 예시가 포함시킴
- API 문서에서 API가 반화할 수 있는 응답( ex) 200 OK, 404 Not Found)인 ApiResponse 객체를 정의하여 사용한다는 의미
addExamplesToResponse 메서드
private void addExamplesToResponses(ApiResponses responses,
Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {
statusWithExampleHolders.forEach(
(status, v) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();
v.forEach(
exampleHolder -> mediaType.addExamples(
exampleHolder.getName(),
exampleHolder.getHolder()
)
);
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
responses.addApiResponse(String.valueOf(status), apiResponse);
}
);
}
addExamplesToResponses 메서드는 ExampleHolder 객체를 사용하여 추가적으로 커스텀된 ApiResponses를 생성하고, 이를 Swagger 문서에서 사용될 ApiResponse에 추가합니다. 이때, ApiResponse는 실제 HTTP 응답이 아닌, Swagger API 문서에 각 API가 반환할 수 있는 응답 예시를 정의하는 객체입니다.
getSwaggerExample 메서드
private Example getSwaggerExample(Enum<?> errorCodeEnum) {
// Enum을 ErrorCode로 캐스팅
ErrorCode errorCode = (ErrorCode) errorCodeEnum;
// ResponseError 객체 생성
ResponseError responseError = ResponseError.builder()
.httpStatusCode(errorCode.getHttpStatus())
.errorMessage(errorCode.getErrorMessage())
.build();
Example example =new Example();
example.setValue(responseError);
return example;
}
getSwaggerExample 메서드는 ErrorCode enum 값을 받아서, 그에 대한 예시 응답 객체를 만듭니다.
그러면 아래와 같이 Swagger 문서에 오류 응답 예시를 생성할 수 있습니다.
결과적으로 아래와 같이 에러 코드 문서화를 할 수 있습니다
학습을 위해 작성된 글입니다. 잘못된 내용이 발견되면 즉각 수정하겠습니다
감사합니다
출처 :
https://inpa.tistory.com/entry/JAVA-☕-누구나-쉽게-배우는-Reflection-API-사용법#reflection_api_기법
[스프링] spring swagger 같은 코드 여러 에러 응답 예시 만들기
[스프링] error code 도메인 별 분리하기 두둥 프로젝트에서는 처리중에 에러가 발생할경우 RuntimeException 을 상속받은 DuDoongException 에서 다시 상속받아서 코드별 에러클래스를 만들고 있다. @Getter @A
devnm.tistory.com
https://leeeeeyeon-dev.tistory.com/92
[Packy] Swagger @ApiResponses를 커스텀 어노테이션으로 대체하기
1. @ApiResponses의 한계 Swagger에서 정상 응답값과 함께 에러 응답값도 명시해주어야 클라이언트 단에서도 에러 응답값에 대한 예외 처리를 할 수 있다. Swagger에서는 기본적으로 아래와 같이 @ApiRespon
leeeeeyeon-dev.tistory.com