개요
이번 글에서 @RestControllerAdvice 어노테이션을 사용하여 전역적으로 예외 처리를 하는 GlobalExceptionHandler 클래스를 만들어 예외 처리를 해보겠습니다.
@RestControllerAdvice
- 전역적으로 예외 처리를 관리하고 응답을 생성하는 어노테이션입니다.
- 여러 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있습니다.
- 표준화된 응답 형식을 유지할 수 있습니다.
사용 순서
- 커스텀 예외 클래스 작성
- 예외 상황 발생
- 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를 통해 여러가지 에외 상황에서 사용자에게 정보를 줄 수 있는 자료구조를 만들었습니다.
마무리
예외 처리는 방법은 정말 다양하여 정답은 없는 것 같습니다.
예외 처리를 통해 예상치 못한 오류 상황에 대해 대처하고 디버깅을 수월하게 하는 등 목적이 다양합니다.
이러한 목적을 생각하여 자신의 프로젝트에 맞게 예외 처리를 해야겠다고 생각했습니다. 감사합니다.