개발일기
[Swagger] 스웨거 커스텀하여 @ApiResponses 커스텀 어노테이션으로 대체하기 본문
[Swagger] 스웨거 커스텀하여 @ApiResponses 커스텀 어노테이션으로 대체하기
한둥둥 2025. 1. 10. 00:04@ApiResponses을 커스텀 어노테이션을 사용하기로 결심한 이유
1. responseCode, description 을 수작업으로 반복적으로 모든 클래스에서 적용해야 한다.
2. @ApiResponse의 코드가 길다 보니 중요 코드가 눈에 확 들어오지 않는다.
예시 코드)
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "인증", description = "인증 관련 API")
public class AuthMailController {
@Operation(summary = "이메일 인증 코드 전송", description = "사용자의 이메일로 인증 코드를 전송합니다.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "성공적으로 인증 코드가 전송됨"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
@PostMapping("/email")
public ResponseEntity<ApiResponse<String>> sendEmail(
@Parameter(description = "인증 요청 데이터", required = true)
@RequestBody AuthMailRequest request) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success("인증 코드 전송 성공"));
}
}
예시이다. 이런식으로 많이 입력을 해주어야 한다.
그러다보니, 불편함을 느낄 수 있다. 나는 이런 문제를 해결 해주기 위하여 어노테이션을 통한 Swagger @ApiResponse를 개편해 주려고 한다.
어노테이션 클래스들
SccessResponseAnnotation 클래스
package com.realworld.common.swagger;
import com.realworld.common.response.code.SuccessResponseCode;
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 SuccessResponseAnnotation {
SuccessResponseCode value() default SuccessResponseCode.SUCCESS;
}
@Target(ElementType.METHOD) : 해당 어노테이션은 메서드에서만 적용 가능하도록 합니다.
@Retention(RetentionPolicy.RUNTIME) : 이 어노테이션은 런타입 동안 유지되도록 설정합니다.
package com.realworld.common.swagger;
import com.realworld.common.response.code.ExceptionResponseCode;
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 ExceptionResponseAnnotations {
ExceptionResponseCode[] value();
}
위에 와 동일합니다. value()메서드를 호출하면 ExceptionResponseCode 배열을 가져옵니다.
package com.realworld.common.response.code;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ExceptionResponseCode {
// 비밀번호 변경에 실패한 경우
FAIL_PASSWORD_CHANGE(HttpStatus.BAD_REQUEST, "패스워드 변경에 실패하였습니다.", 400),
// 유저 아이디가 존재하지 않은 경우
NOT_EXISTS_USERID(HttpStatus.BAD_REQUEST, "존재하지 않는 아이디입니다.", 400),
// 비밀번호가 올바르지 않습니다.
NOT_EQUAL_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다.", 400),
// 아이디
VALIDATION_USERID_ERROR(HttpStatus.BAD_REQUEST, "아이디 형식이 올바르지 않습니다.", 400),
// 존재하지 않는 이메일
NOT_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "존재하지 않는 이메일입니다.", 400),
// 이메일 인증 중복
EMAIL_DUPLICATION_ERROR(HttpStatus.BAD_REQUEST, "이메일이 중복되었습니다.", 400),
// 이메일 인증 코드 오류
EMAIL_AUTH_NUMBER_ERROR(HttpStatus.BAD_REQUEST, "이메일 인증코드를 잘못 입력하였습니다. 다시 시도해 주세요.", 400),
// 인증 시간 만료
EMAIL_EXPIRED_ERROR(HttpStatus.UNAUTHORIZED, "이메일 인증 시간이 만료되었습니다.", 401),
// 잘못된 이메일 요청
EMAIL_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "잘못된 요청입니다. 이메일 인증을 다시 시도해주세요.", 400),
// 사용자 잘못된 요청
LOGIN_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "비밀번호 또는 아이디가 올바르지 않습니다.", 400),
// 중복되는 아이디 이메일
LOGIN_DUPLICATION_ERROR(HttpStatus.CONFLICT, "이미 존재하는 아이디 또는 이메일 입니다.", 409),
// TOKEN 만료
JWT_TOKEN_EXPIRED_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다.", 401),
// 변조된 토큰
JWT_WRONG_TYPE_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "변조된 토큰입니다.", 401),
// Token 오류
JWT_TOKEN_REQUEST_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다.", 401),
// 토큰 오류
JWT_UNKNOWN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다.", 401),
// 변조된 토큰
UNSUPPORTED_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "변조된 토큰입니다.", 401),
// 패스워드 오류
PASSWORD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "패스워드 형식이 올바르지 않습니다.", 400),
// 포토카드 게시물 조회 오류
NOT_EXISTS_PRODUCT(HttpStatus.BAD_REQUEST, "포토카드가 존재하지 않습니다.", 400),
// 포토카드 사용자 불일치 오류
NOT_MATCHES_USER_PRODUCT(HttpStatus.BAD_REQUEST, "글을 작성한 유저가 아닙니다.", 400),
// 임시 저장 포토카드 조회 오류
NOT_EXISTS_TEMPORARILY_PRODUCT(HttpStatus.BAD_REQUEST, "임시 저장 포토 카드가 존재하지 않습니다.", 400),
// 잘못된 서버 요청
BAD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "Bad Request Exception", 400),
// @RequestBody 데이터 미 존재
REQUEST_BODY_MISSING_ERROR(HttpStatus.BAD_REQUEST, "Required request body is missing", 400),
// 유효하지 않은 타입
INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "Invalid Type Value", 400),
// Request Parameter 로 데이터가 전달되지 않을 경우
MISSING_REQUEST_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "Missing Servlet RequestParameter Exception", 400),
// 입력/출력 값이 유효하지 않음
IO_ERROR(HttpStatus.BAD_REQUEST, "I/O Exception", 400),
// JSON 파싱 실패
JSON_PARSE_ERROR(HttpStatus.BAD_REQUEST, "JsonParseException", 400),
// jackson.core processing error
JACKSON_PROCESS_ERROR(HttpStatus.BAD_REQUEST, "com.fasterxml.jackson.core Exception", 400),
// 권한이 없음
FORBIDDEN_ERROR(HttpStatus.FORBIDDEN, "Forbidden Exception", 403),
// 서버로 요청한 리소스가 존재하지 않음
NOT_FOUND_ERROR(HttpStatus.FORBIDDEN, "Not Found Exception", 403),
// NULL Point Exception 발생
NULL_POINT_ERROR(HttpStatus.FORBIDDEN, "Null Point Exception", 403),
// @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
NOT_VALID_ERROR(HttpStatus.FORBIDDEN, "handle Validation Exception", 403),
// @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
NOT_VALID_HEADER_ERROR(HttpStatus.FORBIDDEN, "Header에 데이터가 존재하지 않는 경우", 403),
// 서버가 처리 할 방법을 모르는 경우 발생
INTERVAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error Exception", 500),
// Transaction Insert Error
INSERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Insert Transaction Error Exception", 500),
// Transaction Update Error
UPDATE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Update Transaction Error Exception", 500),
// Transaction Delete Error
DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Delete Transaction Error Exception", 500);
private final HttpStatus httpStatus;
private final String message;
private final int resultCode;
}
필드)
- HttpStatus httpStatus: HTTP 응답 상태를 나타내며, 예외 상황에 적절한 HTTP 상태 코드 지정
- String message : 예외 상황에 대한 메시지를 담고 있어 클라이언트가 에러의 원인을 이해할 수 있도록 설명
- int resultCode: API의 응답 결과를 표현하는 코드 일반적으로 비즈니스 로직에서 에러를 구분하거나 클라이언트에게 더 상세한 정보를 제공하기 위해 사용한다.
SwaggerConfigV3 클래스
package com.realworld.common.swagger;
import com.realworld.common.response.SuccessResponse;
import com.realworld.common.response.code.ExceptionResponseCode;
import com.realworld.common.response.code.SuccessResponseCode;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.SneakyThrows;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;
@OpenAPIDefinition(
info = @Info(
title = "capo documentation",
version = "1.0",
description = "연예인 포토카드 api 스웨거 명세서"
)
)
@SecurityScheme(
name = "Bearer Authentication",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT"
)
@Configuration
public class SwaggerConfigV3 {
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
// @SuccessResponseAnnotation 추가
SuccessResponseAnnotation successAnnotation = handlerMethod.getMethodAnnotation(SuccessResponseAnnotation.class);
// 응답 코드 설정
SuccessResponseCode successResponseCode = Optional.ofNullable(successAnnotation).map(SuccessResponseAnnotation::value).orElse(SuccessResponseCode.SUCCESS);
this.addResponseBodyWrapperSchema(operation, SuccessResponse.class, "result", successResponseCode);
ExceptionResponseAnnotations exceptionAnnotation = handlerMethod.getMethodAnnotation(ExceptionResponseAnnotations.class);
if (exceptionAnnotation != null) {
this.addExceptionResponses(operation, exceptionAnnotation);
}
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
SwaggerRequestBody swaggerRequestBody = parameter.getMethodAnnotation(SwaggerRequestBody.class);
if (swaggerRequestBody != null) {
RequestBody requestBody = new RequestBody()
.description(swaggerRequestBody.description())
.required(swaggerRequestBody.required());
Content content = getContent(swaggerRequestBody);
requestBody.setContent(content);
operation.setRequestBody(requestBody);
}
}
operation.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"));
return operation;
};
}
private Content getContent(SwaggerRequestBody swaggerRequestBody) {
Content content = new Content();
MediaType mediaType = new MediaType();
if (swaggerRequestBody.content().length > 0 && swaggerRequestBody.content()[0].schema() != null) {
// 커스텀 어노테이션의 첫 번째 content에서 schema 설정 가져오기
Schema<?> schema = new Schema<>();
Class<?> schemaImplementation = swaggerRequestBody.content()[0].schema().implementation();
schema.set$ref(schemaImplementation.getSimpleName());
mediaType.setSchema(schema);
}
content.addMediaType("application/json", mediaType);
return content;
}
@SneakyThrows
private <T> void addResponseBodyWrapperSchema(Operation operation, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
ApiResponses responses = operation.getResponses();
String responseCode = String.valueOf(successResponseCode.getHttpStatus().value());
if (!"200".equals(responseCode)) {
ApiResponse existingResponse = responses.get("200");
this.changeResponseCode(existingResponse, responses, responseCode);
}
ApiResponse response = responses.computeIfAbsent(String.valueOf(successResponseCode.getHttpStatus()), key -> new ApiResponse());
response.setDescription(successResponseCode.getMessage());
Content content = response.getContent();
if (content != null) {
content.keySet().forEach(key -> {
MediaType mediaType = content.get(key);
mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, successResponseCode));
});
}
}
private void changeResponseCode(ApiResponse existingResponse, ApiResponses responses, String responseCode) {
if (existingResponse != null) {
ApiResponse newResponse = new ApiResponse()
.description(existingResponse.getDescription())
.content(existingResponse.getContent());
responses.addApiResponse(responseCode, newResponse);
responses.remove("200");
}
}
@SneakyThrows
private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
Schema<T> wrapperSchema = new Schema<>();
T instance = type.getDeclaredConstructor().newInstance();
for (Field field : type.getDeclaredFields()) {
String fieldName = field.getName();
String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
Method getterMethod = type.getMethod(getterName);
Object value = getterMethod.invoke(instance);
Schema<?> fieldSchema = new Schema<>().example(value);
if (fieldName.equals("result")) {
fieldSchema = originalSchema;
} else if (fieldName.equals("resultCode")) {
fieldSchema.example(successResponseCode.getResultCode());
} else if (fieldName.equals("resultMsg")) {
fieldSchema.example(successResponseCode.getMessage());
} else if (fieldName.equals("httpStatus")) {
fieldSchema.example(successResponseCode.getHttpStatus());
}
wrapperSchema.addProperty(fieldName, fieldSchema);
}
wrapperSchema.addProperty(wrapFieldName, originalSchema);
return wrapperSchema;
}
private void addExceptionResponses(Operation operation, ExceptionResponseAnnotations exceptionResponseAnnotations) {
for (ExceptionResponseCode exceptionCode : exceptionResponseAnnotations.value()) {
this.addExceptionResponse(operation, exceptionCode);
}
}
private void addExceptionResponse(Operation operation, ExceptionResponseCode exceptionResponseCode) {
ApiResponses responses = operation.getResponses();
String responseCodeKey = String.valueOf(exceptionResponseCode.getHttpStatus().value());
ApiResponse response = new ApiResponse()
.description(exceptionResponseCode.getMessage());
Content content = new Content();
MediaType mediaType = new MediaType();
mediaType.setSchema(createExceptionSchema(exceptionResponseCode));
content.addMediaType("application/json", mediaType);
response.setContent(content);
responses.addApiResponse(responseCodeKey + "_" + exceptionResponseCode.name(), response);
}
private <T> Schema<T> createExceptionSchema(ExceptionResponseCode exceptionResponseCode) {
Schema<T> exceptionSchema = new Schema<>();
exceptionSchema.addProperty("httpStatus", new Schema<>().example(exceptionResponseCode.getHttpStatus().toString()));
exceptionSchema.addProperty("resultCode", new Schema<>().example(exceptionResponseCode.getResultCode()));
exceptionSchema.addProperty("resultMsg", new Schema<>().example(exceptionResponseCode.getMessage()));
return exceptionSchema;
}
}
해당 코드는 최대한 함수로 분리하여 코드를 구현하였다. 하지만 "200" 등 문자열 자체를 그냥 비교 해주는 부분이 조금 찝찝하다고 해야하나 그래서 고민을 하다가 해당 코드를 그대로 두는 방향으로 하기로 했다. 맨위에서 private static final 로 선언하기에는 너무 많은 정보가 있다고 판단하였다. 그렇다고 따로 Enum 타입을 만드는 것도 애매하다 판단하여 해당 방식으로 진행하였다. 맞는건지.. 아직 내 수준에서 고민하는 것에서는 결론이 안나오는거 같다.
함수를 만들면서 최대한 기능 별로 동작 별로 나눌려고 노력하였다.
클래스 레벨 애노테이션 @OpenApiDefinition
- openAPI 명세를 정의한다.
- title : API 문서 제목
- version API 버전.
- description: API에 대한 설명을 제공합니다.
@SecurityScheme
- 보안 스키마를 정의합니다.
- name: 스키마 이름 (Bearer Authentication)
- type: HTTP 스키마를 사용
- schme: bearer 인증 방식
- bearerFormat: JWT 토큰 형식
@Configuration
- Spring의 설정 클래스임을 나타냅니다.
operationCustomizer 메서드
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
// @SuccessResponseAnnotation 추가
SuccessResponseAnnotation successAnnotation = handlerMethod.getMethodAnnotation(SuccessResponseAnnotation.class);
// 응답 코드 설정
SuccessResponseCode successResponseCode = Optional.ofNullable(successAnnotation).map(SuccessResponseAnnotation::value).orElse(SuccessResponseCode.SUCCESS);
this.addResponseBodyWrapperSchema(operation, SuccessResponse.class, "result", successResponseCode);
ExceptionResponseAnnotations exceptionAnnotation = handlerMethod.getMethodAnnotation(ExceptionResponseAnnotations.class);
if (exceptionAnnotation != null) {
this.addExceptionResponses(operation, exceptionAnnotation);
}
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
SwaggerRequestBody swaggerRequestBody = parameter.getMethodAnnotation(SwaggerRequestBody.class);
if (swaggerRequestBody != null) {
RequestBody requestBody = new RequestBody()
.description(swaggerRequestBody.description())
.required(swaggerRequestBody.required());
Content content = getContent(swaggerRequestBody);
requestBody.setContent(content);
operation.setRequestBody(requestBody);
}
}
operation.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"));
return operation;
};
}
1. @SuccessResponseAnnotation 을 사용하여 성공 응답의 기본 구조를 정의
2. @ExceptionResponseAnnotation 을 사용하여 예외 응답 스키마를 문서화.
3. @SwaggerRequestBody 처리하여 요청 본문(Request Body)의 문서화를 지원
4. Bearer 인증(JWT) 사용한 보안 요구 사항 추가
addResponseBodyWrapperSchema
@SneakyThrows
private <T> void addResponseBodyWrapperSchema(Operation operation, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
ApiResponses responses = operation.getResponses();
String responseCode = String.valueOf(successResponseCode.getHttpStatus().value());
if (!"200".equals(responseCode)) {
ApiResponse existingResponse = responses.get("200");
this.changeResponseCode(existingResponse, responses, responseCode);
}
ApiResponse response = responses.computeIfAbsent(String.valueOf(successResponseCode.getHttpStatus()), key -> new ApiResponse());
response.setDescription(successResponseCode.getMessage());
Content content = response.getContent();
if (content != null) {
content.keySet().forEach(key -> {
MediaType mediaType = content.get(key);
mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, successResponseCode));
});
}
}
API 응답 본문을 래핑하여 통일된 스키마로 반환하도록 처리
응답 코드가 200이 아닌 경우, 기존에 200으로 처리되어있는 디폴트를 새응답 코드로 변경하여 감싸준다.
changeResponseCode
private void changeResponseCode(ApiResponse existingResponse, ApiResponses responses, String responseCode) {
if (existingResponse != null) {
ApiResponse newResponse = new ApiResponse()
.description(existingResponse.getDescription())
.content(existingResponse.getContent());
responses.addApiResponse(responseCode, newResponse);
responses.remove("200");
}
}
기존 응답 코드 200을 다른 응답 코드로 변경
wrapSchema
@SneakyThrows
private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
Schema<T> wrapperSchema = new Schema<>();
T instance = type.getDeclaredConstructor().newInstance();
for (Field field : type.getDeclaredFields()) {
String fieldName = field.getName();
String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
Method getterMethod = type.getMethod(getterName);
Object value = getterMethod.invoke(instance);
Schema<?> fieldSchema = new Schema<>().example(value);
if (fieldName.equals("result")) {
fieldSchema = originalSchema;
} else if (fieldName.equals("resultCode")) {
fieldSchema.example(successResponseCode.getResultCode());
} else if (fieldName.equals("resultMsg")) {
fieldSchema.example(successResponseCode.getMessage());
} else if (fieldName.equals("httpStatus")) {
fieldSchema.example(successResponseCode.getHttpStatus());
}
wrapperSchema.addProperty(fieldName, fieldSchema);
}
wrapperSchema.addProperty(wrapFieldName, originalSchema);
return wrapperSchema;
}
API 응답을 래핑하는 스키마를 생성
successResponse 같은 구조로 데이터를 감쌈.
동적으로 필드 이름과 값을 기반으로 스키마 생성
addExceptionResponses 및 addExceptionResponse
private void addExceptionResponses(Operation operation, ExceptionResponseAnnotations exceptionResponseAnnotations) {
for (ExceptionResponseCode exceptionCode : exceptionResponseAnnotations.value()) {
this.addExceptionResponse(operation, exceptionCode);
}
}
private void addExceptionResponse(Operation operation, ExceptionResponseCode exceptionResponseCode) {
ApiResponses responses = operation.getResponses();
String responseCodeKey = String.valueOf(exceptionResponseCode.getHttpStatus().value());
ApiResponse response = new ApiResponse()
.description(exceptionResponseCode.getMessage());
Content content = new Content();
MediaType mediaType = new MediaType();
mediaType.setSchema(createExceptionSchema(exceptionResponseCode));
content.addMediaType("application/json", mediaType);
response.setContent(content);
responses.addApiResponse(responseCodeKey + "_" + exceptionResponseCode.name(), response);
}
@ExceptionResponseAnnotations에 정의된 예외 응답 코드를 문서화
각 예외 응답은 HTTP 상태 코드와 메시지를 포함
createExceptionSchema
private <T> Schema<T> createExceptionSchema(ExceptionResponseCode exceptionResponseCode) {
Schema<T> exceptionSchema = new Schema<>();
exceptionSchema.addProperty("httpStatus", new Schema<>().example(exceptionResponseCode.getHttpStatus().toString()));
exceptionSchema.addProperty("resultCode", new Schema<>().example(exceptionResponseCode.getResultCode()));
exceptionSchema.addProperty("resultMsg", new Schema<>().example(exceptionResponseCode.getMessage()));
return exceptionSchema;
}
예외 응답의 스키마를 생성해준다.
각 필드(httpStatus, resultCode, resultMsg) 이 필드는 SuccesResponse<T>의 필드 값이다.
ExceptionResponse<T> 클래스
package com.realworld.common.response;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@Builder
@RequiredArgsConstructor
public class ExceptionResponse<T> {
private final HttpStatus httpStatus;
private final int resultCode;
private final T result;
private final String resultMsg;
}
예외처리시, 들어갈 필드 값
SuccessResponse<T> 클래스
package com.realworld.common.response;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class SuccessResponse<T> {
private final T result;
private final int resultCode;
private final HttpStatus httpStatus;
private final String resultMsg;
}
각각의 필드 값 존재
package com.realworld.web.auth.mail.controller;
import com.realworld.application.auth.mail.service.AuthMailService;
import com.realworld.common.response.SuccessResponse;
import com.realworld.common.response.code.ExceptionResponseCode;
import com.realworld.common.response.code.SuccessResponseCode;
import com.realworld.common.swagger.ExceptionResponseAnnotations;
import com.realworld.common.swagger.SuccessResponseAnnotation;
import com.realworld.common.swagger.SwaggerRequestBody;
import com.realworld.web.auth.mail.payload.request.AuthMailRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Tag(name= "auth", description="인증")
@RestController
@RequiredArgsConstructor
@RequestMapping("/v2/auth")
public class AuthController {
private final AuthMailService authMailService;
@Operation(
summary = "이메일 인증 메일 보내기",
description = "이메일 인증 메일 보내기 API"
)
@SuccessResponseAnnotation(SuccessResponseCode.CREATED)
@ExceptionResponseAnnotations({ExceptionResponseCode.EMAIL_REQUEST_ERROR})
@PostMapping(value="/email")
public ResponseEntity<SuccessResponse<Object>> authMail(@SwaggerRequestBody(description = "이메일 인증 보내기 요청 정보", required = true, content = @Content(schema = @Schema(implementation = AuthMailRequest.class))) @RequestBody AuthMailRequest request) {
authMailService.send(request);
return ResponseEntity.ok(new SuccessResponse<>(null, 201, HttpStatus.CREATED,"메일 전송 성공"));
}
}
이런식으로 진짜 간단하게 어노테이션으로 모든걸 해결 할 수 있게 되었다.
여담으로 @SwaggerRequestBody는 @RequestBody 스웨거를 사용하지 않기 위해 만든 어노테이션이다.
'photocard backend server 개발일기' 카테고리의 다른 글
[JPA] QueryDSL No property 'xxxxx' found for type 'xxxxx' (0) | 2025.01.14 |
---|---|
[DDD 모델링] 포토카드 도메인 주도 개발을 위한 회원 , 프로필 모델링 (2) | 2025.01.13 |
로컬에서 소나큐브 실행하기 (0) | 2025.01.09 |
[인증메일] 메일 구현 및 테스트 코드 작성 (0) | 2025.01.08 |
[Spring Boot] 캐시 + Spring Boot 인증메일 저장하기 (0) | 2025.01.06 |