반응형
  • [Spring Boot] Exception 처리 - 에러 페이지 적용(화면), 에러 코드 적용(API) 여기서 ErrorCode와 ExceptionDto를 활용하여 Exception을 출력하는 방법에 대해서 정리했었음
  • 이 방식은 Response의 status와 body를 직접 만들어서 응답해주는 방식
  • 하지만 이 방식은 실제로 프로젝트에 적용하기에는 어려움이 있음
    • 이 방식을 사용하기 위해서는 모든 Return Type을 맞춰줘야 하는 어려움이라던지, Service 단에서 에러가 발생하는 경우 에러 처리가 힘들다는 등의 단점이 존재
  • 따라서 Response를 수정해서 응답하는 방식이 아닌 exception이 발생한 지점에서 throw를 통해 exception을 던지고 나중에 이를 받아 처리하는 작업이 필요
    • @ExceptionHandler, @ControllerAdvice, @RestControllerAdvice 사용

ErrorCode, MyException 생성

ErrorCode (enum)

@Getter
@AllArgsConstructor
public enum ErrorCode {

    USERNAME_NOT_FOUND(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다"),
    INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "권한이 없습니다"),
    DUPLICATED_USER_NAME(HttpStatus.CONFLICT, "유저명이 중복됩니다"),
    DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "DB에러가 발생하였습니다");

    private HttpStatus status;
    private String message;

}

MyException

  • ExceptionDto 와는 달리 RuntimeException을 상속 받음
    • 단순히 출력용이 아닌 Exception 처리를 하기 위함
@Getter
public class MyException extends RuntimeException{

    private String result;
    private ErrorCode errorCode;
    private String message;

    public MyException(ErrorCode errorCode) {
        this.result = "ERROR";
        this.errorCode = errorCode;
        this.message = errorCode.getMessage();
    }
}

ExceptionRestController, ExceptionService 생성

ExceptionRestController

  • Controller가 아닌 Service에서 exception이 발생한 상황을 가정
@RestController
@RequestMapping("/exception-example")
@RequiredArgsConstructor
public class ExceptionRestController {

    private final ExceptionService exceptionService;

    @GetMapping("/throw-my-exception/1")
    public void throwMyException1() {
        // ex) 로그인 시 username에 해당하는 User가 없는 경우
        exceptionService.login();
    }

    @GetMapping("/throw-my-exception/2")
    public void throwMyException2() {
        // ex) 로그인 하지 않은 유저가 댓글을 작성하려는 경우
        exceptionService.writeComment();
    }

    @GetMapping("/throw-my-exception/3")
    public void throwMyException3() {
        // ex) 회원가입 시 username이 중복되는 경우
        exceptionService.join();
    }

    @GetMapping("/throw-my-exception/4")
    public void throwMyException4() {
        // ex) 댓글 추가 시 DB 에러가 발생한 경우
        exceptionService.editComment();
    }
}

ExceptionService

@Service
public class ExceptionService {

    public void login() {
        throw new MyException(ErrorCode.USERNAME_NOT_FOUND);
    }

    public void writeComment() {
        throw new MyException(ErrorCode.INVALID_PERMISSION);
    }

    public void join() {
        throw new MyException(ErrorCode.DUPLICATED_USER_NAME);
    }

    public void editComment() {
        throw new MyException(ErrorCode.DATABASE_ERROR);
    }
}

여기까지의 결과

  • log에는 원하는 exception이 제대로 발생하지만, response를 확인해보면 HttpStatus가 500인 것을 확인할 수 있음
    • 원하는 결과는 HttpStatus.NOT_FOUND(404) 에러가 출력되는 것임

 

@ExceptionHandler 적용

  • 위에서 작성한 ExceptionRestController에 아래 코드 추가
    • ExceptionRestController에서 MyException이 발생하면 myExceptionHandler가 실행되는 방식
    • MyException.class가 아닌 RuntimeException.class, IOException.class 등과 같이 기본 exception들을 처리할 수도 있음
@ExceptionHandler(MyException.class)
public ResponseEntity<?> myExceptionHandler(MyException e) {
    e.printStackTrace();
    return ResponseEntity.status(e.getErrorCode().getStatus())
            .body(new ExceptionDto(e.getErrorCode()));
}

결과

  • log 뿐만 아닌, response에도 원하는 HttpStatus가 출력되는 것을 확인할 수 있음

 

 

@ControllerAdvice, @RestControllerAdvice

  • 위에서 적용한 @ExceptionHandler는 메소드가 속한 Controller나 RestController에서만 적용됨
  • Controller, RestController가 여러개이고 Controller마다 굳이 따로 excetpion 처리를 하지 않고, 모두 동일한 방식으로 처리하려는 상황에서 @ExceptionHandler는 비효율적임
  • 이런 상황에서는 @ControllerAdvice, @RestControllerAdvice 사용
    • @ControllerAdivce는 @Controller에, @RestControllerAdivce는 @RestController에만 적용되는 것이 아닌 @RestController = @Controller + @ResponseBody와 같은 차이

@RestControllerAdvice 적용 예제

  • 일단 ExceptionRestController에 추가했던 myExceptionHandler 삭제 후 진행
  • annotations를 사용해 특정 어노테이션에만 적용하거나, basePackages를 사용해 특정 패키지에만 적용 시키는 등의 방법으로 적용 범위를 지정할 수 있음
  • 만약 MyException이 발생한다면 myExceptionHandler에서 해당 exception 처리
  • runtimeExceptionHandler에서는 @ResponseStatus를 통해 HttpStatus를 설정하고 body는 String 타입으로 return
//@RestControllerAdvice(annotations = Controller.class)
@RestControllerAdvice(basePackages = "com.study.hellospring.exception_example")
public class ExceptionManager {

    @ExceptionHandler(MyException.class)
    public ResponseEntity<?> myExceptionHandler(MyException e) {
        e.printStackTrace();
        return ResponseEntity.status(e.getErrorCode().getStatus())
                .body(new ExceptionDto(e.getErrorCode()));
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String runtimeExceptionHandler(RuntimeException e) {
        e.printStackTrace();
        return "Runtime Exception 발생!";
    }
}

결과

  • ExceptionRestController 뿐만 아닌 해당 패키지의 다른 Controller에서 발생한 runtimeException도 잘 처리하는 것을 확인할 수 있음

 

(추가) @ModelAttribute, @InitBinder

  • @ControllerAdvice, @RestControllerAdvice를 보통 exception 처리에 많이 활용하는데 @ModelAttribute, @InitBinder와 같이 사용하여 더 다양하게 활용할 수 있음
    • ex) @ControllerAdvice, @RestControllerAdvice의 범위에 포함되는 요청 시, 매번 입력되는 자료를 변환 및 Model 추가

@ModelAttribute

  • @ModelAttribute가 적용되는 Controller의 메서드가 실행될 때 모든 요청마다 Model에 값을 담아 넘길 수 있음
@Slf4j
@Controller
public class TestController {

    @GetMapping("/model-attribute")
    public String home() {
        log.info("home 실행");
        return "home.html";
    }

    @ModelAttribute
    public void addModel(Model model) {
        log.info("Model Attribute 실행");
        model.addAttribute("nowTime", LocalDateTime.now());
    }
}

결과

  • home.html 화면을 return 할 때, home() 메서드에서는 model을 추가하지 않았지만 아래와 같이 값이 잘 넘어오는 것을 확인할 수 있음

@InitBinder

  • 주로 검증이나 자료형 변환 등에 사용
  • 문자열을 입력받으면 이를 날짜로 변환하는 작업을 @InitBinder을 통해 변환하는 예제 코드
@Slf4j
@Controller
public class TestController {

    @Data
    private static class InitBinderDto {
        private String name;
        private Date createdAt;
    }

    @PostMapping("/init-binder")
    @ResponseBody
    public String test(@RequestBody InitBinderDto dto) {
        log.info("name : {}", dto.getName());
        log.info("createdAt : {}", dto.getCreatedAt());
        return dto.toString();
    }

    @InitBinder()
    public void initBinder(WebDataBinder dataBinder) {
        log.info("Init Binder 실행");
        // String -> Date 변환
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
    }
}

결과

  • 문자열 "2023-01-10"을 입력했지만 Date Type으로 변환된 것을 확인할 수 있음

 

반응형

↓ 클릭시 이동

복사했습니다!