Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/example/phrasebe/apiPayload/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.phrasebe.apiPayload;

import com.example.phrasebe.apiPayload.code.BaseCode;
import com.example.phrasebe.apiPayload.code.status.SuccessStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {

@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 어노테이션을 추가한 이유가 있나요?

@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.

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



// 성공한 경우 응답 생성

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()를 호출하는 것이 더 사용하기 편해 보입니다!


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 static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(false, code, message, data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.phrasebe.apiPayload.code;

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이라는 네이밍을 봤을 때 어떤 필드인지 감이 잘 안 오는 것 같습니다


ReasonDTO getReasonHttpStatus();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.phrasebe.apiPayload.code;

public interface BaseErrorCode {

ErrorReasonDTO getReason();

ErrorReasonDTO getReasonHttpStatus();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.phrasebe.apiPayload.code;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ErrorReasonDTO {

private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;

public boolean getIsSuccess(){return isSuccess;}
}
18 changes: 18 additions & 0 deletions src/main/java/com/example/phrasebe/apiPayload/code/ReasonDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.phrasebe.apiPayload.code;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ReasonDTO {

private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;

public boolean getIsSuccess(){return isSuccess;}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.phrasebe.apiPayload.code.status;

import com.example.phrasebe.apiPayload.code.BaseErrorCode;
import com.example.phrasebe.apiPayload.code.ErrorReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {

// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),


// 멤버 관려 에러
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ErrorReasonDTO getReason() {
return ErrorReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}

@Override
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() 함수가 필요한 상황 구분이 잘 되지 않는 것 같습니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.phrasebe.apiPayload.code.status;

import com.example.phrasebe.apiPayload.code.BaseCode;
import com.example.phrasebe.apiPayload.code.ReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {

// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;


@Override
public ReasonDTO getReason() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.build();
}

@Override
public ReasonDTO getReasonHttpStatus() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.httpStatus(httpStatus)
.build()
;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.example.phrasebe.apiPayload.exception;

import com.example.phrasebe.apiPayload.ApiResponse;
import com.example.phrasebe.apiPayload.code.ErrorReasonDTO;
import com.example.phrasebe.apiPayload.code.status.ErrorStatus;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {


@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));

return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
}

@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

Map<String, String> errors = new LinkedHashMap<>();

e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
});

return handleExceptionInternalArgs(e,HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
}

@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();

return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
}

@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}

private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {

ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
// e.printStackTrace();

WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}

private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
return super.handleExceptionInternal(
e,
body,
headers,
status,
request
);
}

private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
WebRequest request, Map<String, String> errorArgs) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}

private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
return super.handleExceptionInternal(
e,
body,
headers,
errorCommonStatus.getHttpStatus(),
request
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.phrasebe.apiPayload.exception;

import com.example.phrasebe.apiPayload.code.BaseErrorCode;
import com.example.phrasebe.apiPayload.code.ErrorReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException{

private BaseErrorCode code;

public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}

public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.phrasebe.apiPayload.exception.handler;

import com.example.phrasebe.apiPayload.code.BaseErrorCode;
import com.example.phrasebe.apiPayload.exception.GeneralException;

public class ExceptionHandler extends GeneralException {
public ExceptionHandler(BaseErrorCode code) {
super(code);
}
}