[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 를 거쳐서 각각 상황에 맞게 변형하기 때문에 각각의 스키마를 만드는 것 보다는
위와 같이 패키지를 따로 분리해서 사용하는 방법을 선택했다.
위의 방법의 장점은 비즈니스 로직에서 분리되서 개발에 집중할 수 있지만 단점으로는 변경사항이 있을 때 해당 부분의 변경을 놓칠 수 있을 것 같다.
댓글남기기