💎 작성된 글의 프로젝트
https://github.com/MARU-EGG/MARU_EGG_BE
💎 작성된 글의 Pull Request
https://github.com/MARU-EGG/MARU_EGG_BE/pull/42
들어가기 전에..
프로젝트를 진행하면서 API 문서를 swagger로 작업하고 있었습니다.
API 마다 각각의 성공, 실패 Response가 존재하게 됩니다. 이때, Swagger에서는 성공의 경우 DTO에 @Schema
를 통해 쉽게 example을 설정할 수 있지만, 예외 Response의 경우 Controller에 content를 모두 입력하고 구현된 스키마까지 작성해야하기에 이를 개선해보고 합니다.
목표는 @CustomApiResponse
와 @CustomApiResponses
를 커스텀 어노테이션으로 정의함으로써, 코드의 가독성을 높이고 유지 보수를 용이하는 것입니다.
이 글에서는 @CustomApiResponses
와 @CustomApiResponse
어노테이션의 작동 방식과 기능, 사용법에 대해 다룹니다.
최종적으로 적용된 코드
적용 전
@Tag(name = "Question API", description = "질문 관련 API 입니다.")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/questions")
public class QuestionController {
private final QuestionService questionService;
@Operation(summary = "질문 요청", description = "질문하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "질문 성공"),
@ApiResponse(responseCode = "400", description = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
}),
@ApiResponse(responseCode = "404", description = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
}),
@ApiResponse(responseCode = "500", description = "내부 서버 오류가 발생했습니다.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
})
})
@PostMapping()
public QuestionResponse question(@Valid @RequestBody QuestionRequest request) {
return questionService.question(request.type(), request.category(), request.content());
}
@Operation(summary = "질문 목록 요청", description = "질문 목록을 보내주는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "질문 성공"),
@ApiResponse(responseCode = "400", description = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
}),
@ApiResponse(responseCode = "500", description = "내부 서버 오류가 발생했습니다.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
})
})
@GetMapping()
public List<QuestionResponse> getQuestions(@Valid @ModelAttribute FindQuestionsRequest request) {
return questionService.getQuestions(request.type(), request.category());
}
}
적용 후
@Tag(name = "Question API", description = "질문 관련 API 입니다.")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/questions")
public class QuestionController {
private final QuestionService questionService;
@Operation(summary = "질문 요청", description = "질문하는 API", responses = {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", description = "질문 또는 답변을 찾지 못한 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
@PostMapping()
public QuestionResponse question(@Valid @RequestBody QuestionRequest request) {
return questionService.question(request.type(), request.category(), request.content());
}
@Operation(summary = "질문 목록 요청", description = "질문 목록을 보내주는 API", responses = {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
@GetMapping()
public List<QuestionResponse> getQuestions(@Valid @ModelAttribute FindQuestionsRequest request) {
return questionService.getQuestions(request.type(), request.category());
}
}
어노테이션 정의
@CustomApiResponse
@CustomApiResponse
은 개별 API 응답을 정의하는 데 사용됩니다. 각 응답에는 오류 유형, 상태 코드, 메시지, 설명이 포함됩니다.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(CustomApiResponses.class)
public @interface CustomApiResponse {
String error() default "RuntimeException";
int status() default 500;
String message() default "서버에 오류가 발생했습니다. 담당자에게 연락주세요.";
String description() default "내부 서버 오류";
}
@Target({ElementType.METHOD})
- 어노테이션 적용 타입
- ElementType.METHOD: 어노테이션이 메소드에만 적용될 수 있음을 의미합니다.
- controller의 메소드 단위로 적용할 예정입니다.
- 추가로, TYPE (클래스, 인터페이스, enum), FIELD (필드), CONSTRUCTOR (생성자) 등이 있습니다.
@Retention(RetentionPolicy.RUNTIME)
- 어노테이션의 보존 정책
- RetentionPolicy.RUNTIME: 어노테이션이 런타임 동안 유지되어야 하며, 리플렉션을 통해 접근할 수 있음을 의미합니다.
- 리플렉션(Reflection): 런타임 시에 프로그램의 구성을 검사하고 수정할 수 있는 기능을 의미
- 참고 자료: https://hudi.blog/java-reflection/
- 추가로, SOURCE (컴파일 타임에만 유지, 컴파일 후 버려짐), CLASS (컴파일러가 클래스 파일에 기록하지만 JVM에 의해 로드되지 않음) 등이 있습니다.
@Documented
- Javadoc 같은 도구에 의해 문서화되어야 함을 의미
@Repeatable(CustomApiResponses.class)
- 이 어노테이션이 동일한 요소에 여러 번 사용할 수 있음
- CustomApiResponses.class: 반복되는 어노테이션을 감싸는 컨테이너 어노테이션을 지정
각 속성의 의미
// API 응답에서 발생할 수 있는 오류의 유형을 지정
String error() default "RuntimeException";
// HTTP 상태 코드를 지정
int status() default 500;
// 오류 메시지를 지정
String message() default "서버에 오류가 발생했습니다. 담당자에게 연락주세요.";
// 오류에 대한 설명을 지정
String description() default "내부 서버 오류";
@CustomApiResponses
@CustomApiResponses
은 여러 @CustomApiResponse
를 그룹화하는 역할을 수행합니다.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomApiResponses {
CustomApiResponse[] value();
}
적용 방법
전체 적용된 코드
@Operation(summary = "질문 요청", description = "질문하는 API", responses = {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", description = "질문 또는 답변을 찾지 못한 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
@PostMapping()
public QuestionResponse question(@Valid @RequestBody QuestionRequest request) {
return questionService.question(request.type(), request.category(), request.content());
}
성공인 경우
@Operation(summary = "질문 요청", description = "질문하는 API", responses = {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
Response DTO 클래스 예시
@Builder
@Schema(description = "질문 응답 DTO", example = """
{
"id": 600697396846981500,
"content": "수시 일정 알려주세요."
"dateInformation": "생성일자: 2024-07-15T23:36:59.834804, 마지막 DB 갱신일자: 2024-07-15T23:36:59.834804",
"answer": {
"id": 600697396935061600,
"content": "2024년 수시 일정은 다음과 같습니다:\\n\\n- 전체 전형 2024.12.19.(목)~12.26.(목) 18:00: 최초합격자 발표 및 시충원 관련 내용 공지 예정\\n- 문서등록 및 등록금 납부 기간: 2025. 2. 10.(월) 10:00 ~ 2. 12.(수) 15:00\\n- 등록금 납부 기간: 2024.12.16.(월) 10:00 ~ 12. 18.(수) 15:00\\n\\n추가로, 복수지원 관련하여 수시모집의 모든 전형에 중복 지원이 가능하며, 최대 6개 이내의 전형에만 지원할 수 있습니다. 반드시 지정 기간 내에 문서등록과 최종 등록(등록금 납부)을 해야 합니다. 또한, 합격자는 합격한 대학에 등록하지 않을 경우 합격 포기로 간주되니 유의하시기 바랍니다.",
"renewalYear": 2024,
"dateInformation": "생성일자: 2024-07-15T23:36:59.847690, 마지막 DB 갱신일자: 2024-07-15T23:36:59.847690"
}
}
""")
public record QuestionResponse(
@Schema(description = "질문 id")
Long id,
@Schema(description = "질문")
String content,
@Schema(description = "답변 DB 생성 및 업데이트 날짜")
String dateInformation,
@Schema(description = "답변 DTO")
AnswerResponse answer
) {
public static QuestionResponse of(Question question, AnswerResponse answer) {
return QuestionResponse.builder()
.id(question.getId())
.content(question.getContent())
.dateInformation(question.getDateInformation())
.answer(answer)
.build();
}
}
예외에 Response에 적용
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", description = "질문 또는 답변을 찾지 못한 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
SwaggerConfig 설정
OperationCustomizer 빈 등록
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
ApiResponses apiResponses = operation.getResponses();
if (apiResponses == null) {
apiResponses = new ApiResponses();
operation.setResponses(apiResponses);
}
handleCustomApiResponses(apiResponses, handlerMethod);
return operation;
};
}
- OperationCustomizer는 Swagger(OpenAPI) 문서에서 각 API 엔드포인트의 동작(Operation)을 사용자 정의할 수 있게 합니다.
- 여기서는 API 메서드의 응답(ApiResponses)을 가져와서, 만약 응답이 정의되어 있지 않다면 새로운 ApiResponses 객체를 생성합니다.
- handleCustomApiResponses 메서드를 호출하여 사용자 정의 응답을 설정합니다.
handleCustomApiResponses 메서드
private void handleCustomApiResponses(ApiResponses apiResponses, HandlerMethod handlerMethod) {
Method method = handlerMethod.getMethod();
CustomApiResponses customApiResponses = method.getAnnotation(CustomApiResponses.class);
if (customApiResponses != null) {
for (CustomApiResponse customApiResponse : customApiResponses.value()) {
ApiResponse apiResponse = new ApiResponse();
apiResponse.setDescription(customApiResponse.description());
addContent(apiResponse, customApiResponse.error(), customApiResponse.status(), customApiResponse.message());
apiResponses.addApiResponse(String.valueOf(customApiResponse.status()), apiResponse);
}
} else {
CustomApiResponse customApiResponse = method.getAnnotation(CustomApiResponse.class);
if (customApiResponse != null) {
ApiResponse apiResponse = new ApiResponse();
apiResponse.setDescription(customApiResponse.description());
addContent(apiResponse, customApiResponse.error(), customApiResponse.status(), customApiResponse.message());
apiResponses.addApiResponse(String.valueOf(customApiResponse.status()), apiResponse);
}
}
}
- handlerMethod로부터 메서드를 가져와서, 해당 메서드에 CustomApiResponses 어노테이션이 있는지 확인합니다.
- CustomApiResponses 어노테이션이 있으면, 그 안에 포함된 여러 CustomApiResponse 어노테이션을 반복하면서 각각의 사용자 정의 응답을 생성합니다.
- CustomApiResponse 어노테이션이 개별적으로 설정된 경우도 처리하여 사용자 정의 응답을 생성합니다.
addContent 메서드
private void addContent(ApiResponse apiResponse, String error, int status, String message) {
Content content = new Content();
MediaType mediaType = new MediaType();
Schema<ErrorResponse> schema = new Schema<>();
schema.$ref("#/components/schemas/ErrorResponse");
mediaType.schema(schema)
.example(new ErrorResponse(error, status, message));
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
}
- ApiResponse 객체에 응답의 내용을 추가합니다.
- ErrorResponse 스키마를 참조하여 JSON 형태로 응답의 예제를 설정합니다.
위 과정을 통해 얻은 Swagger 문서 형태
아래와 같이 전체 내용을 포함한 최종 문서를 작성했습니다:
Swagger 코드 분리 시도
Swagger API 문서를 관리하는 코드를 인터페이스와 구현 클래스로 분리하여 코드 가독성을 높이고자 했습니다. 이에 따라 @CustomApiResponses
와 같은 커스텀 어노테이션을 인터페이스에 정의하고, 이를 상속받는 구현 클래스에서 적용하려고 했습니다. 다음은 해당 구조에 대한 예시입니다:
@Tag(name = "Place API", description = "장소 관련 API")
public interface PlaceAPIPresentation {
@Operation(summary = "장소 등록", description = "새로운 장소를 등록하는 API", responses = {
@ApiResponse(responseCode = "200", description = "장소 등록 성공"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 입력 값")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Validation failed for argument [0] in public void org.findy.findy_be.place.api.PlaceController.registerPlace(org.findy.findy_be.place.dto.request.PlaceRequest) with 7 errors: [Field error in object 'placeRequest' on field 'link': rejected value [null]; codes [NotNull.placeRequest.link,NotNull.link,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [placeRequest.link,link]; arguments []; default message [link]]; default message [널이어서는 안됩니다]] [Field error in object 'placeRequest' on field 'address': rejected value [null]; codes [NotNull.placeRequest.address,NotNull.address,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [placeRequest.address,address]; arguments []; default message [address]]; default message [널이어서는 안됩니다]]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "해당 id : 1의 즐겨찾기가 존재하지 않습니다.", description = "장소를 저장할 즐겨찾기를 못 찾는 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
void registerPlace(@Valid @RequestBody PlaceRequest request);
}
@RestController
@RequestMapping("/api/places")
@RequiredArgsConstructor
public class PlaceController implements PlaceAPIPresentation {
private final RegisterPlace registerPlace;
@PostMapping()
public void registerPlace(@Valid @RequestBody PlaceRequest request) {
registerPlace.invoke(request);
}
}
문제 발생
인터페이스에 @CustomApiResponses
와 같은 커스텀 어노테이션을 정의하고 구현 클래스에서 이를 상속받아 코드를 간결하게 유지하려 했으나, 구현 클래스에서 인터페이스의 커스텀 어노테이션을 인식하지 못하는 문제가 발생했습니다.
어노테이션의 보존 정책을 RetentionPolicy.RUNTIME
으로 설정했기 때문에 리플렉션을 통해 적용될 것이라 생각했지만, Swagger 설정에서 이를 별도로 처리하지 않으면 자동으로 적용되지 않는 한계가 있었습니다. 이는 자바의 리플렉션과 Spring AOP의 특성 때문으로, 기본적으로 인터페이스에 정의된 어노테이션은 구현 클래스에서 읽히지 않기 때문입니다.
이 문제로 인해 구현 클래스에서 인터페이스에 정의된 @CustomApiResponses
가 인식되지 않아, 인터페이스와 구현 클래스 양쪽에 중복 설정을 할 수밖에 없는 상황이었습니다.
리플렉션(Reflection)이란?
리플렉션(Reflection)이란 런타임에 클래스의 구조(메서드, 필드, 생성자 등)를 동적으로 탐색하고 조작할 수 있게 해줍니다. 리플렉션을 통해 컴파일 시점에 타입을 알 수 없는 객체를 조작할 수 있으며, 메서드를 호출하거나 필드 값을 변경하는 등의 작업을 수행할 수 있습니다.
예를 들어, Spring에서 빈을 동적으로 주입하거나 Jackson에서 JSON 직렬화에 활용됩니다.
해결 방법
이 문제를 해결하기 위해 Swagger 설정의 OperationCustomizer
에서 리플렉션을 활용하여 인터페이스의 어노테이션을 직접 읽어오는 방식을 적용했습니다. OperationCustomizer
를 사용해 인터페이스 메서드의 @CustomApiResponses
를 수동으로 탐색하여 구현 클래스에서도 해당 어노테이션이 적용되도록 수정했습니다.
리플렉션을 이용한 인터페이스 어노테이션 검색
아래는 SwaggerConfig
클래스의 handleCustomApiResponses
메서드에 리플렉션을 통해 인터페이스 메서드의 어노테이션을 탐색하고 적용하는 로직을 추가한 예시입니다.
수정된 코드
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
ApiResponses apiResponses = operation.getResponses();
if (apiResponses == null) {
apiResponses = new ApiResponses();
operation.setResponses(apiResponses);
}
handleCustomApiResponses(apiResponses, handlerMethod);
return operation;
};
}
private void handleCustomApiResponses(ApiResponses apiResponses, HandlerMethod handlerMethod) {
Method method = handlerMethod.getMethod();
// 1. 우선 구현 클래스에서 어노테이션을 찾습니다.
CustomApiResponses customApiResponses = method.getAnnotation(CustomApiResponses.class);
// 2. 없다면 인터페이스에서 어노테이션을 찾습니다.
if (customApiResponses == null) {
for (Class<?> customApiInterface : handlerMethod.getBeanType().getInterfaces()) {
try {
Method interfaceMethod = customApiInterface.getMethod(method.getName(), method.getParameterTypes());
customApiResponses = interfaceMethod.getAnnotation(CustomApiResponses.class);
if (customApiResponses != null) {
break;
}
} catch (NoSuchMethodException e) {
// 인터페이스에 해당 메서드가 없는 경우 무시
}
}
}
// 3. 찾아낸 customApiResponses를 ApiResponses에 추가
if (customApiResponses != null) {
for (CustomApiResponse customApiResponse : customApiResponses.value()) {
ApiResponse apiResponse = new ApiResponse();
apiResponse.setDescription(customApiResponse.description());
addContent(apiResponse, customApiResponse.error(), customApiResponse.status(),
customApiResponse.message(), customApiResponse.isArray());
apiResponses.addApiResponse(String.valueOf(customApiResponse.status()), apiResponse);
}
}
}
코드 변경 설명
handleCustomApiResponses
메서드:- 구현 클래스의 메서드에서
@CustomApiResponses
어노테이션을 먼저 검색합니다. - 만약 구현 클래스에
@CustomApiResponses
가 없으면handlerMethod.getBeanType().getInterfaces()
를 통해 구현 클래스가 상속받는 모든 인터페이스를 순회하며 어노테이션을 찾습니다. - 인터페이스에 같은 이름과 파라미터를 가진 메서드가 있다면, 해당 메서드의
@CustomApiResponses
를 읽어옵니다.
- 구현 클래스의 메서드에서
- 리플렉션을 통한 인터페이스 어노테이션 검색:
Method interfaceMethod = customApiInterface.getMethod(method.getName(), method.getParameterTypes());
구문을 사용해, 메서드 이름과 파라미터가 같은 메서드를 인터페이스에서 찾고, 어노테이션을 읽어오는 방식으로 구현했습니다.
- Swagger의
ApiResponses
에 응답 추가:- 찾은
@CustomApiResponses
어노테이션을 이용해 각 커스텀 응답을ApiResponses
에 추가함으로써 Swagger 문서화에 반영되도록 설정했습니다.
- 찾은
이로 인해 인터페이스에 정의한 @CustomApiResponses
어노테이션이 구현 클래스에서도 자동으로 적용될 수 있었습니다.
결론
annotation에 대해 공부해보는 좋은 경험이라는 마음으로 시작한 작업이었는데 생각보다 가독성 측면과 결과에서는 유의미한 결과를 냈다는 생각이 들었습니다.
// 적용하기 전 (여기에 추가로 예외 메세지 적용해야함)
@ApiResponse(responseCode = "404", description = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))
})
// 적용한 후
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", description = "질문 또는 답변을 찾지 못한 경우"),
NestJS로 개발하던 때에도 decorator를 Custom해봤던 경험을 살려서 작업했는데 아직 사이드이펙트는 떠오르는 것이 없지만, 애초에 API 문서를 보기 쉽도록 하고, 작성하는데에 귀찮음을 굉장히 줄일 수 있었다는 점에서 꽤나 성공적이라고 생각했습니다.
Java의 annotation을 좀더 이해했으니 필요한 상황에 좀더 적용해보고자 합니다. (WebClient를 annotation으로 작성했던 적이 있었는데 AOP까지 적용하고나니 정말 큰 사이드 이펙트가 예상되어 접었던..적이 있으니 다시..?)