Spring으로 방명록 애플리케이션을 구현하는 시리즈입니다.
- [Spring] 방명록 애플리케이션 (1) - 환경 설정
- [Spring] 방명록 애플리케이션 (2) - 준비 단계 ( 스프링 활용하기 )
- [Spring] 방명록 애플리케이션 (3) - 구현
- [Spring] 방명록 애플리케이션 (4) - 정적 파일 처리 ( DefaultServletHandler )
- [Spring] 방명록 애플리케이션 (5) - 뷰 객체 생성 ( ViewResolver )
- [Spring] 방명록 애플리케이션 (6) - 예외 처리 ( ExceptionHandler )
- [Spring] 방명록 애플리케이션 (7) - 3 Layer Architecture와 Service 계층
- [Spring] 방명록 애플리케이션 (8) - 커넥션 풀 ( Connection Pool ) DBCP
- [Spring] 방명록 애플리케이션 (9) - Mybatis 환경 설정
- [Spring] 방명록 애플리케이션 (10) - Mybatis 적용
효율적인 예외 처리
DB에 접근하는 DAO 객체의 메서드에서 SQL Exception이 발생할 수 있습니다.
그래서 DAO를 깔끔하게 처리 하기 위해 자신이 예외를 처리하지 않고, 예외 던지기 throws를 한다면,
3 - layer에서 DAO와 연결되어 있는 service 계층에서 SQL Exception을 처리해야 합니다.
그런데 비즈니스 로직을 처리하는 계층인 service입장에서 SQL은 기술적인 부분입니다.
service는 "유저가 없다"는 예외 같이 비즈니스와 관련된, 자신에게 의미 있는 예외만 받는 것이 좋습니다.
service 입장에서 SQL이 작동하든 말든 관심이 없습니다.
이것이 논리적으로 layer를 나눈 이유이기도 하고요.
그래서 SQL Exception이 발생하면 DAO에서 직접 처리해야 할 것입니다.
그런데 예외 처리는 증괄호가 많기 때문에 가독성을 매우 떨어뜨립니다.
게다가 예외 처리 과정도 다음과 같이 일관됩니다.
1) 예외에 대한 로그를 남긴다.
2) 클라이언트에게 에러 페이지 보여준다.
이에 따라 DAO에서 예외가 발생했을 경우 예외 처리 과정을 한 곳에서 처리하도록 하도록 하려고 합니다.
즉 exception이 발생하면 throws로 예외 던지기를 하지 말고 전환을 통해 한 곳에서 예외 처리를 하도록 할 것입니다.
com.victolee.guestbook.exception 패키지를 만들고 GuestbookExcpetion 클래스를 만들도록 하겠습니다.
GuestbookExcpetion
public class GuestbookExcpetion extends RuntimeException { private static final long serialVersionUID = 1L; public GuestbookExcpetion() { super("GuestBookDAOException Occurs"); } public GuestbookExcpetion(String msg) { super(msg); } }
이제 DAO에서 직접 예외를 처리하던 ( e.printStackTrace() 메서드 ) try - catch 부분을 아래와 같이 수정하겠습니다.
이렇게 작성을 하면 DAO에서 예외가 발생했을 경우 GuestbookException에서 처리할 수 있습니다.
즉 DAO의 모든 메서드마다 로그를 남기고, 에러 페이지를 보여주는 일관된 과정을 GuestbookException으로 예외를 던지면 GuestbookException에서 모든 예외를 처리할 수 있습니다.
DAO에서 예외가 발생하면 service 계층에서 예외를 처리해야 하는데,
service 계층에서도 예외를 처리하지 않으면 Controller로 예외가 전달됩니다.
따라서 DAO에서 발생한 예외를 Controller에서 받아 GuestbookException에서 예외가 처리되도록 Controller의 메서드를 작성하겠습니다.
예외를 처리하라는 핸들러인 @ExceptionHandler 어노테이션을 작성하면 DAO에서 발생한 예외를 받을 수 있습니다.
자 이제 DAO에서 예외가 발생하면 GuestbookExcpetion 예외가 발생하도록 예외를 전환하게 되고,
Controller에서 이를 받아 처리하게 됩니다.
( 여기서는 로그를 남기는 과정을 생략하고, 에러 페이지만 응답하도록 작성했습니다. )
이제 views/error 폴더에 error.jsp 파일을 작성하고
DAO에서 쿼리를 일부로 틀리게 작성하여 요청을 해보시면 에러 페이지를 응답하는 것을 확인할 수 있습니다.
Global Exception
그런데 위의 예외 처리도 문제가 있습니다.
모든 컨트롤러 마다 @ExceptionHandler 어노테이션이 붙은 메서드를 작성해야 한다는 것이죠.
코드의 중복은 피하는 것이 좋기 때문에 이를 해결하려고 합니다.
이 방법은 Controller 마다 예외를 처리할 필요 없이 예외를 처리하는 클래스에 @ControllerAdvice 어노테이션을 작성하면 글로벌하게 예외를 처리할 수 있습니다.
GlobalExceptionHandler
@ControllerAdvice public class GlobalExceptionHandler extends RuntimeException { // 모든 예외 처리 @ExceptionHandler(Exception.class) // 기술침투는 하지 않는 것이 좋지만 뷰 페이지로 돌려야 하기 때문에 어쩔 수 없이 HttpServlet 객체를 사용 public void handlerException(HttpServletRequest request, HttpServletResponse response, Exception e) throws Exception{ // 1. 로깅 // 로그는 파일로 남겨야 하지만 지금은 jsp페이지로 보내기로 하자 StringWriter error = new StringWriter(); e.printStackTrace(new PrintWriter(error)); request.setAttribute("error", error); // 2. 에러페이지 // 컨트롤러가 아니기 때문에 뷰 페이지의 full path를 작성해야 함 request.getRequestDispatcher("/WEB-INF/views/error/error.jsp").forward(request, response); } }
로깅 과정은 로그를 남기는 과정을 흉내 낸 것입니다.
실제로는 logback 라이브러리를 통해 로그 파일로 로그를 기록하는 것이 좋습니다.
그리고 클라이언트에게 보여줄 에러 페이지를 반환합니다.
DAO객체에서 예외가 발생하므로 미리 Root Application Context에 GlobalExceptionHandler 객체가 저장되어 있어야 합니다.
따라서 @ControllerAdvice 어노테이션을 스캔 할 수 있도록 applicationContext.xml을 수정하도록 하겠습니다.
applicationContext.xml
GlobalExceptionHandler 클래스는 com.victolee.guestbook.exception 패키지에 있으므로 어노테이션을 스캐닝 하는 base-package에 추가했습니다.
그러면 GlobalExceptionHandler 클래스의 @ControllerAdvice 어노테이션을 읽어 들이고, 이 클래스는 예외 처리를 하는 클래스로 인식하게 됩니다.
다음으로 DAO에서 예외를 전환하는 부분을 수정하도록 하겠습니다.
GuestBookDAO
GlobalExceptionHandler 클래스에서 request scope로 error 객체를 전달했으므로, error 내용을 보기 위해 JSP파일에서 값을 찍어보도록 하겠습니다.
error.jsp
마찬가지로 DAO에서 쿼리를 일부로 틀리게 작성해서 테스트를 해볼 수 있습니다.
이상으로 스프링에서 예외를 처리하는 방법에 대해 알아보았습니다.
DAO에서 발생한 예외는 DAO에서 직접 처리하는 것이 좋은데, 매 번 똑같은 코드를 DAO의 모든 메서드 마다 작성하는 것은 좋지 않으므로 한 곳에서 예외를 처리할 수 있도록 GlobalExceptionHandler 클래스를 작성했습니다.
GlobalExceptionHandler 클래스에서는 로그를 남기고 에러 페이지를 반환하는 역할을 합니다.
모든 예외는 로그를 남기고, 에러 페이지를 렌더링 하는 과정을 수행하기 때문이죠.
이제 코드의 중복 없이 예외를 처리할 수 있게 되었습니다.