Spring, Spring Boot/예외 처리

@RestControllerAdvice를 이용한 예외 처리

너지살 2023. 8. 21. 21:26

@RestControllerAdvice에서 예외를 처리한 후 사용자에게 반환

 

 

개요

이번 글에서 @RestControllerAdvice 어노테이션을 사용하여 전역적으로 예외 처리를 하는 GlobalExceptionHandler 클래스를 만들어 예외 처리를 해보겠습니다.

 

 

@RestControllerAdvice 

  • 전역적으로 예외 처리를 관리하고 응답을 생성하는 어노테이션입니다. 
  • 여러 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있습니다. 
  • 표준화된 응답 형식을 유지할 수 있습니다. 

 

사용 순서 

  1. 커스텀 예외  클래스 작성
  2. 예외 상황 발생
  3. GlobalExceptionHandler에서 예외 처리

 

제가 예외 처리한 상황은 회원 가입을 할 때 들어와야 할 데이터가 들어오지 않아 예외 처리를 해야 하는 상황 입니다. 

 

 

 

1. 커스텀 예외 클래스 작성 ( FieldDataException )

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FieldDataException extends RuntimeException{

    private String field;
    private String reason;

}

field와 reason 데이터를 만들어 어떤 field가 문제가 있는지 reason에는 그 문제가 무엇인지 사용자에게 반환하려 합니다. 

 

 

 

2. 예외 상황 발생 ( UserServiceImpl )

import com.dotd.user.dto.UserResponseDto;
import com.dotd.user.dto.UserRegisterRequestDto;
import com.dotd.user.entity.User;
import com.dotd.user.exception.FieldDataException;
import com.dotd.user.mapper.UserMapper;
import com.dotd.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
    @Override
    public UserResponseDto register(UserRegisterRequestDto dto) {

        // 예외 발생시 클래스를 만들어 예외 처리 실행
        if (dto.getLoginId() == null || dto.getLoginId().isEmpty()) {
            throw new FieldDataException("loginId", "loginId가 비어있습니다.");
        }
        else if(dto.getPassword() == null || dto.getPassword().isEmpty()) {
            throw new FieldDataException("password", "password가 비어있습니다.");
        }

        log.info("userService의 register 실행");
        User user = userMapper.userRegisterRequestDtoToUser(dto);
        User save = userRepository.save(user);
        UserResponseDto result = userMapper.userToUserResponseDto(save);

        return result;
    }
}

들어와야 할 데이터가 null이거나 비어있으면 적절한 메시지를 담아 커스텀 예외 처리 클래스를 생성했습니다. 

 

 

 

3. GlobalExceptionHandler에서 예외 처리 

import com.dotd.user.exception.FieldDataException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 필드가 비어있는 예외를 처리하는 메소드 
    @ExceptionHandler(value = {FieldDataException.class})
    public ResponseEntity<Object> FieldDataExceptions(FieldDataException ex, WebRequest request) {

        errorSpotLog(ex);

        // 에러 응답 생성
        ErrorDetails errorDetails = new ErrorDetails();
        errorDetails.setCode("400");
        errorDetails.setMessage("field is Missing");

        Map<String, String> detailsMap = new HashMap<>();


        detailsMap.put("field", ex.getField());
        detailsMap.put("reason", ex.getReason());

        errorDetails.setDetails(detailsMap);

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setError(errorDetails);

        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }


  // 에러 위치 표시 로그
    public void errorSpotLog(Exception ex) {
        StackTraceElement[] stackTrace = ex.getStackTrace();
        String className;
        String methodName;
        for (StackTraceElement element : stackTrace) {
            className = element.getClassName();
            methodName = element.getMethodName();
            if (className.contains(".service.") || className.contains(".controller.")) {
                log.error("에러 발생 위치 : {}.{}", className, methodName);
                break;
            }
        }
        log.error("메시지 : {}", ex.getMessage());
    }
    
    
        // 에러 응답
    @Data
    public class ErrorResponse {
        private ErrorDetails error;
    }

    // 에러 응답 디테일
    @Data
    public class ErrorDetails {
        private String code;
        private String message;
        private Map<String, String> details;
    }
}

GlobalExceptionHandler는 여러 Controller에서 발생한 예외를 처리하고 일정한 응답을 하기 위해 사용합니다.

저는 여기서 2개의 메소드와 2개의 클래스를 사용했습니다. 

 

 

 

FieldDataException 메소드 

    // 필드가 비어있는 예외를 처리하는 메소드 
    @ExceptionHandler(value = {FieldDataException.class})
    public ResponseEntity<Object> FieldDataExceptions(FieldDataException ex, WebRequest request) {

        errorSpotLog(ex);

        // 에러 응답 생성
        ErrorDetails errorDetails = new ErrorDetails();
        errorDetails.setCode("400");
        errorDetails.setMessage("field is Missing");

        Map<String, String> detailsMap = new HashMap<>();


        detailsMap.put("field", ex.getField());
        detailsMap.put("reason", ex.getReason());

        errorDetails.setDetails(detailsMap);

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setError(errorDetails);

        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

FieldDataException이 메소드에는 UserServiceImpl에서 발생한 Field에 발생한 예외를 처리하는 메소드 입니다.

커스텀 예외 클래스를 발생할 때 만든 field와 reason을 사용자에게 응답할 errorResponse에 담아 어떤 필드가 어떤 문제가 있는지 알려줍니다. 

 

 

 

errorSpotLog 메소드 

  // 에러 위치 표시 로그
    public void errorSpotLog(Exception ex) {
        StackTraceElement[] stackTrace = ex.getStackTrace();
        String className;
        String methodName;
        for (StackTraceElement element : stackTrace) {
            className = element.getClassName();
            methodName = element.getMethodName();
            if (className.contains(".service.") || className.contains(".controller.")) {
                log.error("에러 발생 위치 : {}.{}", className, methodName);
                break;
            }
        }
        log.error("메시지 : {}", ex.getMessage());
    }

errorSpotLog  메소드에선 에러가 발생한 클래스, 메소드와 이유를 로그로 나타냅니다.

개발자들이 로그를 통해 어디서 에러가 발생했고 이유가 뭔지 파악하기 쉽게 하기 위해 만들었습니다. 

 

 

 

ErrorResponse 클래스 

    @Data
    public class ErrorResponse {
        private ErrorDetails error;
    }

    // 에러 응답 디테일
    @Data
    public class ErrorDetails {
        private String code;
        private String message;
        private Map<String, String> details;
    }

일정한 응답을 하기 위해 에외 응답 클래스를 만들었습니다.

code와 message를 공통적으로 가지고 Map<String, String> details를 통해 여러가지 에외 상황에서 사용자에게 정보를 줄 수 있는 자료구조를 만들었습니다. 

 

 

 

 

마무리

예외 처리는 방법은 정말 다양하여 정답은 없는 것 같습니다.

예외 처리를 통해 예상치 못한 오류 상황에 대해 대처하고 디버깅을 수월하게 하는 등 목적이 다양합니다.

이러한 목적을 생각하여 자신의 프로젝트에 맞게 예외 처리를 해야겠다고 생각했습니다. 감사합니다.