문제점
클라이언트는 에러가 날 때 어떤 오류가 나는지 상세히 알지 못한다.
프론트와의 원활한 협업을 위해 에러를 상세히 내려주자는 의견이 나왔다.
우리 프로젝트에선 에러코드를 Enum으로 정의하고 있었는데, 이 ErrorCode를 건드리지 않고 그대로 클라이언트에게 전송할 수 있을지가 이슈였다.
@Getter
@AllArgsConstructor
public enum ErrorCode {
UNKNOWN("G0001", "알 수 없는 오류가 발생했습니다."),
INVALID_ACCESS("G0002", "잘못된 접근입니다."),
DUPLICATED_EMAIL("G0003", "중복된 이메일이 이미 존재합니다."),
DUPLICATED_NAME("G0004", "중복된 이름이 이미 존재합니다." ),
INVALID_REQUEST("G0005","잘못된 요청입니다."),
REWARD_NOT_FOUND("R0001", "리워드 정보를 찾을 수 없습니다."),
STOCK_SETTING_ERROR("R0002", "재고는 양수여야 합니다."),
NOT_MATCH("R0003", "리워드의 프로젝트 아이디에 매치할 수 없습니다."),
FUNDING_NOT_FOUND("F0001", "펀딩 정보를 찾을 수 없습니다."),
INVALID_FUNDING_DURATION("F0002", "펀딩 종료 시간이 펀딩 시작 시간보다 앞설 수 없습니다."),
CANNOT_CREATE_FUNDING("F0003", "펀딩을 새로 생성할 수 없습니다."),
MAKER_NOT_FOUND("M0001","메이커 정보를 찾을 수 없습니다."),
SUPPORTER_NOT_FOUND("S0001","서포터 정보를 찾을 수 없습니다."),
PROJECT_NOT_FOUND("P0001", "프로젝트 정보를 찾을 수 없습니다."),
PROJECT_ACCESS_DENY("P0002", "프로젝트가 개설된 이후로는 접근할 수 없습니다."),
POST_NOT_FOUND("B0001", "게시글 정보를 찾을 수 없습니다."),
CANNOT_CREATE_POST("B0002", "게시글을 새로 생성할 수 없습니다."),
ORDER_COUNT_ERROR("O0001", "주문 수량은 1개 이상이어야 합니다."),
ORDER_NOT_FOUND("O0002","주문 정보를 찾을 수 없습니다." ),
ORDER_COUNT_EXCEED("O0003", "주문 수량이 재고 수량을 초과했습니다.");
private String code;
private String errorMessage;
}
어떻게?
커스텀 어노테이션을 만들어 스웨거로 보내질 정보인 Operation 객체를 커스텀하여 정보에 응답 코드와 예시를 보내줄 것이다.
스웨거 타입
Operation이란
"Operation"은 HTTP 요청 및 응답과 관련된 특정 작업이나 작업의 결과를 가리키며, HTTP 상태 코드는 이러한 작업의 결과를 전달하는 데 사용된다.
Response 객체
우리가 커스텀하게 될 부분은 Response 안에 있는 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": {}
}
}
}
이 Response 객체 안에는 다음과 같이 application/json 이 있는데
// 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"
}
}
}
}
}
application/json 안에는 examples가 들어있고, 우리는 해당 example 객체들을 도메인 별 에러코드를 담게 할 것이다.
그리고 해당 에러코드들 (하나의 example) 의 summary와 value 값들을 우리가 정의한 ErrorCode enum에서 값을 가져와 넣어줄 것이다.
프로젝트에 적용
1. 커스텀 어노테이션 생성
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExample {
Class<ErrorCode> value();
String domain();
}
우리 프로젝트에선 에러 코드가 도메인 별 에러코드, 에러 내용을 담고 있어 두가지의 정보를 모두 내려주기 위해 domain() 과 value() 를 추가하였다.
2. 커스텀 어노테이션 정보를 가져오기
우리 프로젝트에선 스웨거 3.x 대를 사용하고 있어 별도의 swagger config 파일이 필요하지 않았지만,
스웨거 예시 응답값 커스텀을 작업하기 위해 SwaggerConfig.class 파일을 추가하였다. github에 SwaggerConfig 클래스 전체 코드가 스웨거 예시 응답값 커스텀 작업을 위해 작성한 코드이니 그대로 참고해도 무방하다.
@Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
ApiErrorCodeExample apiErrorCodeExample =
handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
// ApiErrorCodeExample 어노테이션 단 메소드 적용
if (apiErrorCodeExample != null) {
generateErrorCodeResponseExample(operation, apiErrorCodeExample.value(), apiErrorCodeExample.domain());
}
우리가 1단계에서 만든 ApiErrorExample 어노테이션이 달려있는 메서드의 스웨어 예시 응답 커스텀이 진행된다. generateErrorCodeResponseExample(operation, apiErrorCodeExample.value(), apiErrorCodeExample.domain()); : 에선 본격적인 스웨거 예시 응답값을 내려주는 메서드이다.
3. 스웨거 예시 응답값 커스텀
ExampleHolder 클래스는 스웨거의 Example 객체를 관리하기 위한 데이터 구조이다.
@Getter
@Builder
public class ExampleHolder {
private Integer statusCode;
// 스웨거의 Example 객체
private Example holder;
private String errorCode;
private String errorMessage;
}
private void generateErrorCodeResponseExample(
Operation operation, Class<ErrorCode> type, String domain) {
ApiResponses responses = operation.getResponses();
// 해당 이넘에 선언된 에러코드들의 목록을 가져온다.
ErrorCode[] errorCodes = type.getEnumConstants();
// 400, 401, 404 등 에러코드의 상태코드들로 리스트로 모은다.
// 400 같은 상태코드에 여러 에러코드들이 있을 수 있다.
Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
Arrays.stream(errorCodes)
.filter(errorCode -> errorCode.getCode().substring(0, 1).equals(domain.substring(0, 1)))
.map(
errorCode -> {
return ExampleHolder.builder()
.statusCode(404)
.holder(getSwaggerExample(errorCode))
.errorCode((errorCode.getCode()))
.errorMessage(errorCode.getErrorMessage())
.build();
})
.collect(groupingBy(ExampleHolder::getStatusCode));
// response 객체들을 responses 에 넣는다.
addExamplesToResponses(responses, statusWithExampleHolders);
}
errorcode를 필터링할때 에러 코드의 첫글자가 도메인의 첫글자와 일치하는 에러들이 필터링되게 하였다. generateErrorCodeResponseExample 메서드는 주어진 operation, type, domain에 대해 스웨거 응답 값을 생성한다.
- 주어진 type에서 에러 코드 목록을 가져온다.
- errorcode의 첫글자가 도메인의 첫글자와 일치하는 에러들이 필터링된다.
- ExampleHolder 객체를 생성하고 스웨거 예시를 생성하여 holder 속성에 할당합니다.
- 생성된 ExampleHolder 객체들을 HTTP 상태 코드별로 그룹화하여 statusWithExampleHolders 맵에 저장한다.
- 최종적으로 addExamplesToResponses 함수를 사용하여 스웨거 응답에 예시를 추가한다.
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.getErrorCode(), exampleHolder.getHolder()));
// ApiResponse 의 content 에 mediaType을 추가합니다.
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
// 상태코드를 key 값으로 responses 에 추가합니다.
responses.addApiResponse(status.toString(), apiResponse);
});
}
4. 커스텀한 어노테이션을 붙여보자 !
/**
* Funding 단건 조회
*/
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "펀딩 조회 성공"
),
@ApiResponse(
responseCode = "404",
description = "펀딩 조회 실패"
)
})
@ApiErrorCodeExample(
value = ErrorCode.class,
domain = "Funding"
)
@Operation(
summary = "펀딩 조회",
description = "프로젝트 id를 이용하여 펀딩을 조회한다."
)
@GetMapping("/{projectId}/fundings")
public ResponseEntity<ResponseTemplate> getFunding(
@Parameter(description = "프로젝트 id") @PathVariable Long projectId
) {
FundingResponseDTO fundingResponseDTO = projectUseCase.getFunding(projectId);
return ResponseEntity.ok(ResponseFactory.getSingleResult(fundingResponseDTO));
}
결과
이제 클라이언트에서도 상세한 에러코드를 확인할 수 있다 !
References
https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#response-object https://devnm.tistory.com/29
'개발 일지 > 프로젝트' 카테고리의 다른 글
인덱싱을 적용한 tps 속도 개선 (0) | 2024.02.06 |
---|---|
멀티모듈 적용기 (의존성, POJO 분리까지) (0) | 2024.02.04 |
JDBC Batch insert (0) | 2023.11.15 |