간만에 코딩 많이 하니까 어질어질하더군뇨..
팀 프로젝트 때 프론트로부터 “이거 응답 형태가 뭔가요?” 라는 물음표 살인마의 저주로부터 많이 안전해집니다
{
isSuccess:
code:
message:
result:
}
조금씩 바꿔도 괜찮습니다 (result 대신 data를 쓴다던지)
@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;
@JsonInclude(JsonInclude.Include.NON_NULL) // result 값은 null이 아닐 때만 응답에 포함시킨다는 뜻
private T result;
//성공한 경우 응답 생성
public static <T> ApiResponse<T> onSuccess(T result) {
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), result);
}
// 실패한 경우 응답 생성
public static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(true, code, message, data);
}
}
<T> ApiResponse<T> 이건 또 어느나라 말이람?ApiResponse<String> stringResponse = ApiResponse.onSuccess("Hello, World!");public interface BaseCode {
public ReasonDTO getReason();
public ReasonDTO getReasonHttpStatus();
}
API 응답 양식을 지정해주기 위해 DTO를 만들어준다
@Getter
@Builder
public class ReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
}
public class TempConverter {
public static TempResponse.TempTestDto toTempTestDTO() {
return TempResponse.TempTestDto.builder()
.testString("테스트")
.build();
}
public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag) {
return TempResponse.TempExceptionDTO.builder()
.flag(flag)
.build();
}
}
TempResponse라는 HTTP 응답 관련 DTO들을 보관하는 클래스에 TempTestDto 만들기
public class TempResponse { //TempResponse가 다양한 응답을 감싸는 역할
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDto{ //TempTestDto는 구체적인 응답데이터 구조를 정의
String testString;
}
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempExceptionDTO {
Integer flag;
}
}
from() 또는 of()를 넣어 stream().map() 을 사용해 변환하는 것과 Converter 클래스를 두는 것의 차이stream().map()을 사용하면 객체 컬렉션을 스트림으로 변환하고, 각 객체를 DTO로 변환하는 데 사용할 수 있다.public class TempRestController {
private final TempQueryService tempQueryService;
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDto> testAPI() {
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
}
살짝 어려운 파트
@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 Error
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다"),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
//Article Error
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),
// Error Test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "에러 테스트"),
;
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();
}
}
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus() {
return this.code.getReasonHttpStatus();
}
}
Exception의 종류를 지정해준 것이다
@Slf4j
@RestControllerAdvice(annotations = {RestController.class}) // 이 클래스는 모든 @RestController에서 발생하는 예외를 처리한다
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) { //Bean Validation API를 사용하여 검증할 때 발생하는 예외로, 주로 요청 데이터의 유효성 검사 실패 시 발생
String errorMessage = e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));
return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request); //예외로부터 에러 메시지를 추출하여 handleExceptionInternalConstraint 메서드를 호출
}
@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);
}
@org.springframework.web.bind.annotation.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) //사용자 정의 예외(GeneralException)를 처리한다
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) { //예외를 처리하고 그 결과를 Http Response로 반환한다
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
);
}
}
다른 부분은 일단 제쳐두고 onThrowException() 메서드를 유심히 보시면 됩니다
@ExceptionHandler(value = GeneralException.class) //사용자 정의 예외(GeneralException)를 처리한다
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
중요 - GeneralException타입의 예외 발생시 getErrorReasonHttpStatus()를 실행한다
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
Command Service (명령 서비스):
Query Service (쿼리 서비스):
오늘은 QueryService 인터페이스와 QueryServiceImpl만 작성해보자
public interface TempQueryService {
void CheckFlag(Integer flag);
}
@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService{
@Override
public void CheckFlag(Integer flag) {
if (flag == 1) { //flag값이 1일 때 예외 발생
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
}
}
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
private final TempQueryService tempQueryService;
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDto> testAPI() {
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
@GetMapping("/exception")
public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(//추가된 부분
@RequestParam Integer flag
) {
tempQueryService.CheckFlag(flag);
return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
}
}
getErrorReasonHttpStatus() 를 시키라고 함getErrorReasonHttpStatus() 에 의해 에러코드를 가지고 BaseErrorCode로 이동getReasonHttpStatus() 를 호출 → BaseErrorCode를 상속받는 ErrorStatus로 이동getReasonHttpStatus()일련의 과정이 이해됐다 = 예외 처리 마스터임 하산하세요
@RestControllerAdvice를 사용하면 애플리케이션 전반에 걸쳐 예외 처리를 일관되게 관리할 수 있었다. 특정 컨트롤러나 메서드에 종속되지 않고 예외 처리를 정의할 수 있는 것도 객체지향 프로그래밍의 방향성에 아주 적합하다고 생각한다