예외 처리 전 에러 페이지 먼저 설정하기
Spring에서 예외 처리란 예외가 발생할 수 있는 곳에 예외 처리를 하여 어플리케이션이 중간에 멈추지 않게끔 만드는 것이다.
WAS(예외 페이지 처리) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (예외 발생)
위처럼 서버 영역인 Controller에서 발생한 예외에 대한 처리를 안하게 되면 예외를 서블릿 컨테이너인 WAS(보통 톰캣)에 던져 에러 페이지를 통해 처리하게 된다.
그러므로 컨트롤러에서 예외가 발생했다면 어플리케이션이 멈추지 않게 하기 위해 예외를 처리해줘야 한다.
참고 - 에러 페이지를 설정하지 않고 모든 예외를 예외 처리하면 되지 않나하고 생각할 수 있다.
하지만 404 Not Found 에러처럼 페이지 자체가 존재하지 않는 경우는 예외 처리를 해줄 수 없다.
왜냐면 페이지 자체가 존재하지 않는 다는 뜻은 서버를 접근하지 못했다는 뜻이라 컨트롤러 진입조차 불가능하기 때문이다. 또한 500 Internal Server Error는 예외 처리를 해주더라도 후에 다시 강제로 예외를 발생시켜서 문법에 오류가 났다는 것을 보여줘야 하는 경우가 많다. 이런 경우에는 에러 페이지가 따로 필요하다.
Spring 프로젝트에서 만약 에러 페이지 설정을 따로 하지 않으면 아래와 같이 톰장 내장 에러 페이지를 출력한다.
톰캣 내장 에러 페이지는 개발자가 의도하지 않은 에러 정보가 담겨있기 때문에 개발자가 직접 web.xml에 설정해주는 과정이 필요하다.
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/common/error.jsp</location>
</error-page>
<error-page>
<error-code>404</error-code>
<location>/common/error.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/common/error.jsp</location>
</error-page>
web.xml에 404 혹은 500 에러가 발생하면 error.jsp 를 뿌려주게 된다.
톰캣 내장 에러 페이지를 쓰지 않고 error.jsp를 통해 개발자가 원하는 페이지의 내용으로 에러를 보여줄 수 있다.
Spring Boot에서는 web.xml를 없애고 ErrorController를 구현한 BasicErrorController로 web.xml의 에러 페이지 기능을 이미 구현해놨기 때문에 아래와 같은 페이지를 뿌려준다.
해당 에러 페이지는 톰캣 내장 에러 페이지처럼 기본 에러 페이지다.
web.xml처럼 error 페이지를 따로 설정하기 위해선 리소스 폴더 밑 /error 폴더에 정적 html 페이지를 넣어주면 원하는 에러 페이지를 출력하도록 개발 할 수 있다.
Spring에서는 web.xml에 단순하게 에러 페이지 파일만 설정했었다.
하지만 Spring Boot에서는 /error 폴더 밑에 에러 페이지를 설정할 뿐만 아니라 application.properties를 통해 에러에 대한 내용을 어느 정도까지 보여줄지 유연하고 자세하게 보여줄 수 있다.(404 Not Found 라는 글보다는 어떤 페이지를 못찾았는지, 혹은 어떤 구체적인 에러가 발생했는지에 대한 정보를 개발자가 알아야 더 쉽게 개발이 가능하지 않겠는가?)
정리하자면 에러페이지라는 것은 예외 처리를 해줄 수 없는 영역에 대한 정보를 보여주기 위한 페이지다.
그럼 이제 404나 500 error와 같은 에러가 아닌 진짜 예외 처리를 하는 대상이 어떤 것인지 알아보자.
예외 처리를 하는 대상 - Checked Exception과 Unchecked Exception
보통 예외의 종류는 위처럼 Checked Exception과 Unchecked Exception이 존재한다.
Checked Exception
Checked Exception은 예외가 발생할 수 있으니 예외 처리를 강제하는 예외를 말한다.
예외 처리를 하지 않으면 컴파일 에러가 발생해 빨간줄이 뜬다. 대표적인 클래스는 FileInputStream이 있다.
이렇게 빨간줄이 뜨니 개발자는 예외처리를 무조건 해줘야 한다.
FileInputStream 내부를 들여다보면 파일을 찾지 못하는 경우(FileNotFoundException) 예외 처리를 하도록 구현해놨는데 이런 경우 개발자는 파일을 못찾을 경우 빈 파일을 생성하도록 예외 처리를 유도하도록 하는 방식을 권장하기 때문에 예외 처리를 위해 try catch로 묶어서 에러 발생시 다른 방식으로 유도하는(파일이 존재하지 않으면 새 파일 생성) 의도된 예외라고 보는 것이다.
다른 말로는 복구 가능한 예외라고 말한다.
Unchecked Exception
Unchecked Exception은 의도하지 않은 예외를 뜻한다.
의도하지 않은 예외가 발생한다면 이전의 처리 과정들을 전부 롤백 처리를 하는 것이 일반적이다.
복구가 불가능한 예외에 속하기 때문이다.
그렇기에 일반적으로 Unchecked Exception는 예외 처리를 해주지 않는다. 예외가 발생하게 두고 사용자에게 에러 페이지를 보여준다.
Unchecked Exception을 예외 처리를 하더라도 로그, 메시지 기능을 쓰기 위해 메시지를 하는 경우가 많고 로그를 뿌려준 이후에는 다시 해당 예외를 throw하는 방식으로 아래처럼 예외 처리를 한다.
try {
System.out.println("로직");
} catch(RuntimeException rex) { //UncheckedException을 catch해서 로그 출력 후 다시 throw
log.error(rex);
throw rex;
}
Unchecked Exception은 첫번째 그림처럼 모든 클래스가 Runtime Exception을 상속받고있다.
Unchecked Exception의 대표 부모인 RuntimeException을 catch하여 예외 처리하여 로그를 뿌려준 후 다시 해당 예외를 던지는 방식으로 처리할 수 있다.
런타임 예외를 확인하기 위해서는 말 그대로 어플리케이션이 항상 구동중이어야한다.
대표적으로 NullPointException(널 참조), ArrayIndexOutOfBoundsException(범위 초과), ArithmeticException(0 나누기) 등이 있다.
다시 반복해서 말하자면 여기에 속한 예외들은 '의도하지 않은 예외'이기 때문에 예외 처리를 요구하지 않는게 일반적이고 예외를 발생시켜야한다.
개발자가 직접 코드 수정을 통해 처리하길 원하는 예외이기 때문에 Checked Exception과는 다르게 예외 처리를 강제하지 않는다.
예외 처리 방법
모든 메소드에 try/catch 걸기
기존 스프링 레거시 방식부터 하나하나 살펴보고자 한다.
Spring 진영에서 가장 오래된 방식의 예외 처리는 try/catch다.
@RequestMapping(value = "/test.do")
public String test(HttpServletRequest request, Model model) throws Exception{
try {
System.out.println("로직");
} catch(RuntimeException rex) {
log.error(rex);
throw rex;
} catch(Exception e) {
log.error(e);
throw e;
}
return "test";
}
지금도 금융권이나 오래된 스프링 버젼을 갖고있는 SI나 SM은 아직 쓰고있는 방식이다.
예외처리를 할 수 있는 방법은 2가지가 있다.
try catch
try catch는 현재 메소드 내에서 예외처리를 하는 방식으로 예외가 발생하면 catch에서 개발자가 원하는대로 로그를 뿌려줄 수 있다. 위에서 말했듯 RuntimeException 같은 경우에는 의도되지 않은 예외이기 때문에 예외 처리가 필요 없으나 로그 출력 이후 throw를 통해 다시 예외를 발생시킨다.
throws
try catch 말고 다른 방식으로는 throws가 있는데 이는 책임 전가 방식을 뜻한다.
책임 전가 방식이란 부모 메소드가 대신해서 try catch를 통해 예외처리를 해줘야하기 때문에 자식 메소드에서는 try catch를 안쓰고 비즈니스 로직을 더 집중해서 구현할 수가 있다.
public void test() throws Exception {
// business logic
}
소스 내부에 try catch를 쓰는 것보다는 훨씬 깔끔하다.
물론 모든 예외를 throws Exception으로 처리하면 코드가 깔끔해질 순 있지만 catch로 예외를 잡아서 개발자가 원하는 대로 로그를 출력할 순 없다.
만약에 부모 메소드가 내가 작성한 메소드가 아니라 라이브러리에 존재하는 메소드인 경우 자식 메소드에서 throws로 던지면 부모 메소드인 라이브러리 메소드 코드에서는 고작 printStackTrace()로 애매모호한 로그를 뿌리고 예외 처리가 되기 때문에 어떤 에러인지 모호하다는 단점이 존재하기 때문이다.
참고로 throws Exception은 예외 처리를 할 대상은 Checked Exception에만 한정한다.
위와 같이 IOException은 throws를 통해 부모 메소드에 전파시킬 수 있다.
throws 책임전가 방식은 Unchecked Exception까지 예외 처리를 해주지 않는다.
자식 메소드에서 Unchecked Exception이 발생하면 throws를 적어주지 않더라도 부모 메소드까지 알아서 전파돼서 예외 처리가 가능하다.
HandlerExceptionResolver
try catch를 쓴다는 것은 명확한 단점이 존재한다. 모든 서비스 코드에 작성해줘야 하기 때문에 개발자는 비즈니스 로직에 집중하기 힘들다는 단점이 존재한다. 이러한 try catch 지옥(?)으로부터 벗어나기 위해 공통 관심사를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였는데 대표적인 것이 예외 처리 전략을 추상화한 HandlerExceptionResolver다.
그리고 HandlerExceptionResolver를 구현한 구현체는 총 4가지가 존재한다.
SimpleMappingExceptionResolver |
DefaultHandlerExceptionResolver |
ResponseStatusExceptionResolver |
ExceptionHandlerExceptionResolver |
전략 패턴으로 구성되어 있어 필요에 따라 사용하는 방식인데 전자정부프레임워크에선 대표적으로 SimpleMappingExceptionResolver를 사용했다.
Spring Boot에선 SimpleMappingExceptionResolver를 제외한 3가지의 방식을 기본 등록하고 사용하도록 만들어져있다. 다만 실무에서는 ExceptionHandlerExceptionResolver를 주로 쓰게되며 스프링과 스프링부트에서 예외 처리의 가장 좋은 방식이라고 알려져 있다.
ExceptionHandlerExceptionResolver 구현체
ExceptionHandlerExceptionResolver를 사용 하는 방법은
@Controller 또는 @ControllerAdvice에 선언된 @ExceptionHandler 을 통해 에러를 처리한다.
@Controller
@Controller에 선언된 @ExceptionHandler는 해당 컨트롤러에서 발생한 예외를 공통 처리한다.
@Controller
public class ErrorTestController {
@RequestMapping("/")
public String test(){
throw new NullPointerException();
}
@ExceptionHandler(value = NullPointerException.class)
public String nullPointerException(NullPointerException ex){
return "nullPointerException execute";
}
}
@ControllerAdvice
@ControllerAdvice에 선언된 @ExceptionHandler는 전범위에 걸쳐 발생한 예외를 공통 처리해줄 수 있다.
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorMessage> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorMessage message = new ErrorMessage(
HttpStatus.NOT_FOUND.value(),
new Date(),
ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<ErrorMessage>(message, HttpStatus.NOT_FOUND);
}
}
예외에 대해 자세히 알아보니 배울 것도 많고 제대로 구현하기에 어려워서 꽤나 재밌었고 실무에서 베스트로 구현된 방법은 조금 더 고민해봐야 할 듯 하다.
'🍃 Spring' 카테고리의 다른 글
@PostConstruct와 bean 생명주기 (0) | 2022.09.19 |
---|---|
Redirect와 Forward 차이점, 특징 및 실무 사용법 (0) | 2022.09.15 |
Spring Boot에서 파일 저장을 위한 상대경로 getRealPath() 사용 금지 (0) | 2022.06.14 |
Spring Boot application Property와 yml 작성 방법 (0) | 2021.04.30 |
@RequestParam, @RequestBody, @ModelAttribute, HttpServletRequest 사용법 (0) | 2021.03.24 |