web developer

[java] try-catch를 이용한 직접 예외 처리와 @ControllerAdvice를 통한 공통 예외 처리 본문

Language/Java

[java] try-catch를 이용한 직접 예외 처리와 @ControllerAdvice를 통한 공통 예외 처리

trueman 2024. 9. 19. 13:39
728x90
728x90

1. 서비스 단에서 커스텀 예외와 일반적인 예외를 throw


서비스 계층에서는 예외를 발생시키기만 하고, 컨트롤러에서 처리하도록 합니다.

  • 커스텀 예외 : 비즈니스 로직에 맞는 예외를 정의하고 서비스에서 명시적으로 발생시킬 수 있습니다.
  • 일반적인 예외 : Java 표준 예외나 라이브러리에서 제공하는 예외를 발생시킬 수 있습니다. 예를 들어, 데이터 검증이나 null 처리 같은 경우에는 *IllegalArgumentException*이나 *NullPointerException*을 사용합니다.
@Service
public class UserService {

    public User getUserById(Long userId) {
        if (userId == null) {
            // 일반적인 예외 throw
            throw new IllegalArgumentException("사용자 ID는 null일 수 없습니다.");
        }

        User user = userRepository.findById(userId);
        if (user == null) {
            // 커스텀 예외 throw
            throw new UserNotFoundException("사용자를 찾을 수 없습니다: " + userId);
        }
        return user;
    }

    public void updateUser(User user) {
        if (user == null) {
            // 일반적인 예외 throw
            throw new NullPointerException("업데이트할 사용자 정보가 null입니다.");
        }
        // 업데이트 로직...
    }
}

 


2. Controller에서 직접 try-catch를 통한 예외 처리


Controller에서는 서비스에서 발생한 예외 중 중요한 예외만 처리합니다. 예를 들어, 사용자에게 특정한 메시지를 반환해야 하거나, 특정 상황에서만 처리해야 하는 예외를 Controller에서 잡아 처리합니다

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        try {
            User user = userService.getUserById(id);
            return new ResponseEntity<>(user, HttpStatus.OK);
        } catch (UserNotFoundException e) {
            // 사용자에게 보여줄 예외는 여기서 처리
            return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
        }
    }
}


예를 들어 AJAX 요청에 대한 서버 측의 에러 처리 방식에 대해서 직접 try-catch를 통해 에러를 처리하고 리다이렉트하거나 에러 페이지를 전송하는 경우는 다음과 같이 처리된다.

  1. 서버가 클라이언트로 HTTP 응답을 반환할 때 발생하는 HTTP 상태 코드에 맞게 web.xml에서 정의한 에러 페이지로 리다이렉트
    • 웹 애플리케이션에서 서버 측의 에러를 특정 HTTP 상태 코드에 매핑하여, 에러 페이지로 리다이렉트하는 방식입니다. 이 방식은 web.xml에 정의된 에러 페이지 매핑을 활용합니다. try-catch를 사용하여 수동으로 에러를 처리하고 리다이렉트하는 대신, web.xml 설정을 통해 자동으로 에러 페이지로 이동하게 됩니다.
  2. SQL 데이터베이스 관련 에러 발생 시 response.sendError를 통해 에러 페이지로 이동
    • try-catch 블록 내에서 예외를 처리하고, response.sendError를 호출하여 특정 에러 페이지로 리다이렉트하는 방식입니다. 이 방법은 특정 예외를 잡아내고, 이를 클라이언트에게 전송하여 적절한 에러 페이지를 표시하도록 합니다.

 


3. @ControllerAdvice를 통한 공통 예외 처리


  • @ControllerAdvice: 모든 컨트롤러에서 발생하는 예외를 전역적으로 처리할 수 있는 클래스입니다. 이를 통해 여러 컨트롤러에서 공통적인 예외 처리 로직을 구현할 수 있습니다.
  • @ExceptionHandler: 특정 예외가 발생했을 때 호출되는 메서드를 정의합니다. 이 메서드는 예외 타입에 따라 호출되어 적절한 처리를 할 수 있습니다.

3-1. web.xml 설정 확인

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

 

Spring의 DispatcherServlet이 설정되어 있어야 합니다. 이는 Spring MVC 컨텍스트에서 필수적이며, 보통 web.xml 파일에 설정되어 있습니다.

3-2. dispatcher-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <!-- 어노테이션 기반의 Spring MVC 설정 -->
    <mvc:annotation-driven />

    <!-- 뷰 리졸버, 메시지 컨버터, 인터셉터 등의 추가 설정 -->
</beans>

 

해당 파일에 <mvc:annotation-driven />을 추가하면, Spring MVC의 어노테이션 기반 기능(@Controller, @RequestMapping, @ExceptionHandler, @ControllerAdvice 등)이 활성화됩니다.

 

3-3. pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.3.25.RELEASE</version>
</dependency>

 

pom.xml에서 Spring의 버전에 따른 의존성 주입은 프로젝트의 Spring 버전에 맞춰서 이루어져야 합니다.

 

3-4. AbstractAnnotationExceptionHandler 클래스를 상속받아 @ControllerAdvice 구현

이 클래스를 상속받아 표준 프레임워크에서 정의한 예외들을 처리할 수 있습니다. AbstractAnnotationExceptionHandler는 예외 처리 메서드를 구현할 때, 공통된 처리를 위한 기본적인 기능을 제공합니다.

GlobalExceptionHandler.java [ver1]
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.ui.Model;
import com.example.framework.AbstractAnnotationExceptionHandler;
import com.example.framework.EgovBizException;
import com.example.framework.FdlException;

@ControllerAdvice
public class GlobalExceptionHandler extends AbstractAnnotationExceptionHandler {

    @ExceptionHandler(EgovBizException.class)
    public String handleEgovBizException(EgovBizException ex, Model model) {
        model.addAttribute("errorMessage", "Business Error: " + ex.getMessage());
        return "bizErrorPage";
    }

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(RuntimeException ex, Model model) {
        model.addAttribute("errorMessage", "Runtime Error: " + ex.getMessage());
        return "runtimeErrorPage";
    }

    @ExceptionHandler(FdlException.class)
    public String handleFdlException(FdlException ex, Model model) {
        model.addAttribute("errorMessage", "FDL Error: " + ex.getMessage());
        return "fdlErrorPage";
    }

    @ExceptionHandler(Exception.class)
    public String handleGenericException(Exception ex, Model model) {
        model.addAttribute("errorMessage", "Unknown Error: " + ex.getMessage());
        return "genericErrorPage";
    }
}

 

@ExceptionHandler 메서드는 각 예외 타입에 대해 처리 메서드를 정의합니다. model.addAttribute()를 사용하여 에러 메시지를 뷰에 전달하고, 적절한 에러 페이지를 반환합니다.

 

GlobalExceptionHandler.java [ver2]
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(EgovBizException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ErrorResponse handleEgovBizException(EgovBizException ex) {
        logger.error("EgovBizException 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("처리 중 문제가 발생했습니다. 다시 시도해 주세요.");
    }

    @ExceptionHandler(DataAccessException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponse handleDataAccessException(DataAccessException ex) {
        logger.error("DataAccessException 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("데이터베이스 오류가 발생했습니다. 나중에 다시 시도해 주세요.");
    }

    @ExceptionHandler(FdlException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponse handleFdlException(FdlException ex) {
        logger.error("FdlException 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("시스템 설정 오류가 발생했습니다. 나중에 다시 시도해 주세요.");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponse handleGenericException(Exception ex) {
        logger.error("Unexpected Exception 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("예상치 못한 오류가 발생했습니다. 나중에 다시 시도해 주세요.");
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public ErrorResponse handleNoHandlerFoundException(NoHandlerFoundException ex) {
        logger.error("NoHandlerFoundException 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("찾을 수 없는 페이지입니다.");
    }
}

 

에러 페이지를 반환하지 않고, 에러를 사용자와 기술자에게 반환합니다.

 

ErrorResponse.java
public class ErrorResponse {

    private String userMessage; // 사용자에게 표시할 메시지

    // 생성자
    public ErrorResponse(String userMessage) {
        this.userMessage = userMessage;
    }

    // 사용자에게 보여줄 메시지
    public String getUserMessage() {
        return userMessage;
    }

    public void setUserMessage(String userMessage) {
        this.userMessage = userMessage;
    }
}

 

오류 응답을 사용자에게 전달하기 위한 간단한 ErrorResponse 클래스입니다.


4. 테스트


ErrorResponse.java
public class ErrorResponse {

    private String userMessage; // 사용자에게 표시할 메시지

    // 생성자
    public ErrorResponse(String userMessage) {
        this.userMessage = userMessage;
    }

    // 사용자에게 보여줄 메시지
    public String getUserMessage() {
        return userMessage;
    }

    public void setUserMessage(String userMessage) {
        this.userMessage = userMessage;
    }
}

 

GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(DataIntegrityViolationException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    @ResponseBody
    public ErrorResponse handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
        logger.error("[GlobalExceptionHandler][test] | DataIntegrityViolationException 발생: ", ex); // 기술적 정보 로그
        return new ErrorResponse("[GlobalExceptionHandler][test] | Data integrity error");
    }
}

 

EgovController.java
@GetMapping("/exception.do")
public String exception1(){
    throw new DataIntegrityViolationException("[EgovController][test] | Duplicate user found");
}

 

log 
2024-09-19 21:25:20,816 DEBUG [org.springframework.web.servlet.DispatcherServlet] DispatcherServlet with name 'action' processing GET request for [/exception.do]
2024-09-19 21:25:20,817 DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] Looking up handler method for path /exception.do
2024-09-19 21:25:20,823 DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping] Returning handler method [public java.lang.String board.sample.web.EgovSampleController.exception1()]
2024-09-19 21:25:20,823 DEBUG [org.springframework.beans.factory.support.DefaultListableBeanFactory] Returning cached instance of singleton bean 'egovSampleController'
2024-09-19 21:25:20,825 DEBUG [org.springframework.web.servlet.DispatcherServlet] Last-Modified value for [/exception.do] is: -1
2024-09-19 21:25:20,849 DEBUG [org.springframework.beans.factory.support.DefaultListableBeanFactory] Returning cached instance of singleton bean 'globalExceptionHandler'
2024-09-19 21:25:20,849 DEBUG [org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver] Invoking @ExceptionHandler method: public board.cmmn.ErrorResponse board.cmmn.GlobalExceptionHandler.handleDataIntegrityViolationException(org.springframework.dao.DataIntegrityViolationException)
2024-09-19 21:25:20,849 ERROR [board.cmmn.GlobalExceptionHandler] [GlobalExceptionHandler][test] | DataIntegrityViolationException 발생: 
org.springframework.dao.DataIntegrityViolationException: [EgovController][test] | Duplicate user found
	at board.sample.web.EgovSampleController.exception1(EgovSampleController.java:2930) ~[classes/:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) ~[spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:854) ~[spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:765) ~[spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) [spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) [spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:489) [servlet-api.jar:?]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:583) [servlet-api.jar:?]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:212) [catalina.jar:8.5.91]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) [catalina.jar:8.5.91]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) [tomcat-websocket.jar:8.5.91]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181) [catalina.jar:8.5.91]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) [catalina.jar:8.5.91]
	at egovframework.rte.ptl.mvc.filter.HTMLTagFilter.doFilter(HTMLTagFilter.java:51) [egovframework.rte.ptl.mvc-3.10.0.jar:?]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181) [catalina.jar:8.5.91]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) [catalina.jar:8.5.91]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) [spring-web-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.25.RELEASE.jar:4.3.25.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181) [catalina.jar:8.5.91]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) [catalina.jar:8.5.91]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) [catalina.jar:8.5.91]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) [catalina.jar:8.5.91]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) [catalina.jar:8.5.91]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130) [catalina.jar:8.5.91]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) [catalina.jar:8.5.91]
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:682) [catalina.jar:8.5.91]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [catalina.jar:8.5.91]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [catalina.jar:8.5.91]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:617) [tomcat-coyote.jar:8.5.91]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) [tomcat-coyote.jar:8.5.91]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:932) [tomcat-coyote.jar:8.5.91]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1694) [tomcat-coyote.jar:8.5.91]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) [tomcat-coyote.jar:8.5.91]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-util.jar:8.5.91]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-util.jar:8.5.91]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:8.5.91]
	at java.lang.Thread.run(Thread.java:834) [?:?]
2024-09-19 21:25:21,017 DEBUG [org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor] Written [board.cmmn.ErrorResponse@446e0fd9] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@4be59e33]
2024-09-19 21:25:21,017 DEBUG [org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver] Resolved [org.springframework.dao.DataIntegrityViolationException: [EgovController][test] | Duplicate user found]
2024-09-19 21:25:21,017 DEBUG [org.springframework.web.servlet.DispatcherServlet] Null ModelAndView returned to DispatcherServlet with name 'action': assuming HandlerAdapter completed request handling
2024-09-19 21:25:21,017 DEBUG [org.springframework.web.servlet.DispatcherServlet] Successfully completed request

 

view 화면

view 화면


참조

  1. https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte:bsl:exception_handling
  2. https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte:fdl:aop:egovrteaopguide
  3. https://incheol-jung.gitbook.io/docs/q-and-a/spring/controlleradvice-exceptionhandler

 

 

728x90
728x90