[Spring] Throwable

Spring 예외처리

예외 처리의 목적은 서버에서 발생된 예외 상황이 최종 사용자에게 전달되는 것을 방지하기 위함이다. Spring 에서는 이를 위해 DAO 나 Service 에서 발생된 예외를 Controller 로 모아 처리하는 방법을 취한다.

조금 더 자세히 말하면 System 예외(Database SQL Error 등)가 발생하면 이를 Business 예외로 다시 던지는 re-throwing 방식을 사용한다. 이 때, @ExceptionHandler@ControllerAdvice 를 사용하면 된다.

이렇게 예외를 re-throwing 할 때, 개발자가 한 가지 결정해줘야 할 것이 바로 Exception 을 상속받은 Checked Excpetion 을 만들 것인지, RuntimeException 을 상속받은 Unchecked Exception 으로 만들지이다.

논란의 여지가 있다고는 하는데.. RuntimeExeption 을 상속받은 Unchecked Exception 으로 만드는 것이 자동으로 앞단 Contoller 에 예외를 전달하기 수월해 이와 같은 방법을 채택한다고 한다.

※ Checked vs Unchecked 간단하게 말하면 명시적으로 try ~ catch 를 통해 잡아줘야하는 것을 Checked Exception, 런타임에 자동으로 잡아주는 것을 Unchecked Exception 이라 한다.

Spring 예외처리 특징

컨트롤러 기반

Layered Architecture 의 DAO 단이나 Service 단에서 발생한 예외가 Controller 로 전달되어 처리된다는 특징을 가진다. 이런 부가기능을 제공하는 Advice 클래스를 작성해야 하며, XML 설정 파일에 <aop:config>를 이용해 관점을 설정한다.

글로벌 Exception 핸들러

예외처리는 cross-cutting concern, 어플리케이션 전체에 포인트컷이 적용되어야 한다. 즉, 모든 과정에서 발생하는 예외를 처리한다는 뜻이다. @CongrollerAdvice 어노테이션을 포함한 클래스는 따라서 전역 예외처리 컨트롤러가 된다. 그리고 컨트롤러에서 캐치한 예외는 View 또는 JSON 응답으로 전해지게 된다.

@ControllerAdvice

스프링 3.2 이상에서 사용할 수 있는 어노테이션으로 @Controller@RestController 에서 발생하는 예외를 catch 한다. 스프링 4.0 이상에서는 특정 컨트롤러만 지정해 catch 하는 기능까지 사용할 수 있다.

해당 컨트롤러 어드바이스도 역시 빈이기 때문에 사용하기 위해서는 servelt-context.xml 에서 ControllerAdvice 를 include 해주어야 한다. 등록이 완료되면 @ControllerAdvice 로 빈(Bean)을 만들어주고, 내부에 @ExceptionHandler(Exception Type) 을 지정해 예외를 인자로 받는 메소드에 예외 타입을 적어주면, 발생한 에러에 따라 메서드를 동작시킬 수 있다.

Spring 4.0 이후부터는 @ControllerAdvice(annotation=RestController.class) 로 지정해주면 RestController 에만 대응되도록 만들 수 있다. 즉, 자동적으로 반환되는 방식이 JSON 형태가 된다.

MyBatis 예외처리

JDBC 관련 코드는 대부분 SQLException 이다. 이러한 SQLException 은 복구가 불가능한 예외이기 때문에 Spring-ByBatis 는 SQLException을 Unchecked 특징을 가진 RuntimeException 인 DataAccessException 으로 re-throwing 한다.

이렇게 전달된 DataAccessException 에서 SQLException 을 추출해 getErrorCode() 메서드를 사용해 에러코드를 확인하는 것이 가능하다.

예외처리 실습

먼저 BizException 이란 이름으로 사용자 정의 예외를 만든다. 이 때, Unchecked Exception 인 RuntimeException 을 상속한다. 그리고 Throwable 과 String msg 를 인자로 받는 생성자까지 세 가지를 아래와 같이 만들어 준다.

package com.example.hello.exception;

public class BizException extends RuntimeException{
	public BizException() {
		super();
	}
	
	public BizException(Throwable t) {
		super(t);
	}
	
	public BizException(String msg) {
		super(msg);
	}
}

위 BizException 타입 에러를 처리할 ControllerAdvice 를 만들기 위해 아래와 같이 객체를 생성하고 내부에 핸들러에 클래스 타입을 지정해준다.

package com.example.hello.controller;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import com.example.hello.exception.BizException;

@ControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler(BizException.class)
	public String handleBizException(Exception e, Model model) {
		// error info
		model.addAttribute("type", e.getClass().getSimpleName());
		model.addAttribute("msg", e.getMessage());
		
		// error.jsp 에 model 전달
		return "error";
	}
}

이후 Controller 에서 GET으로 접근되는 URL 중 한 곳에 testService() 메소드를 작성해준다.

@GetMapping("")
public String index() {
    bbsService.testService();
    return "index";
}

이 때, 주입된 bbsService 빈(Bean)은 다음과 같다. 중간에 throw new BizException("testService fail"); 을 통해 강제로 예외를 던지는 것을 확인할 수 있다.

package com.example.hello.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.hello.dao.ArticleDAO;
import com.example.hello.exception.BizException;
import com.example.hello.vo.Article;

@Service
public class BbsService {
	@Autowired
	private ArticleDAO articleDAO;
	
	/* ... */
	
	public void testService() {
		System.out.println("executed");
		throw new BizException("testService fail");
	}
}

이제 서버를 실행해 Postman 으로 GET 요청을 전달하면 아래와 같이 예외가 정상적으로 발생해 jsp 파일로 전달되는 것을 확인할 수 있다.

이와 비슷하게 @ExcptionHandler(MyBatisSystemException.class) 를 사용하면 Service 단에서 발생하는 SQLExcption 을 처리할 수 있다.

Updated:

Leave a comment