지난 글에서는 error code를 도메인별로 분할하는 작업을 진행했었다.
이번 글에서는 위와 같이 클라이언트들이 에러에대해 보기 쉽도록 나열을 하는 방법을
공유하고자 한다.
목차
1. 문제점
2. 에러 코드를 일일히 적지 않고 어떻게 옮길 수 있을까.
3. 스웨거 타입 분석
4. 적용하기
4.1. 커스텀 어노테이션 생성
4.2. 커스텀 어노테이션 정보를 가져오기
4.3. 스웨거 예시 응답값 커스텀
1. 문제점
로직 처리중에 에러를 내뱉게되어서 400번대로 응답을 해주게 되면, 클라이언트도
해당 api 를 사용할 때 어떤 때 해당 오류가 나는것인지 알아야 될 때가 있다.
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "이전까지 회원가입을 하지 않았던 경우",
content = @Content(
schema = @Schema(implementation = AfterOauthResponse.class)))
@ApiResponse(
responseCode = "200",
description = "이미 회원가입을 했던 유저인 경우",
content = @Content(
schema = @Schema(implementation = AfterOauthResponse.class)))
})
그래서 별도로 스웨거로 문서화 하기 위해서 스웨거 앞에 위와같은 형식으로 값을 적어주게 된다.
여기서 매우 복잡아진다.
에러 코드를 enum 으로 기술을 했음에도,
우리는 별도로 문서작업을 또 해야한다.
또한 클라이언트가 주문 도메인에있는 에러 코드들 문서로 뽑아주세요~ 요청하는 순간..
눈물이 앞을 가린다. 계속 바뀔텐데 이중 삼중으로 해야한다..
그래서 필자가 이미 만들어놨던 에러코드 이넘값들을 클라이언트에게
스웨거로 보여주는 방식을 고민해 보았다.
2. 에러 코드를 일일히 적지 않고 어떻게 옮길 수 있을까.
위 글에선, 어노테이션과 리플렉션을 사용해서 스웨거로 보내질 정보인
Operation 객체를 적절히 커스텀하여, 스웨거 인증을 하나만 풀게끔 하는 방법을 공유했다.
마찬가지로, 우리는 어노테이션과 리플렉션을 사용해서
별도의 example/{domain} 형식의 api 를 만든뒤에
커스텀 어노테이션을 에러코드 정보와 함께 기술해서,
OperationCustomizer 에서 해당 커스텀 어노테이션이 붙은 api라면
Operation 정보에 응답 코드와 그 예시들을 보여줄 것이다.
3. 스웨거 타입 분석
우리가 커스텀 하게될 부분은
Responses 안에 있는 Response 객체이다.
공식 문서에 의하면 Responses 클래스 안에는 Response 클래스 형식을 가지는 필드들이있다.
// Operation 중 responses 필드
"responses": {
"200": { // Response
"description": "Pet updated.",
"content": {
"application/json": {},
"application/xml": {}
}
},
"405": { // Response
"description": "Method Not Allowed",
"content": {
"application/json": {},
"application/xml": {}
}
}
},
"200" : <Response>
"400" : <Response>
위와 같은 형식이다.
content 안에 application/json 형식안에 있는 객체는 Media Type Object 이다
// Media Type Object
{
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
},
"examples": {
"cat" : {
"summary": "An example of a cat",
"value":
{
"name": "Fluffy",
"petType": "Cat",
"color": "White",
"gender": "male",
"breed": "Persian"
}
},
"dog": {
"summary": "An example of a dog with a cat's name",
"value" : {
"name": "Puma",
"petType": "Dog",
"color": "Black",
"gender": "Female",
"breed": "Mixed"
}
}
}
}
}
Media Type Object 안에
examplse 안에는 Example Object 가 올수 있는데
우린 summary 와, value 값을 도메인별 ErrorCode enum 에서 가져와서 예시로 적어줄 것이다
사진을 보면 좀더 이해하기에 쉬울것같다.
4. 적용하기
4.1. 커스텀 어노테이션 생성
우선 커스텀 어노테이션을 만들어보자.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExample {
Class<? extends BaseErrorCode> value();
}
//ex
@ApiErrorCodeExample(UserErrorCode.class)
public void getUserErrorCode() {}
ApiErrorCodeExample 어노테이션을 만들고, 안에 value는 BassErrorCode 를 확장시킨 타입의 Class만 받도록 하였다.
적용시킨모습은 위의 예시 와 같다.
4.2. 커스텀 어노테이션 정보를 가져오기
//SwaggerConfig.java
@Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
ApiErrorCodeExample apiErrorCodeExample =
handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
// ApiErrorCodeExample 어노테이션 단 메소드 적용
if (apiErrorCodeExample != null) {
generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
}
return operation;
};
}
위처럼 리플랙션을 사용해서 해당메서드의 ApiErrorCodeExample 어노테이션이 달려있다면
스웨거 예시 응답값의 커스텀을 진행한다.
중요한점은 apiErrorCodeExample.value() 로 BaseErrorCode 타입의 예시를 가져올 수 있다는 점이다.
4.3. 스웨거 예시 응답값 커스텀
private void generateErrorCodeResponseExample(
Operation operation, Class<? extends BaseErrorCode> type) {
ApiResponses responses = operation.getResponses();
// 해당 이넘에 선언된 에러코드들의 목록을 가져옵니다.
BaseErrorCode[] errorCodes = type.getEnumConstants();
// 400, 401, 404 등 에러코드의 상태코드들로 리스트로 모읍니다.
// 400 같은 상태코드에 여러 에러코드들이 있을 수 있습니다.
Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
Arrays.stream(errorCodes)
.map(
baseErrorCode -> {
try {
ErrorReason errorReason = baseErrorCode.getErrorReason();
return ExampleHolder.builder()
.holder(
getSwaggerExample(
baseErrorCode.getExplainError(),
errorReason))
.code(errorReason.getStatus())
.name(errorReason.getCode())
.build();
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
})
.collect(groupingBy(ExampleHolder::getCode));
// response 객체들을 responses 에 넣습니다.
addExamplesToResponses(responses, statusWithExampleHolders);
}
//ExampleHolder
@Getter
@Builder
public class ExampleHolder {
// 스웨거의 Example 객체입니다. 위 스웨거 분석의 Example Object 참고.
private Example holder;
private String name;
private int code;
}
//
private Example getSwaggerExample(String value, ErrorReason errorReason) {
//ErrorResponse 는 클라이언트한 실제 응답하는 공통 에러 응답 객체입니다.
ErrorResponse errorResponse = new ErrorResponse(errorReason, "요청시 패스정보입니다.");
Example example = new Example();
example.description(value);
example.setValue(errorResponse);
return example;
}
위에서 스웨거의 응답값을 분석한대로.
400번대 상태코드에는 여러개의 Example Object 가 올수 있다.
private void addExamplesToResponses(
ApiResponses responses, Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {
statusWithExampleHolders.forEach(
(status, v) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
// 상태 코드마다 ApiResponse을 생성합니다.
ApiResponse apiResponse = new ApiResponse();
// List<ExampleHolder> 를 순회하며, mediaType 객체에 예시값을 추가합니다.
v.forEach(
exampleHolder -> mediaType.addExamples(
exampleHolder.getName(), exampleHolder.getHolder()));
// ApiResponse 의 content 에 mediaType을 추가합니다.
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
// 상태코드를 key 값으로 responses 에 추가합니다.
responses.addApiResponse(status.toString(), apiResponse);
});
}
위와같은 형식으로 상태코드를 기준으로 예시객체들을 모은뒤에,
ApiResponses 객체에 넣어주는, 구성으로 돌아간다.
더 복잡아질것같아서 뺀 이야기지만, 두둥에서는
해당 설명을 더 적어주고 싶을땐, ExplainError 라는 커스텀 어노테이션을 만들어서
위처럼 예시 설명을 적어두고 있다.
public enum UserErrorCode implements BaseErrorCode {
@ExplainError("회원가입시에 이미 회원가입한 유저일시 발생하는 오류. 회원가입전엔 항상 register valid check 를 해주세요")
USER_ALREADY_SIGNUP(BAD_REQUEST, "USER_400_1", "이미 회원가입한 유저입니다."),
@Override
public String getExplainError() throws NoSuchFieldException {
Field field = this.getClass().getField(this.name());
ExplainError annotation = field.getAnnotation(ExplainError.class);
return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
}
사실 소스보다 더 중요한것은 스웨거가 어떤 구조로 되어있는가를 파악해야 한다.
어느 백엔드 프레임워크를 쓰던, 스웨거는 공통의 json 형식을 받아서 렌더링을 해준다.
스프링이면, 스프링. nestjs면 nestjs 결국 해당 프레임워크에서
어떻게 json 형식을 만들어 주냐에 따라서 달라질 뿐이지.
view에서 받는 정보는 다 같다.
스웨거의 구조를 이해하시면 좀더 커스텀하기 편하실것 같다.
위 소스는 아래 레포지토리에서 참고 가능하다.
'스프링' 카테고리의 다른 글
[스프링] spring rate limit 적용히기 bucket4j (2) | 2023.03.06 |
---|---|
[스프링] spring 프록시 환경에서 HttpContentCache 적용 (0) | 2023.03.06 |
[스프링] 공통 응답 형식 만들기 ResponseBodyAdvice (0) | 2023.03.05 |
[스프링] error code 도메인 별 분리하기 (0) | 2023.03.05 |
[스프링] spring swagger api 하나만 인증 풀기 (2) | 2023.03.05 |