Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: apiPayload 세팅 #1

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open

feat: apiPayload 세팅 #1

wants to merge 5 commits into from

Conversation

ajwoong
Copy link

@ajwoong ajwoong commented Dec 20, 2024

ApiResponse 기본 구조

@Getter
@RequiredArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse {
    private final Boolean isSuccess;
    private final String code;
    private final String message;

    @JsonInclude(Include.NON_NULL)
    private final PageInfo pageInfo;
    @JsonInclude(Include.NON_NULL)
    private final Object result;
    
    // ResponseEntity 생성 메소드들...
}

모든 응답 값에 포함되는 isSuccess, code, message 필드값과 null이 아닌 경우에만 반환될 pageInfo, result 필드를 정의하였습니다.

PageInfo의 경우 아래와 같은 구조로 되어있습니다.

@Getter
@AllArgsConstructor
public class PageInfo {
    private Integer page;
    private Integer size;
    private Boolean hasNext;
}

성공 코드에 대한 반환값 생성 함수

public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, PageInfo pageInfo, Object result) {
    return new ResponseEntity<>(
            new ApiResponse(true, status.getCode(), status.getMessage(), pageInfo, result),
            status.getHttpStatus()
    );
}

모든 필드값을 받아서 ApiResponse 객체를 생성하여 ResponseEntity의 형태가 감싼 응답 형태를 반환하도록 구성하였습니다. 해당 함수는 다른 onSuccess 함수에서 사용됩니다.

data 반환 메소드

// 데이터에 대한 응답
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, Object result) {
    return onSuccess(status, null, result);
}

data를 포함한 응답 값을 반환해주고자 할 경우, 반환하고자 하는 SuccessStatus 객체와 result 객체를 받아와 �위에서 정의한 onSuccess 함수가 호출되도록 구성하였습니다.

아래는 컨트롤러에서 해당 메소드를 사용하는 예시입니다.

@GetMapping("/data")
public ResponseEntity<ApiResponse> getDataTest() {
    String result = testService.testData();
    return ApiResponse.onSuccess(SuccessStatus._OK, result);
}

기본 응답값 반환 메소드

// SuccessStatus에 대한 응답
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status) {
    return onSuccess(status, null, null);
}

isSuccess, code, message만 반환할 경우 컨트롤러에서 SuccessStatus를 넘겨주면 onSuccess(status, page, result) 함수를 재활용하여 해당 메소드에 result 값과 page 정보가 null 로 설정되도록 구성하였습니다.

아래는 컨트롤러에서 해당 메소드를 사용하는 예시입니다.

@GetMapping("/response")
public ResponseEntity<ApiResponse> getResponseTest() {
    boolean success = testService.testResponse();
    if (success) {
        return ApiResponse.onSuccess(HttpStatus._OK);
    }
}

예외에 대한 응답값 반환 메소드

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler
    public ResponseEntity<ApiResponse> validation(ConstraintViolationException e) {
        String errorMessage = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .findFirst()
                .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
        return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
        e.printStackTrace();

        String errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
        return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<ApiResponse> handleMissingServletRequestParameterException(
            MissingServletRequestParameterException e
    ) {
        e.printStackTrace();

        String errorMessage = e.getParameterType() + " 타입의 " + e.getParameterName() + " 파라미터가 없습니다.";
        return ApiResponse.onFailure(ErrorStatus.VALIDATION_ERROR, errorMessage);
    }

    @ExceptionHandler(NoResourceFoundException.class)
    public ResponseEntity<ApiResponse> handleNoResourceFoundException(NoResourceFoundException e) {
        e.printStackTrace();

        return ApiResponse.onFailure(ErrorStatus._NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse> handleException(Exception e) {
        e.printStackTrace();

        return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(GeneralException.class)
    public ResponseEntity<ApiResponse> handleGeneralException(GeneralException e) {
        e.printStackTrace();

        return ApiResponse.onFailure(e.getErrorStatus(), e.getMessage());
    }
}

예외가 발생한 경우 ExceptionAdvice에서 스프링 컨테이너 내 발생하는 모든 예외상황을 받아와 ApiResponse.onFailure() 메서드를 호출하도록 설정하였습니다.

// ErrorCode에 대한 응답
public static ResponseEntity<ApiResponse> onFailure(ErrorStatus error) {
    return new ResponseEntity<>(
            new ApiResponse(false, error.getCode(), error.getErrorMessage(), null, null),
            error.getHttpStatus()
    );
}

ApiResponse에서는 ErrorStatus를 받아서 ApiResponse 객체를 만들어주고, 이를 ResponseEntity로 감싼 응답 형태를 반환해주도록 구성하였습니다.

현재 프로젝트 상에서는 모든 예외처리에 대한 응답값을 반환하는 주체가 ErrorExceptionHandler에서 처리해주고 있기 때문에 실제 개발 시에는 원하는 예외 상황에서 다음과 같이 예외를 발생시켜주면 됩니다.

throw new GeneralException(ErrorStatus.USER_NOT_FOUND);

페이지네이션 정보를 포함한 응답값 반환 메소드

// 페이지네이션에 대한 응답
public static ResponseEntity<ApiResponse> onSuccess(SuccessStatus status, Page<?> page) {
    PageInfo pageInfo = new PageInfo(page.getNumber(), page.getSize(), page.hasNext());
    return onSuccess(status, pageInfo, page.getContent());
}

페이지네이션을 적용할 경우 page 객체에 PageInfo 세팅을 위해 필요한 정보들과 data가 함께 들어가 있기 때문에 page 정보에 따라 PageInfo 객체를 만들어주고 기본 onSuccess 함수에 전달해줍니다.

아래는 컨트롤러에서 해당 메소드를 사용하는 예시입니다.

@GetMapping("/page")
public ResponseEntity<ApiResponse> getPage() {
    Page<ModelEntity> page = testService.testPage();
    return ApiResponse.onSuccess(SuccessStatus._OK, page);
}

현재 해당하는 코드는 데이터베이스가 설정되기 전이므로 주석처리 해두었습니다. (데이터소스를 찾을 수 없기 때문에 테스트 시 실행 안 됨)

data를 반환하는 컨트롤러와 에러를 던지는 컨트롤러에 대한 테스트코드를 작성해두었고, 두 테스트 모두 통과하였습니다.

Copy link
Contributor

@SuHyeon00 SuHyeon00 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseCode와 BaseErrorCode 인터페이스를 두개로 굳이 분리할 이유가 있을까 싶은게 에러 코드를 반환하는 상황이나 성공 코드를 반환하는 상황이나 갖고 있어야 할 정보로는 HttpStatus와 기본 응답 형태에 해당하는 isSuccess, code, message 필드로 동일할 것 같습니다.

추가로 현재 구현된 ApiResponse 클래스에서 사용하는 메소드로 클라이언트의 반환값을 설정해준다면 HttpStatus까지는 ApiResponse로 설정이 불가능한 것으로 보입니다. 아래 코드와 같이 사용한다고 생각하였을 때 응답 body에 해당하는 부분은 설정한 BAD_REQUEST에 해당하는 데이터로 반환이 되지만 실제 HttpStatus값은 200 OK로 설정되어 있는 것을 확인할 수 있습니다.

@PostMapping
public ApiResponse<String> postExample() {
    return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST.getCode(), ErrorStatus._BAD_REQUEST.getMessage(), null);
}
스크린샷 2024-12-20 오후 9 57 05

아마 Spring Framework의 ResponseEntity 키워드로 찾아보시면 반환되는 HttpStatus를 어떻게 커스텀할 수 있을지 파악하실 수 있을 것 같고 혹시 도움이 필요하시다면 카톡이나 코멘트로 남겨주세요!

그리고 예제 코드 짜면서 느낀건데 onFailure 함수도 BaseErrorCode 자체를 받아서 사용하는 편이 더 사용성이 좋아 보입니다..!
-> 이 부분 생각해보니 에러 상황일 경우 GeneralException을 던져서 Advice에서 받아 처리하는 로직으로 핸들링하는 것 같은데 Advice 클래스 확인해보니 onFailure를 사용하지 않는 것 같더라구요..?

Comment on lines 16 to 19
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSuccess 필드에만 @JsonProperty 어노테이션을 추가한 이유가 있나요?

private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지네이션 정보를 내려주는 경우도 있을텐데 이와 관련된 설정도 필요해 보입니다!

Comment on lines 26 to 28
public static <T> ApiResponse<T> onSuccess(T result){
return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 개인적인 사용성 측면의 피드백이긴 한데 클라이언트로 반환하는 값에 200 코드로 내려가는 경우더라도 result 필드가 없는 채로 반환되는 경우가 있을 것 같은데 이를 사용하는 입장에서 onSuccess(null) 보다는 onSuccess()를 호출하는 것이 더 사용하기 편해 보입니다!

Comment on lines 30 to 32
public static <T> ApiResponse<T> of(BaseCode code, T result){
return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 동일하게 result가 없는 상황에 대해서도 고려해보시면 좋을 것 같아요!


public interface BaseCode {

ReasonDTO getReason();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason이라는 네이밍을 봤을 때 어떤 필드인지 감이 잘 안 오는 것 같습니다

Comment on lines 38 to 46
public ErrorReasonDTO getReasonHttpStatus() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpStatus를 확인하고자 하는 용도라면 HttpStatus만 반환해주면 되지 않을까요? 이 함수와 getReason() 함수가 필요한 상황 구분이 잘 되지 않는 것 같습니다!

@SuHyeon00 SuHyeon00 requested a review from Sunho12 January 5, 2025 20:42
@SuHyeon00 SuHyeon00 self-assigned this Jan 5, 2025
@SuHyeon00 SuHyeon00 requested review from a team and SuHyeon00 January 5, 2025 20:42
Copy link

@Sunho12 Sunho12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나하나 코드 따라 쳐보면서 정상 작동 확인했습니다 !! 수고 많으셨구 배워갑니다 ~~ LGTM 🫶🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants