본문 바로가기

Spring Boot

[Spring Boot] 유효성 검사와 예외 처리를 통한 API 구현

728x90

기존 문제점

기존 포스트에서 작성한 서비스 로직에서는 두 가지 문제점이 있습니다:

  1. 서비스 로직 내에서 오류 처리를 수행한다: 이로 인해 코드가 복잡해지고 가독성이 떨어집니다.
  2. 오류 발생 시 상태 코드 200을 반환한다: 클라이언트는 오류가 발생했음에도 불구하고 정상 응답으로 인식할 수 있습니다.

이 문제를 해결하기 위해 ResponseEntity를 사용하여 상태 코드를 적절하게 설정하고, ExceptionHandler를 사용하여 예외 처리를 분리하는 방법을 적용해 보겠습니다.

Step 1: ResponseEntity를 활용한 상태 코드 설정

먼저, ResponseEntity를 사용하여 상태 코드를 반환하도록 변경하겠습니다. 이렇게 하면 오류 발생 시 적절한 상태 코드를 반환할 수 있습니다.

변경된 StudentValidationController 코드

package org.example.controller;

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.stream.Collectors;

@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    // 반환 값을 ResponseEntity
    public ResponseEntity<Api<? extends Object>> studentInfo(
            @Valid @RequestBody Api<Student> student,
            BindingResult bindingResult
    ) {
        if (bindingResult.hasErrors()) {
            var errorList = bindingResult.getFieldErrors().stream()
                    .map(it -> {
                        var format = "%s : { %s } 은 %s";
                        var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
                        return errorFormat;
                    }).collect(Collectors.toList());

            var body = Api.Error.builder()
                    .errorList(errorList)
                    .build();

            var response = Api.builder()
                    .statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
                    .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
                    .error(body)
                    .build();

            // 400 상태 코드로 설정하고 body에 데이터 보내기
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(response);
        }

        log.info("info : {}", student);
        // 200 코드로 설정 후 body로 데이터 보내기
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(student);
    }
}

이렇게 하면 오류 상태에서 데이터를 전송했을 때 200 대신 적절한 상태 코드(400)를 반환할 수 있습니다.

Step 2: ExceptionHandler를 활용한 예외 처리 분리

서비스 로직에서 오류 처리를 분리하기 위해 ExceptionHandler를 사용합니다. 이를 통해 예외 처리를 전담하는 클래스를 생성합니다.

ValidationExceptionHandler 클래스 생성

package org.example.exception;

import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice // 예외처리 어노테이션
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Api<? extends Object>> handleValidationExceptions(MethodArgumentNotValidException exception) {
        var errorList = exception.getFieldErrors().stream()
                .map(it -> {
                    var format = "%s : { %s } 은 %s";
                    var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
                    return errorFormat;
                }).collect(Collectors.toList());

        var body = Api.Error.builder()
                .errorList(errorList)
                .build();

        var response = Api.builder()
                .statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
                .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .error(body)
                .build();

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(response);
    }
}

이 클래스를 통해 MethodArgumentNotValidException이 발생할 때마다 지정된 메서드가 실행되어 예외를 처리합니다. 이를 통해 서비스 로직에서 예외 처리 코드가 제거되어 코드가 더 깔끔해집니다.

최종 코드

StudentValidationController.java

package org.example.controller;

import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {

    @PostMapping("")
    public Api<Student> studentInfo(
            @Valid 
            @RequestBody 
            Api<Student> student) {
        log.info("info : {}", student);
        
        var body = student.getData();
        
        var response = Api.<Student>builder()
                .statusCode(String.valueOf(HttpStatus.OK.value()))
                .message(HttpStatus.OK.getReasonPhrase())
                .error(body)
                .build();
                           .
        return student;
    }
}

ValidationExceptionHandler.java

package org.example.exception;

import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Api<?>> handleValidationExceptions(MethodArgumentNotValidException exception) {
        var errorList = exception.getFieldErrors().stream()
                .map(it -> {
                    var format = "%s : { %s } 은 %s";
                    var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
                    return errorFormat;
                }).collect(Collectors.toList());

        var body = Api.Error.builder()
                .errorList(errorList)
                .build();

        var response = Api.builder()
                .statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
                .message(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .error(body)
                .build();

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(response);
    }
}

요약

이 글에서는 Spring Boot에서 유효성 검사와 예외 처리를 분리하여 API를 더 깔끔하고 유지보수하기 쉽게 만드는 방법을 설명했습니다. ResponseEntity를 사용하여 상태 코드를 설정하고, ExceptionHandler를 사용하여 예외 처리를 전담하는 클래스를 생성함으로써 코드의 가독성과 확장성을 높였습니다. 이를 통해 오류 발생 시 클라이언트에게 정확한 상태 코드와 메시지를 전달할 수 있게 되었습니다.

728x90