ResponseEntity와 Exception

seokseungmin·2024년 12월 10일

Today I Learned

목록 보기
20/20

Spring Boot에서 ResponseEntity와 ResponseMessage, Validation 흐름

1. ResponseEntity란?

Spring Boot에서 ResponseEntity는 HTTP 응답을 세밀하게 제어하기 위해 제공되는 클래스입니다.
다음과 같은 세 가지 주요 요소를 설정할 수 있습니다:

  • HTTP 상태 코드: 응답 상태를 나타냅니다 (예: 200 OK, 404 Not Found).
  • 응답 본문: 클라이언트에게 반환할 데이터 (JSON, XML 등).
  • 응답 헤더: 추가적인 메타데이터 (예: 인증 토큰, 캐싱 정책).

2. ResponseMessage란?

ResponseMessage는 응답 데이터를 일관성 있게 제공하기 위해 설계된 커스텀 클래스입니다.
모든 API가 동일한 응답 형식을 유지하도록 도와주며, 성공과 실패 결과를 명확히 구분할 수 있습니다.

ResponseMessage의 전체 구조

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResponseMessage<T> {

    private String result;
    private List<Errors> errors;
    private T data;

    public static <T> ResponseMessage<T> success() {
        return success(null);
    }

    public static <T> ResponseMessage<T> success(T data) {
        return new ResponseMessage<T>(SUCCESS.toString(), null, data);
    }

    public static <T> ResponseMessage<T> fail(ErrorCode errorCode) {
        return ResponseMessage.<T>builder()
            .result(FAIL.toString())
            .errors(List.of(new Errors(errorCode)))
            .build();
    }

    public static <T> ResponseMessage<T> fail(List<Errors> errors) {
        return ResponseMessage.<T>builder()
            .result(FAIL.toString())
            .errors(errors)
            .build();
    }

    public enum Result {
        SUCCESS, FAIL
    }
}

ResponseMessage의 주요 구조

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ResponseMessage<T> {
    private String result;         // 성공(SUCCESS) 또는 실패(FAIL) 결과
    private List<Errors> errors;   // 에러 정보 리스트
    private T data;                // 응답 데이터
}
  • result: 응답 결과(SUCCESS 또는 FAIL).
  • errors: 실패 시 포함되는 에러 리스트.
  • data: 성공 시 포함되는 응답 데이터.

성공/실패 응답 생성

// 성공 응답
public static <T> ResponseMessage<T> success(T data) {
    return new ResponseMessage<>("SUCCESS", null, data);
}

// 실패 응답
public static <T> ResponseMessage<T> fail(ErrorCode errorCode) {
    return ResponseMessage.<T>builder()
        .result("FAIL")
        .errors(List.of(new Errors(errorCode)))
        .build();
}

3. 요청 데이터 검증 흐름: @Valid

@Valid는 요청 데이터를 검증할 때 사용됩니다.
Spring Boot에서 @Valid를 적용하면 요청 데이터가 DTO의 유효성 조건에 부합하지 않을 경우, 예외(MethodArgumentNotValidException)가 발생합니다.


4. 프로젝트 코드 예시로 배우는 Validation 흐름

(1) 요청 DTO 정의

요청 데이터를 검증하는 DTO입니다.
이 클래스는 사용자 회원가입 요청을 처리하며, 다양한 유효성 조건을 포함합니다.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JoinDTO {

    @NotBlank(message = "USER-ERROR-VALID-00006")  // 이름 필수 입력
    private String username;

    @NotBlank(message = "USER-ERROR-VALID-00007")  // 닉네임 필수 입력
    private String nickname;

    @NotBlank(message = "USER-ERROR-VALID-00008")  // 전화번호 필수 입력
    private String phone;

    @Size(min = 4, message = "USER-ERROR-VALID-00009")  // 최소 4자
    private String password;

    @Email(message = "USER-ERROR-VALID-00011")  // 유효한 이메일 형식
    private String email;
}

(2) 컨트롤러 코드

요청 데이터 검증이 @Valid에 의해 수행됩니다.

@PostMapping
public ResponseEntity<ResponseMessage<Void>> joinProcess(@RequestBody @Valid JoinDTO joinDTO) {
    userService.joinProcess(joinDTO);
    return ResponseEntity.status(HttpStatus.CREATED).body(ResponseMessage.success());
}

(3) GlobalExceptionHandler

@Valid에서 발생한 검증 실패 예외를 처리하는 클래스입니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 유효성 검증 실패 시 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ResponseMessage<Void>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        List<Errors> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(fieldError -> new Errors(ValidErrorCode.findErrorCode(fieldError)))
            .collect(Collectors.toList());

        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ResponseMessage.fail(errors));
    }

    // 기타 예외 처리
    @ExceptionHandler(BizException.class)
    public ResponseEntity<ResponseMessage<Void>> handleBizException(BizException ex) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ResponseMessage.fail(ex.getErrorCode()));
    }
}

(4) ErrorCode와 Errors 클래스

에러를 관리하는 ErrorCodeErrors 클래스입니다.
에러 코드는 메시지와 함께 프로젝트 전반에서 재사용됩니다.

public interface ErrorCode {
    String getErrorCode();
    String getErrorMessage();
}
@Getter
@RequiredArgsConstructor
public enum ValidErrorCode implements ErrorCode {

    MIN_VALID_ERROR("USER-ERROR-VALID-00001", "값이 최솟값 이상이여야합니다."),
    VALID_ERROR("USER-ERROR-VALID-00002", "유효성 검사 실패."),
    DATABASE_VALID_ERROR("USER-ERROR-VALID-00003", "데이터베이스 제약 조건 위반."),
    FORBIDDEN("USER-ERROR-VALID-00004", "권한이 없습니다."),

    USER_NOT_FOUND_ERROR("USER-ERROR-VALID-00005", "사용자가 없습니다."),
    USERNAME_ERROR("USER-ERROR-VALID-00006", "사용자 이름은 필수 입니다."),
    NICKNAME_ERROR("USER-ERROR-VALID-00007", "사용자 닉네임은 필수 입니다."),
    PHONE_ERROR("USER-ERROR-VALID-00008", "사용자 핸드폰번호는 필수 입니다"),
    PASSWORD_ERROR("USER-ERROR-VALID-00009", "비밀번호는 4자 이상 입력해야 합니다."),
    NEW_PASSWORD_ERROR("USER-ERROR-VALID-00010", "새비밀번호는 4자 이상 입력해야 합니다."),
    MAIL_ERROR("USER-ERROR-VALID-00011", "이메일 형식이 잘못되었습니다."),
    LOGIN_FAIL_ERROR("USER-ERROR-VALID-00012", "로그인 실패!"),
    USER_PW_MISMATCH_ERROR("USER-ERROR-VALID-00013", "사용자 비밀번호 불일치!"),

    PROFILE_NOT_FOUND_ERROR("USER-ERROR-VALID-00014", "프로필이 없습니다."),
    MISSING_MBTI_ERROR("USER-ERROR-VALID-00015", "MBTI는 필수 입력 항목입니다."),
    MISSING_SMOKING_STATUS_ERROR("USER-ERROR-VALID-00016", "흡연 여부는 필수 입력 항목입니다."),
    MISSING_GENDER_ERROR("USER-ERROR-VALID-00017", "성별은 필수 입력 항목입니다."),
    MISSING_BIRTH_DATE_ERROR("USER-ERROR-VALID-00018", "생년월일은 필수 입력 항목입니다."),
    AUTHENTICATION_CODE_ERROR("USER-ERROR-VALID-00019", "인증 코드는 필수입니다.");

    private final String errorCode;
    private final String errorMessage;

    public static ErrorCode findErrorCode(FieldError fieldError) {

        return Arrays.stream(ValidErrorCode.values())
            .filter(v -> v.getErrorCode().equals(fieldError.getDefaultMessage())) // USER-ERROR-VALID-00002
            .findAny()
            .get();
    }
}
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class Errors {

    private String errorCode;
    private String errorMessage;

    public Errors(ErrorCode errorCode) {
        this.errorCode = errorCode.getErrorCode();
        this.errorMessage = errorCode.getErrorMessage();
    }
}
  • ValidErrorCode는 각 필드의 검증 실패에 대한 코드와 메시지를 정의합니다.
  • 에러 코드와 메시지는 Errors 객체로 변환되어 클라이언트에 전달됩니다.

5. 요청 흐름과 예외 처리 과정

1) 클라이언트 요청

{
  "username": "",
  "nickname": "john_doe",
  "phone": "123456789",
  "password": "123",
  "email": "invalid-email"
}
  • username 필수 값 누락.
  • password 길이 부족.
  • email 형식 유효하지 않음.

2) 검증 실패

@Valid에 의해 MethodArgumentNotValidException 예외가 발생합니다.
GlobalExceptionHandler가 이를 처리하여 다음과 같은 응답을 반환합니다.


3) 클라이언트 응답

{
  "result": "FAIL",
  "errors": [
    {
      "errorCode": "USER-ERROR-VALID-00006",
      "errorMessage": "사용자 이름은 필수입니다."
    },
    {
      "errorCode": "USER-ERROR-VALID-00009",
      "errorMessage": "비밀번호는 최소 4자 이상이어야 합니다."
    },
    {
      "errorCode": "USER-ERROR-VALID-00011",
      "errorMessage": "유효하지 않은 이메일 형식입니다."
    }
  ],
  "data": null
}

6. 코드 흐름 요약

  1. 클라이언트가 JoinDTO를 요청 본문으로 보냄.
  2. @Valid에 의해 DTO 유효성 검증 수행.
  3. 검증 실패 시 MethodArgumentNotValidException 발생.
  4. GlobalExceptionHandler가 예외를 처리하여 에러 응답 반환.
  5. 클라이언트는 에러 코드와 메시지를 통해 문제를 파악.

7. ResponseEntity와 ResponseMessage의 장점

  1. 일관된 응답 구조:
    • 모든 API 응답이 동일한 형식을 유지.
  2. 확장성:
    • 새로운 에러 코드를 쉽게 추가 가능.
  3. 디버깅 용이성:
    • 에러 코드와 메시지를 통해 문제를 빠르게 파악 가능.

결론

Spring Boot에서 ResponseEntity, ResponseMessage, @Valid를 결합하여 유효성 검증과 응답 처리를 일관되게 관리할 수 있었습니다.
명확한 에러 코드와 메시지를 제공함으로써, 클라이언트가 요청의 문제를 정확히 이해할 수 있는 시스템을 구축.

profile

0개의 댓글