[Hoops PJ] 트러블 슈팅 & TIL (8)
공통으로 사용하는 API 리펙토링
단순한 API Response를 응답해야하는 경우가 있을 때 그 상황에 맞는 API 값을 만들어서 반환하게 만들었었다.
예를 들어서 단순한 성공 케이스만을 전달 할 때 아래와 같이 사용하고 있었다.
컨트롤러
public class ReportController {
private final ReportService reportService;
@PreAuthorize("hasRole('USER')")
@PostMapping("/user")
public ResponseEntity<CustomApiResponse> report(
@RequestBody @Valid ReportDto request) {
this.reportService.reportUser(request);
return ResponseEntity.ok().body(
new CustomApiResponse("유저신고", "Success"));
}
@PreAuthorize("hasRole('OWNER')")
@GetMapping("/contents/{report_id}")
public ResponseEntity<CustomApiResponse> reportContents(
@PathVariable("report_id") @NotBlank String report_id) {
return ResponseEntity.ok().body(
new CustomApiResponse("신고내역",
this.reportService.reportContents(report_id)));
}
CustomApiResponse
@Builder
@Getter
@Setter
@AllArgsConstructor
public class CustomApiResponse {
private String title;
private String detail;
public CustomApiResponse of() {
return CustomApiResponse.builder()
.title(this.title)
.detail(this.detail)
.build();
}
}
근데 swagger를 정리하다보니 수정의 필요성이 있었다.
물론 현재 처럼 상태 변환 없이 단순하게 사용하는 데 있어서 크게 수정할 필요는 없다고 생각했지만 아래 와 같이 의존하는 부분이 꽤 많았다.
스키마를 따로 분리해줘서 만들어준다고 해도 CustomApiResponse에 너무 많이 의존을 하고 있다고 생각했다.
이럴 경우 수정할 때 피곤해지니 의존성을 줄여야 된다고 생각했다.
예를 들어서 Json 방식이 아닌 xml, yml 같은 형식 변환이 필요하거나 혹은 다른 형태로 변환하게 된다면? -> 지옥당첨
특히 단순하게 Success 응답만 필요한 경우, 현재 중복이 많으므로 형식을 강제로 통일해 줄 수 있도록 아래와 같이 변경했다.
CustomApiResponse -> ApiResponseFactory 리팩토링 DI,DIP
컨트롤러
public class ReportController {
private final ReportService reportService;
private final ApiResponseFactory apiResponseFactory; <- 추가
@PreAuthorize("hasRole('USER')")
@PostMapping("/user")
public ResponseEntity<BasicApiResponse> report(
@RequestBody @Valid ReportDto request) {
this.reportService.reportUser(request);
return ResponseEntity.ok().body(
apiResponseFactory.createSuccessResponse("유저신고"));
}
@PreAuthorize("hasRole('OWNER')")
@GetMapping("/contents/{report_id}")
public ResponseEntity<BasicApiResponse> reportContents(
@Parameter(description = "신고 유저 PK", name = "report_id")
@PathVariable("report_id") @NotBlank String report_id) {
return ResponseEntity.ok().body(
apiResponseFactory.createSuccessWithDetailResponse(
"신고 내역", this.reportService.reportContents(report_id)));
}
ApiResponseFactory 컴포넌트
@Component
public class ApiResponseFactory {
public BasicApiResponse createSuccessResponse(String title) {
return new CustomApiResponse(title, "Success");
}
public BasicApiResponse createSuccessWithDetailResponse(String title, String detail) {
return new CustomApiResponse(title, detail);
}
}
BasicApiResponse 인터페이스
public interface BasicApiResponse {
String getTitle();
String getDetail();
}
CustomApiResponse 클래스
@Builder
@Getter
@AllArgsConstructor
public class CustomApiResponse
implements BasicApiResponse, SwaggerApiResponse {
private String title;
private String detail;
public static CustomApiResponse of(String title, String detail) {
return CustomApiResponse.builder()
.title(title)
.detail(detail)
.build();
}
}
BasicApiResponse
인터페이스를 만듬CustomApiResponse
는BasicApiResponse
인터페이스를 사용해서 DIP(Dependency Inversion Principle)ApiResponseFactory
에서 응답 값이 Success 같이 형태가 일정해야 하는 부분은 형식을 강제로 통일하게 만듬ApiResponseFactory
를 컴포넌트로 만들어서 컨트롤러에 주입
사실 간단한 부분을 쓸데 없이 추상화한 부분도 없지 않아 있다.
하지만 혹시라도 수정하게 되거나 의존성등을 생각한다면 지금과 같이 변경하는 게 좋다고 생각했다.
SwaggerSchema
스키마 같은 경우 공통으로 ApiResponseFactory
를 사용하지만 응답 값은 다르게 반환해서 보여줘야 할 때가 있다.
이럴 경우 스키마도 각각의 경우에 맞게 변경한 예시를 보여줘야 하는데 이 때 어떻게 할지 고민이 되었다.
특히 page 처리 같은 경우도 어떻게 해야할 지 고민이 되었다.
가장 좋은 방법은 DTO 단에서 schema를 처리하는 게 좋지만 현재 컨트롤러상에서 GET 방식은 Dto가 따로 없으며 이걸 위해 따로 각각 주소에 맞는 response dto를 만들기가 너무 귀찮았다.
그래서 나는 swager에 사용하는 스키마 패키지를 만든 후 response를 아래와 같이 구현했다.
컨트롤러
@Slf4j
@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
@Tag(name = "REPORT")
public class ReportController {
private final ReportService reportService;
private final ApiResponseFactory apiResponseFactory;
@Operation(summary = "유저간 신고 기능")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "유저 신고 기능 성공",
content = @Content(schema = @Schema(implementation = ReportResponse.ReportUser.class))),
@ApiResponse(responseCode = "400", description = "커스텀 에러",
content = @Content(schema = @Schema(implementation = ErrorResponse.CustomError.class))),
@ApiResponse(responseCode = "403", description = "리프레시 토큰 만료",
content = @Content(schema = @Schema(implementation = ErrorResponse.ExpiredRefreshToken.class))),
@ApiResponse(responseCode = "500", description = "서버 에러 표시",
content = @Content(schema = @Schema(implementation = ErrorResponse.ServerError.class)))
}
)
@PreAuthorize("hasRole('USER')")
@PostMapping("/user")
public ResponseEntity<BasicApiResponse> report(
@RequestBody @Valid ReportDto request) {
this.reportService.reportUser(request);
return ResponseEntity.ok().body(
apiResponseFactory.createSuccessResponse("유저신고"));
}
@Operation(summary = "신고된 유저 리스트")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "신고된 리스트 출력",
content = @Content(schema = @Schema(implementation = ReportResponse.PageReportUsersList.class))),
@ApiResponse(responseCode = "400", description = "커스텀 에러",
content = @Content(schema = @Schema(implementation = ErrorResponse.CustomError.class))),
@ApiResponse(responseCode = "403", description = "리프레시 토큰 만료",
content = @Content(schema = @Schema(implementation = ErrorResponse.ExpiredRefreshToken.class))),
@ApiResponse(responseCode = "500", description = "서버 에러 표시",
content = @Content(schema = @Schema(implementation = ErrorResponse.ServerError.class)))
})
@PreAuthorize("hasRole('OWNER')")
@GetMapping("/user-list")
public ResponseEntity<Page<ReportListResponseDto>> reportList(
@RequestParam(value = "page", defaultValue = "0") @Positive int page,
@RequestParam(value = "size", defaultValue = "10") @Positive int size) {
return ResponseEntity.ok()
.body(this.reportService.reportList(page, size));
}
@Operation(summary = "신고 내역 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "신고 내역 조회",
content = @Content(schema = @Schema(implementation = ReportContent.class))),
@ApiResponse(responseCode = "400", description = "커스텀 에러",
content = @Content(schema = @Schema(implementation = ErrorResponse.CustomError.class))),
@ApiResponse(responseCode = "403", description = "리프레시 토큰 만료",
content = @Content(schema = @Schema(implementation = ErrorResponse.ExpiredRefreshToken.class))),
@ApiResponse(responseCode = "500", description = "서버 에러 표시",
content = @Content(schema = @Schema(implementation = ErrorResponse.ServerError.class)))
})
@PreAuthorize("hasRole('OWNER')")
@GetMapping("/contents/{report_id}")
public ResponseEntity<BasicApiResponse> reportContents(
@Parameter(description = "신고 유저 PK", name = "report_id")
@PathVariable("report_id") @NotBlank String report_id) {
return ResponseEntity.ok().body(
apiResponseFactory.createSuccessWithDetailResponse(
"신고 내역", this.reportService.reportContents(report_id)));
}
\
스웨거용 ErrorResponse 스키마
package com.hoops.commonResponse.swaggerSchema;
import io.swagger.v3.oas.annotations.media.Schema;
public class ErrorResponse {
@Schema(name = "ExpiredRefreshToken", description = "리프레시 토큰 만료 응답")
public static class ExpiredRefreshToken extends
com.zerobase.hoops.exception.ErrorResponse {
@Schema(description = "errorCode", example = "EXPIRED_REFRESH_TOKEN")
private String errorCode;
@Schema(description = "errorMessage", example = "리프레시 토큰의 기간이 만료되었습니다.")
private String errorMessage;
@Schema(description = "응답 상태", example = "403")
private String statusCode;
}
@Schema(name = "ServerError", description = "서버에러")
public static class ServerError extends
com.zerobase.hoops.exception.ErrorResponse {
@Schema(description = "errorCode", example = "INTERNAL_SERVER_ERROR")
private String errorCode;
@Schema(description = "errorMessage", example = "내부 서버 오류")
private String errorMessage;
@Schema(description = "응답 상태", example = "500")
private String statusCode;
}
@Schema(name = "CustomError", description = "커스텀에러")
public static class CustomError extends
com.zerobase.hoops.exception.ErrorResponse {
@Schema(description = "errorCode", example = "CustomError")
private String errorCode;
@Schema(description = "errorMessage", example = "상황에 맞는 커스텀 에러.")
private String errorMessage;
@Schema(description = "응답 상태", example = "400")
private String statusCode;
}
}
스웨거용 ReportResponse 스키마
package com.hoops.commonResponse.swaggerSchema;
import com.zerobase.hoops.users.type.AbilityType;
import com.zerobase.hoops.users.type.GenderType;
import com.zerobase.hoops.users.type.PlayStyleType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class ReportResponse {
// Report
@Getter
@AllArgsConstructor @Schema(name = "ReportUser", description = "유저 신고 응답")
public static class ReportUser implements SwaggerApiResponse {
@Schema(description = "응답 메시지", example = "유저신고")
private String title;
@Schema(description = "응답 상태", example = "Success")
private String detail;
}
@Getter
@Schema(name = "PageReportUsersList", description = "신고된 유저 리스트 페이지")
public static class PageReportUsersList {
@Schema(description = "페이지의 항목 리스트")
private List<ReportUsersList> content;
@Schema(description = "Pageable")
private Pageable pageable;
@Schema(description = "마지막 페이지 여부")
private boolean last;
@Schema(description = "총 요소 수")
private long totalElements;
@Schema(description = "총 페이지 수")
private int totalPages;
@Schema(description = "페이지 크기")
private int size;
@Schema(description = "현재 페이지 번호")
private int number;
@Schema(description = "정렬 정보")
private Sort sort;
@Schema(description = "첫 페이지 여부")
private boolean first;
@Schema(description = "현재 페이지의 요소 수")
private int numberOfElements;
@Schema(description = "페이지가 비어있는지 여부")
private boolean empty;
public PageReportUsersList(List<ReportUsersList> content,
Pageable pageable, boolean last, long totalElements,
int totalPages, int size, int number, Sort sort, boolean first,
int numberOfElements, boolean empty) {
this.content = content;
this.pageable = pageable;
this.last = last;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.size = size;
this.number = number;
this.sort = sort;
this.first = first;
this.numberOfElements = numberOfElements;
this.empty = empty;
}
}
@Getter
@Schema(name = "Pageable", description = "Pageable")
public static class Pageable {
private int pageNumber;
private int pageSize;
private Sort sort;
private long offset;
private boolean unpaged;
private boolean paged;
}
@Getter
@Schema(name = "Sort", description = "정렬 정보")
public static class Sort {
private boolean empty;
private boolean sorted;
private boolean unsorted;
}
@Getter
@Schema(name = "ReportUsersList", description = "신고 내역")
public static class ReportUsersList {
@Schema(description = "신고 PK", example = "2")
private Long reportId;
@Schema(description = "유저 PK", example = "5")
private Long userId;
@Schema(description = "유저 이름", example = "김갑수")
private String userName;
@Schema(description = "매너 포인트", example = "3.5")
private String mannerPoint;
@Schema(description = "성별", example = "MALE")
private GenderType gender;
@Schema(description = "능력", example = "SHOOT")
private AbilityType ability;
@Schema(description = "플레이 스타일", example = "AGGRESSIVE")
private PlayStyleType playStyle;
}
@Getter
@AllArgsConstructor @Schema(name = "ReportContent", description = "신고 내역")
public static class ReportContent implements SwaggerApiResponse {
@Schema(description = "응답 메시지", example = "신고내역")
private String title;
@Schema(description = "응답 상태", example = "정말 비매너로 경기를 하고 욕설을 너무 많이해서 불화가 많습니다.")
private String detail;
}
}
SwaggerApiResponse 인터페이스
package com.hoops.commonResponse.swaggerSchema;
public interface SwaggerApiResponse {
String getTitle();
String getDetail();
}
Dto 를 사용해서 데이터를 받는 경우 response 부분을 따로 만들어서 각각 필드에 스키마를 먹이면 되지만 그렇게 못 하는? 경우도 있고
error 나 exception 처리 같은 경우 customException에서 CustomApiResponse
와 같은 방식으로 ErrorCode
를 거쳐서 각각 상황에 맞게 변형하기 때문에 각각의 스키마를 만드는 것 보다는
위와 같이 패키지를 따로 분리해서 사용하는 방법을 선택했다.
위의 방법의 장점은 비즈니스 로직에서 분리되서 개발에 집중할 수 있지만 단점으로는 변경사항이 있을 때 해당 부분의 변경을 놓칠 수 있을 것 같다.
댓글남기기