|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'ExceptionHandler와 ControllerAdvice를 알아보자' |
| 4 | +author: [5기_포이] |
| 5 | +tags: ['spring'] |
| 6 | +date: '2023-05-02T12:00:00.000Z' |
| 7 | +draft: false |
| 8 | +image: ../teaser/cycle.png |
| 9 | +--- |
| 10 | + |
| 11 | +콘솔 애플리케이션을 구현할 때, 우리는 예외를 핸들링하기 위해 try / catch문을 사용했습니다. |
| 12 | + |
| 13 | +그러나 웹 애플리케이션에서는 예외 처리 방법이 조금 다릅니다. |
| 14 | + |
| 15 | +이번 글에서는 스프링을 사용한 웹 애플리케이션 적용할 수 있는 예외 처리 방법인 `@ExceptionHandler`와 `@ControllerAdvice`에 대해 알아보겠습니다. |
| 16 | + |
| 17 | +<!-- end --> |
| 18 | + |
| 19 | +## 스프링부트의 기본적인 예외 처리 |
| 20 | + |
| 21 | +```java |
| 22 | +@RestController |
| 23 | +public class SimpleController { |
| 24 | + |
| 25 | + @GetMapping(path = "/errorExample") |
| 26 | + public String invokeError() { |
| 27 | + throw new IllegalArgumentException(); |
| 28 | + } |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +라는 예시에서 예외를 던지는 메서드를 호출하는 URL `/errorExample`에 요청을 보내봅시다. |
| 33 | + |
| 34 | +웹으로 요청을 보낸다면 스프링부트가 제공하는 기본 에러 페이지가 표시됩니다. |
| 35 | + |
| 36 | +`BasicErrorController` 는 요청의 Accept 헤더 값이 text/html일 때, 예외가 발생하면 `/error` 라는 경로로 재요청을 보내줍니다. |
| 37 | + |
| 38 | +해당 페이지는 `/error` 경로에 등록된 기본 에러 페이지입니다. |
| 39 | + |
| 40 | +만약 기본 에러 페이지가 아닌 스스로 작성한 커스텀 페이지를 보여주고 싶다면 뷰 템플릿 경로에 커스텀 페이지 파일을 만들어서 넣어두면 됩니다. |
| 41 | + |
| 42 | +오류 페이지 파일명에 따라 표시되는 웹페이지를 다르게 설정할 수 있습니다. |
| 43 | + |
| 44 | +- 4xx.html: 400대 오류 페이지 |
| 45 | +- 5xx.html: 500대 오류 페이지 |
| 46 | +- 404.html: 404 오류 페이지 |
| 47 | + |
| 48 | +만약 페이지 변경뿐 아니라 더 상세하게 예외의 내용을 응답에 담고 싶다면, `BasicErrorController`를 상속한 `@Controller` 클래스를 만들어 `errorHtml()` 메서드와 `error()` 메서드를 재정의해주면 됩니다. |
| 49 | + |
| 50 | +다만 이 방법은 url만 알면 누구나 마음대로 error 페이지에 접근할 수 있다는 단점이 있습니다. |
| 51 | + |
| 52 | +이러한 단점을 `@ExceptionHandler`를 사용해 해결할 수 있습니다. |
| 53 | + |
| 54 | +## @ExceptionHandler |
| 55 | + |
| 56 | +`@ExceptionHandler` 어노테이션을 사용하면 value로 원하는 예외를 지정하고 이를 핸들링 할 수 있습니다. |
| 57 | + |
| 58 | +예외에 대한 세부적인 정보 또한 응답으로 전달해 줄 수 있습니다. |
| 59 | + |
| 60 | +```java |
| 61 | +@RestController |
| 62 | +public class SimpleController { |
| 63 | + |
| 64 | + // ... |
| 65 | + |
| 66 | + @ExceptionHandler(value = IllegalArgumentException.class) |
| 67 | + public ResponseEntity<String> invokeError(IllegalArgumentException e) { |
| 68 | + ... |
| 69 | + return new ResponseEntity<>("error Message", HttpStatus.BAD_REQUEST); |
| 70 | + } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +위 처럼 컨트롤러별로 반환하고자 하는 body와 메시지를 예외별로 적절하게 선택하여 세밀한 정보를 제공할 수 있습니다. |
| 75 | + |
| 76 | +`@ExceptionHandler`는 value 속성으로 지정한 예외뿐 아니라 예외의 자식 클래스도 전부 캐치해 지정된 응답을 반환하게 됩니다. (value 속성을 지정하지 않는다면 메서드의 파라미터에 있는 예외가 자동으로 지정됩니다.) |
| 77 | + |
| 78 | +만약 자식 클래스는 다른 예외 처리를 적용하고 싶다면 다음과 코드를 같이 작성하여 처리할 수 있습니다. |
| 79 | + |
| 80 | +```java |
| 81 | +@RestController |
| 82 | +public class SimpleController { |
| 83 | + |
| 84 | + // ... |
| 85 | + |
| 86 | + @ExceptionHandler(value = IllegalArgumentException.class) |
| 87 | + public ResponseEntity<String> invokeError(IllegalArgumentException e) { |
| 88 | + ... |
| 89 | + return new ResponseEntity<>("부모 클래스", HttpStatus.BAD_REQUEST); |
| 90 | + } |
| 91 | + |
| 92 | + @ExceptionHandler(value = IllegalArgumentExtendsException.class) |
| 93 | + public ResponseEntity<String> invokeError(IllegalArgumentException e) { |
| 94 | + ... |
| 95 | + return new ResponseEntity<>("자식 클래스", HttpStatus.BAD_REQUEST); |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +`@ExceptionHandler`는 코드를 작성한 컨트롤러에서만 발생하는 예외만 처리됩니다. |
| 101 | + |
| 102 | +만약 같은 예외에 대해 여러 컨트롤러에서 같은 처리를 하고 싶다면 컨트롤러마다 같은 메서드를 작성해 주어야만 합니다. |
| 103 | + |
| 104 | +즉, 코드의 중복이 발생할 수밖에 없습니다. |
| 105 | + |
| 106 | +## ControllerAdvice |
| 107 | + |
| 108 | +`@ControllerAdvice`는 `@Component` 어노테이션의 특수한 케이스로, 스프링 부트 애플리케이션에서 전역적으로 예외를 핸들링할 수 있게 해주는 어노테이션입니다. |
| 109 | + |
| 110 | +이를 통해 코드의 중복을 해결할 수 있습니다. |
| 111 | + |
| 112 | +또한, 하나의 클래스 내에서 정상 동작 시 호출되는 코드와 예외를 처리하는 코드를 분리할 수 있습니다. |
| 113 | + |
| 114 | +다음은 `@ControllerAdvice` 와 `@RestConrollerAdvice` 의 구현의 일부입니다. |
| 115 | + |
| 116 | +```java |
| 117 | +@Target(ElementType.TYPE) |
| 118 | +@Retention(RetentionPolicy.RUNTIME) |
| 119 | +@Documented |
| 120 | +@ControllerAdvice |
| 121 | +@ResponseBody |
| 122 | +public @interface RestControllerAdvice { |
| 123 | + ... |
| 124 | +} |
| 125 | + |
| 126 | +@Target(ElementType.TYPE) |
| 127 | +@Retention(RetentionPolicy.RUNTIME) |
| 128 | +@Documented |
| 129 | +@Component |
| 130 | +public @interface ControllerAdvice { |
| 131 | + ... |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +`@Component`어노테이션을 붙이면 Component Scan 과정을 거쳐 Bean으로 등록됩니다. |
| 136 | + |
| 137 | +즉, 이 어노테이션을 사용하는 `ControllerAdvice` 또한 Bean으로 관리된다는 것을 알 수 있습니다. |
| 138 | + |
| 139 | +또한, `@RestControllerAdvice`는 `@ResponseBody` 어노테이션이 붙어있으므로 응답을 Json으로 처리한다는 것을 알 수 있습니다. |
| 140 | + |
| 141 | +`@ControllerAdvice` 어노테이션을 통해 예외를 핸들링하는 클래스를 구현해 보았습니다. |
| 142 | + |
| 143 | +```java |
| 144 | +@ControllerAdvice |
| 145 | +public class SimpleControllerAdvice { |
| 146 | + |
| 147 | + @ExceptionHandler(IllegalArgumentException.class) |
| 148 | + public ResponseEntity<String> IllegalArgumentException() { |
| 149 | + return ResponseEntity.badRequest().build(); |
| 150 | + } |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +위 처럼 코드를 작성하게 되면 애플리케이션 내의 모든 컨트롤러에서 발생하는 `IllegalArgumentException`을 해당 메서드가 처리하게 됩니다. |
| 155 | + |
| 156 | +**주의점** |
| 157 | + |
| 158 | +여러 `ControllerAdvice`가 있을 때 `@Order`어노테이션으로 순서를 지정하지 않는다면 Spring은 `ControllerAdvice`를 임의의 순서로 호출합니다. 즉, 사용자가 예상하지 못한 예외 처리가 발생할 수 있습니다. |
| 159 | + |
| 160 | +### basePackages |
| 161 | + |
| 162 | +만약 여러 `ControllerAdvice`를 세분화하고 싶다면 `basePackages` 속성을 이용할 수 있습니다. |
| 163 | + |
| 164 | +작성된 패키지와 하위 패키지에서 발생하는 예외는 해당 `ControllerAdvice`에서 처리하도록 지정할 수 있습니다. |
| 165 | + |
| 166 | +```java |
| 167 | +@ControllerAdvice(basePackages = {"org.woowa.tmp.pkg"} |
| 168 | +public class SimpleControllerAdvice { |
| 169 | + |
| 170 | + @ExceptionHandler(IllegalArgumentException.class) |
| 171 | + public ResponseEntity<String> IllegalArgumentException() { |
| 172 | + return ResponseEntity.badRequest().build(); |
| 173 | + } |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +`basePackages` 속성을 통해 뷰와 모델을 이용해 직접 페이지를 반환하는 Controller는 예외가 발생하면 직접 예외 페이지를 응답하게 하고, JSON 형식으로 데이터를 주고받는 Controller는 예외가 발생하면 예외에 대한 정보를 JSON 형식으로 응답하게 할 수도 있습니다. |
| 178 | + |
| 179 | +### assignableTypes |
| 180 | + |
| 181 | +`assignableTypes` 속성을 이용하면 클래스 단위로도 `ControllerAdvice`를 적용할 수 있습니다. |
| 182 | + |
| 183 | +클래스 단위로 사용되는 만큼 `basePackages` 속성보다 조금 더 세밀하게 처리를 분리해 주고 싶을 때 사용하면 유용합니다. |
| 184 | + |
| 185 | +```java |
| 186 | +@ControllerAdvice(assignableTypes = {SimpleController.class} |
| 187 | +public class SimpleControllerAdvice { |
| 188 | + |
| 189 | + @ExceptionHandler(IllegalArgumentException.class) |
| 190 | + public ResponseEntity<String> IllegalArgumentException() { |
| 191 | + return ResponseEntity.badRequest().build(); |
| 192 | + } |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +### ResponseEntityExceptionHandler |
| 197 | + |
| 198 | +Spring은 스프링 예외를 미리 처리해 둔 `ResponseEntityExceptionHandler`를 추상 클래스로 제공하고 있습니다. |
| 199 | + |
| 200 | +`ResponseEntityExceptionHandler`에는 스프링 예외에 대한 `ExceptionHandler`가 모두 구현되어 있습니다. |
| 201 | + |
| 202 | +하지만 에러 메시지는 반환하지 않으므로 스프링 예외에 대한 에러 응답을 보내려면 이 클래스를 상속하여 아래 메서드를 오버라이딩 해야 합니다. |
| 203 | + |
| 204 | +```java |
| 205 | +public abstract class ResponseEntityExceptionHandler { |
| 206 | + ... |
| 207 | + |
| 208 | + protected ResponseEntity<Object> handleExceptionInternal( |
| 209 | + Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { |
| 210 | + |
| 211 | + if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { |
| 212 | + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); |
| 213 | + } |
| 214 | + return new ResponseEntity<>(body, headers, status); |
| 215 | + } |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +## 마무리 |
| 220 | + |
| 221 | +스프링에서는 `@ExceptionHandler` `@ControllerAdvice`를 통해 편리한 예외 처리 기능들을 제공합니다. |
| 222 | + |
| 223 | +또한 여러 가지 속성을 통해 직접 메서드를 오버라이딩하는 것처럼 상세하게 세부 사항들을 지정해 줄 수도 있습니다. |
| 224 | + |
| 225 | +코드가 더 간결해지고, 에러 처리 코드의 위치를 사용자가 유연하게 관리할 수 있다는 장점도 있습니다. |
| 226 | + |
| 227 | + |
| 228 | +## 참고 |
| 229 | +- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc |
| 230 | +- https://tecoble.techcourse.co.kr/post/2021-05-10-controller_advice_exception_handler/ |
| 231 | +- https://www.baeldung.com/spring-controllers |
0 commit comments