반응형
@ControllerAdvice를 사용한 중앙집중 예외처리
0. 패키지 구조
1. ErrorCode를 정의한다.
- Enum 타입으로 ErrorCode 클래스를 정의했다. 이렇게 관리하면 커스텀한 에러처리를 할수가 있다.
- 우선 나는 status, code, message를 필드로 선언했고 이 모든것들을 생성자로 받도록 설정했다.
- status에는 우리가 자주보는 400번대 500에러가 있고 code에는 내가 직접 지정한 커스텀 에러코드를 넣어줬다. 마지막으로 message에는 알아보기 쉽게 한글에러 메시지를 넣어줬다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* 커스텀한 에러코드를 작성한다.
*/
@Getter
@AllArgsConstructor
public enum ErrorCode {
USER_ALREADY_EXIST(409, "1001", "유저가 이미 존재합니다."),
USER_NOT_FOUND(404, "1002", "유저를 찾을수 없습니다."),
INVALID_PASSWORD(401, "1003", "패스워드가 틀렸습니다."),
DUPLICATED_USER_NAME(409, "1004", "중복된 아이디입니다."),
INVALID_TOKEN(401, "1005", "토큰이 유효하지 않습니다."),
INVALID_PERMISSION(401, "1006", "접근권한이 없습니다."),
INTERNAL_SERVER_ERROR(500, "1008", "서버에 에러가 발생했습니다.")
;
private int status;
private String code;
private String message;
}
2. 커스텀 예외 클래스를 생성한다.
- 나는 UserApplicationException이라는 클래스명으로 내 프로젝트에 적용시킬 예외 클래스를 만들어 줬다.
- 이때 클래스에서 RuntimeException을 상속받는걸 잊지말자 -> [extends RuntimeException]
- 내부에는 ErrorCode 필드만 선언한다.
- 추후 예외처리는 이 객체 내부에 접근해서 GlobalControllerAdvice에서 처리한다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* User 커스텀 예외처리 클래스
*/
@Getter
@AllArgsConstructor
public class UserApplicationException extends RuntimeException {
private ErrorCode errorCode;
}
3. GlobalControllerAdvice 클래스를 생성한다.
- @ControllerAdvice 어노테이션을 사용하여 전역 예외 처리 클래스를 생성한다. 이 클래스는 모든 컨트롤러에서 발생하는 예외를 처리하는 역할을 한다. @RestController를 사용하는지 아니면 @Controller를 사용하는지에따라 상단의 ControllerAdivce 어노테이션 코드가 달라진다.
- 코드를 작성하기전에 이걸 꼭 확인해라 -> @ExceptionHandler() 이 괄호안에는 본인이 만든 예외클래스.class를 넣어줘야하고 메서드의 매게변수로도 꼭 본인이 만든 예외클래스를 넣어줘라 (커스텀 예외 중앙처리 ExceptionHandler에만 해당함)
- @RestController를 사용할때 -> @RestControllerAdvice 적용
- 내용을 확인해보자면 내가 직접 만든 커스텀한 예외클래스인 UserApplicationException이 터졌을때 @ExceptionHandler의 코드가 동작해서 예외를 처리해 준다. 당연히 api전용인 @RestController를 사용했기때문에 return도 ResponseEntity<>로 처리한다.
- 나머지 RuntimeException이 터졌을때에는 하단의 RuntimeException전용 에러처리가 동작해서 NPE같은 처리를 원래대로 해준다.
- 내용을 확인해보자면 내가 직접 만든 커스텀한 예외클래스인 UserApplicationException이 터졌을때 @ExceptionHandler의 코드가 동작해서 예외를 처리해 준다. 당연히 api전용인 @RestController를 사용했기때문에 return도 ResponseEntity<>로 처리한다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 중앙 집중 예외처리를 위한 GlobalControllerAdvice 선언
* RestController를 사용중이니 Advice도 @RestControllerAdvice를 사용한다.
*/
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {
/**
* 에러 중앙처리 로직 작성완료
*/
@ExceptionHandler(UserApplicationException.class)
public ResponseEntity<?> applicationHandler(UserApplicationException e) {
log.error("Error occurs {}", e.toString());
// 에러 응답을 세팅한다.
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("code", e.getErrorCode().getCode());
errorResponse.put("message", e.getErrorCode().getMessage());
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(errorResponse);
}
/**
* 이 방식을 적용하면 조금 더 상세하게 에러 정보를 볼수가 있다. (NPE같은 특정 예외처리의 세부정보를 제공한다.)
* 하지만 이 방법은 예외 메시지가 직접 노출되므로 주의해야 한다.
* 최근에는 이렇게 사용하는게 조금 더 선호되는것 같다.(더 많은 정보를 제공하고, 필터링으로 보안문제를 해결할 수 있고, 유연하다.)
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> applicationHandler(RuntimeException e) {
log.error("Error occurs {}", e.toString());
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("code", ErrorCode.INTERNAL_SERVER_ERROR.getCode());
errorResponse.put("message", e.getMessage());
return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
.body(errorResponse);
}
}
- @Controller를 사용할 때 -> @ControllerAdvice 적용
- 위의 @RestControllerAdvice와 다르게 이녀석은 타임리프를 사용하기에 return이 ModelAndView인것을 볼 수 있다.
- 그리고 api는 response에 에러코드를 담아줬지만 여기서는 error페이지로 이동시키면서 ModelAndView안에 "errorMessage"를 바로 담아줘서 error.html안에서 모델에 접근해서 가져다 사용할수가 있다.
- NPE를 잡아주는 RuntimeException은 여기에도 따로 적용시켜줬다.
- 위의 @RestControllerAdvice와 다르게 이녀석은 타임리프를 사용하기에 return이 ModelAndView인것을 볼 수 있다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
/**
* 중앙 집중 예외처리를 위한 GlobalControllerAdvice 작성
*/
@Slf4j
@ControllerAdvice
public class GlobalControllerAdvice {
/**
* UserApplicationException 발생했을 때 호출되는 메서드
* 에러 페이지를 보여주고, 에러 메시지를 모델에 추가한다.
* <p th:text="${errorMessage}"></p> 이런식으로 타임리프에 에러페이지를 만들고 가져다 사용한다.
*/
@ExceptionHandler(UserApplicationException.class)
public ModelAndView handleProfileApplicationException(UserApplicationException e) {
log.error("Error occurs {}", e.toString());
// Spring MVC는 알아서 'error'라는 이름의 뷰를 찾아서 렌더링한다.
ModelAndView mav = new ModelAndView("error"); // 에러 페이지의 뷰 이름
mav.addObject("errorMessage", e.getErrorCode().getMessage()); // 에러 메시지를 모델에 추가
return mav;
}
/**
* RuntimeException이 발생했을 때 호출되는 메서드
* 에러 페이지를 보여주고, 에러 메시지를 모델에 추가한다.
*/
@ExceptionHandler(RuntimeException.class)
public ModelAndView handleRuntimeException(RuntimeException e) {
log.error("Error occurs {}", e.toString());
// Spring MVC는 알아서 'error'라는 이름의 뷰를 찾아서 렌더링한다.
ModelAndView mav = new ModelAndView("error"); // 에러 페이지의 뷰 이름
mav.addObject("errorMessage", e.getMessage()); // 에러 메시지를 모델에 추가
return mav;
}
}
4. 커스텀 예외를 코드에 적용시키는 예시
- 아래 코드는 userId를 통해 User 엔티티를 찾는 Service레이어의 코드다.
- 코드에 있는 userRepository.findById()는 Jpa에서 자체적으로 지원하는 코드이고 이건 Optional<T>을 반환한다. 즉 Optional을 반환하니까 에러가 나면 orElseThrow()로 Optional에서 객체를 꺼내면서 동시에 커스텀한 예외처리를 해줄수가 있다는 것이다.
- 그래서 아래와 같이 Optional안에 값이 없다면(empty) 내가만든 예외인 UserApplicationException을 던지면서 그 안에 매게변수로 ErrorCode에 직접 선언해준 USER_NOT_FOUND를 넣어서 던져줬다.
- 이러면 userId로인한 조회의 오류가 생기면 내가만든 예외인 USER_NOT_FOUND의 정보가 전달될 것이다.
import com.diningtalk.user.domain.User;
import com.diningtalk.user.dto.UserDto;
import com.diningtalk.user.exception.ErrorCode;
import com.diningtalk.user.exception.UserApplicationException;
import com.diningtalk.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
public UserDto findUser(Long userId) {
return userRepository.findById(userId)
.map(UserDto::fromEntity)
.orElseThrow(() -> new UserApplicationException(ErrorCode.USER_NOT_FOUND));
}
}
반응형
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
Spring Boot 기초: 어노테이션 활용하기 (1편) (0) | 2023.08.08 |
---|---|
Spring Framework 이해하기: 다형성, 개방-폐쇄 원칙(OCP), 인터페이스 활용의 장점 (0) | 2023.08.08 |
스프링의 핵심: Singleton 패턴 알아보기 (0) | 2023.08.07 |
스프링 프레임워크의 핵심: 제어의 역전(IoC) (0) | 2023.08.07 |
[스프링, 스프링부트] Spring - 의존성 주입(DI - Dependency Injection) (0) | 2023.08.07 |