
예외와 에러
예외(Exception)
입력 값의 처리가 불가능 하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황
개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통한 처리가 필요하다.
에러(Error)
자바의 가상머신에서 발생시키는 것으로 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없다.
ex) 메모리 부족(OutOfMemory), 스택 오버플로(StackOverFlow)
코드를 살펴보면서 문제가 발생하지 않도록 예방해서 원천적으로 차단해야 한다.
예외 클래스

모든 예외 클래스는 Throwable 클래스를 상속받는다.
Exception 클래스 구분
Exception 클래스는 다양한 자식 클래스를 가지고 있다.
크게 Checked Exception과 Unchecked Exception으로 구분할 수 있다.
| Checked Exception | Unchecked Exception | |
| 처리 여부 | 반드시 예외 처리 필요 | 명시적 처리를 강제하지 않음 |
| 확인 시점 | 컴파일 단계에서 확인 가능 | 실행 중 단계에서 확인 가능 |
| 대표적인 예외 클래스 | IOException SQL Exception |
RuntimeException NullPointerException IllegalArgumentException IndexOutOfBoundException SystemException |
| IDE 캐치 여부 | IDE에서 캐치해서 반드시 예외 처리를 할 수 있게 표시 | 문법상 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외 |
| RuntimeException 상속 여부 | 모두 RuntimeException을 상속받지 않음 | 모두 RuntimeException을 상속받음 |
예외 처리 방법
예외가 발생했을 때 이를 처리하는 방법
1. 예외 복구
예외 상황을 파악해서 문제를 해결하는 방식
try/catch
예외 복구의 대표적인 방법
public static void main(String[] args) {
try {
int result = divide(10, 0); // 0으로 나누는 메서드 호출
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Exception caught: Division by zero.");
}
}
public static int divide(int num1, int num2) {
return num1 / num2; // 0으로 나누는 연산
}
| try | 예외가 발생할 수 있는 코드 작성 외부 라이브러리를 사용하는 경우엔 대체로 IDE가 try블럭을 사용하라고 알려준다. 개발자가 직접 작성한 로직은 예외 상황을 예측해 try블럭에 포함 시켜야한다. |
| catch | try에서 발생하는 예외 상황을 처리하는 내용을 작성 catch블럭을 여러개 작성 가능 - 순차적으로 거쳐 매칭되는 예외를 찾아 예외 처리 수행 |
2. 예외 처리 회피
예외 발생 시점에 바로 처리하는 것이 아닌 예외가 발생한 메서드를 호출한 곳에서 처리를 할 수 있게 전가하는 방식
public static void readFile(String filename) throws FileNotFoundException, IOException {
FileReader fileReader = new FileReader(filename);
// 파일을 읽는 작업 수행
fileReader.close();
}
| throw | 어떤 예외가 발생했는지 호출부에 내용을 전달 |
3. 예외 전환
앞의 두 방식을 적절하게 섞은 방식
예외가 발생햇을 때 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하면서 좀 더 적합한 예외 타입으로 전달할 필요가 있다.
try/catch문을 방식을 사용하면서 catch블록에서 throw 키워드를 사용해 다른 예외 타입으로 전달하면 된다.
후에 나오는 커스텀 예외를 만드는 과정에서 이 방법을 사용할 것이다.
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionConversionExample {
public static void main(String[] args) {
try {
readAndProcessFile("file.txt");
} catch (FileProcessingException e) {
System.out.println("Error processing file: " + e.getMessage());
}
}
public static void readAndProcessFile(String filename) throws FileProcessingException {
try {
FileReader fileReader = new FileReader(filename);
// 파일을 읽고 처리하는 작업 수행
fileReader.close();
} catch (FileNotFoundException e) {
// 더 구체적인 예외로 변환하여 다시 던짐
throw new FileProcessingException("File not found: " + filename, e);
} catch (IOException e) {
// 더 구체적인 예외로 변환하여 다시 던짐
throw new FileProcessingException("Error reading file: " + filename, e);
}
}
}
class FileProcessingException extends Exception {
public FileProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
스프링 부트 예외 처리 방식
보통 외부에서 들어온 데이터를 처리할 때 예외가 있으면 예외를 정상으로 복구하는 것이 아닌 어떤 문제가 있는지 알려주는 경우가 많다. 아래에서 스프링 부트에서 사용하는 예외 처리 방법을 알아보자.
예외가 발생했을 때 클라이언트에 오류 메시지를 전달하려면 컨트롤러로 전달해야한다.
스프링 부트에는 전달받은 예외를 처리하는 방식에는 모든 컨트롤러의 예외를 처리하는 방식과 특정 컨트롤러의 예외를 처리하는 방식으로 크게 두 가지가 있다.
1. 모든 컨트롤러의 예외 처리
@RestControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리할 수 있다.
@RestControllerAdvice 활용
@RestConrollerAdvice와 @ConrollerAdvice는 스프링에서 제공하는 어노테이션이다.
컨트롤러에서 던진 예외는 @RestConrollerAdvice와 @ConrollerAdvice가 선언되어 있는 핸들러 클래스에서 매핑된 예외 타입을 찾아 처리하게 된다.
발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능을 수행한다.
...
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class CustomExceptionHandler {
private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("Advice 내 exceptionHandler 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
basePackages: 예외 관제하는 범위 지정
@RestControllerAdvice(basePackages = "com.springboot.valid_exception")
위처럼 하면 예외를 관제하는 범위를 지정이 가능하다.
별도 범위 설정이 없으면 전역 범위에서 예외를 처리한다.
@ExceptionHandler(value = RuntimeException.class)
@Controller나 @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의할 때 사용
@ExceptionHandler(value = RuntimeException.class) // RuntimeException 예외 클래스를 처리하겠다.
public ResponseEntity<Map<String, String>> handleException(RuntimeException e, HttpServletRequest request) {
...
}
- value속성
- 어떤 예외를 처리할지는 value속성으로 등록한다.
- 배열 형식도 가능하다. -> 여러 예외 클래스를 등록할 수도 있다.
hanleException 메소드 내부 - 응답 메시지 리턴
클라이언트에게 오류가 발생했다는 것을 알리는 응답 메시지를 구상해서 리턴한다.
컨트롤러 메서드에 다른 리턴 타입이 설정되어 있어도 핸들러 메서드에서 별도의 리턴 타입을 지정할 수 있다.
HttpHeaders responseHeaders = new HttpHeaders();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("Advice 내 exceptionHandler 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
Map객체에 응답 메시지를 구성하고
ResponseEntity에 HttpHeader, HttpStatus, Body 값을 담아 전달한다.

2. 특정 컨트롤러 예외 처리
@ExceptionHandler를 통해 특정 컨트롤러의 예외를 처리할 수 있다.
별도 범위 설정이 없으면 전역 범위에서 예외를 처리하기 때문에 아래처럼 특정 컨트롤러 클래서 내에 @ExceptionHandler 어노테이션을 사용한 메서드를 선언해서 해당 컨트롤러에서만 예외 처리를 할 수도 있다.
@RestController
@RequestMapping("/exception")
public class ExceptionController {
private final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class);
@GetMapping
public void getRuntimeException() {
throw new RuntimeException("getRuntimeException 메소드 호출");
}
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(RuntimeException e,
HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("클래스 내 handleException 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
예외 처리 우선순위
1. 클래스 내의 핸들러 메서드가 글로벌 예외 처리인 @ControllerAdvice의 메서드보다 우선순위가 높다.

2. 같은 클래스 내의 메서드는 좀 더 구체적인 예외 클래스가 지정된 쪽이 우선순위가 더 높다.

커스텀 예외
대부분의 상황은 표준 예외(Standard Exception)를 사용하면 해결된다.
그럼 커스텀 예외를 사용하면 어떤 부분이 좋은지 알아보자.
커스텀 예외의 장점
1. 이름만으로도 예외 상황을 어느 정도 짐작할 수 있다.
개발자가 네이밍을 할 때 의도를 담을 수 있기 때문에 이름만 보고도 예외 상황을 짐작할 수 있다.
반면 표준 예외는 해당 예외 타입의 이름만으로는 이해하기 어려울 수 있어 예외 메시지를 상세히 작성해야 하는 번거로움이 있다.
2. 직접 관리하기가 수월해진다.
개발자가 직접 코드로 관리하기 때문에 책임 소재를 애플리케이션 내부로 가져올 수 있다.
이를 통해 동일한 예외를 한 곳에서 처리하며 특정 상황에 맞는 예외 코드를 적용할 수 있다.
3. 예외 상황에 대한 처리 용이
애플리케이션에서 발생하는 예외 상황들을 한 곳에 처리할 수 있다.
표준 예외는 의도한 부분과 다른 에러가 존재할 수 있는데 이 의도하지 않은 부분도 이미 정해진 의도했던 예외 처리 로직코드를 이용해서 처리해버릴 수 있다. 이렇게 되면 어디에서 문제가 발생했는지 확인하기가 어려워 질 수 있다.
커스텀 예외로 관리하면 외도한 부분은 의도한대로 처리하고 의도치 않은 부분에서 발생한 예외는 직접 작성한 처리 코드로 처리하지 않아 의도치 못한 부분을 찾기가 수월해진다.
커스텀 예외 클래스 생성하기
참고: 만드는 목적에 따라 커스텀 예외의 생성 방법이 다르다.
@ControllerAdvice와 @ExceptionHanler의 무분별한 예외 처리 방지를 위한 커스텀 예외
구조적인 설계를 통한 예외 클래스 생성 방법을 알아 보자.
상속 구조 살펴보기

- Exception
public class Exception extends Throwable {\
static final long serialVersionUID = -3387516993124229948L;
// 기본 생성자
public Exception() {
super();
}
// 메시지를 포함하는 생성자
public Exception(String message) {
super(message);
}
// 메시지와 원인(Throwable)을 포함하는 생성자
public Exception(String message, Throwable cause) {
super(message, cause);
}
// 원인(Throwable)을 포함하는 생성자
public Exception(Throwable cause) {
super(cause);
}
}
- Throwable
public class Throwable implements Serializable {
private static final long serialVersionUID = -3042686055658047285L;
private transient Object backtrace;
private String detailMessage;
// 생성자
public Throwable() {
fillInStackTrace();
}
// 메시지를 포함하는 생성자
public Throwable(String message) {
fillInStackTrace();
detailMessage = message;
}
// 예외 메시지를 반환하는 메서드
public String getMessage() {
return detailMessage;
}
// 예외 클래스명과 메시지를 반환하는 메서드
public String getLocalizedMessage() {
return getMessage();
}
...
}
Exception 클래스의 생성자가 부모의 Throwable 클래스의 생성자를 호출한다.
Throwable생성자는 message 값을 datailMessage 변수에 입력한다.
커스텀 예외를 생성해도 이 처럼 해당 message변수를 사용하한다.
참고: 커스텀 예외는 상위 예외 클래스를 상속받는다.
여기서 상속받을 예외 클래스는 의도한 예외가 발생하는 상황에 해당하는 예외 클래스다. 그래서 보통은 커스텀 예외는 상위 예외 클래스보다 좀 더 구체적으로 명시한다.
HttpStatus(Enum) 포함시키기
- HttpStatus를 커스텀 예외 클래스에 포함시키면 Handler안에서 HttpStatus를 생성해서 선언할 필요없이 예외 클래스만 전달받아 클래스 안에 HttpStatus가 포함이 되어 있는 구조로 설계할 수 있다.
// Handler 내부
HttpStatus httpStatus = HttpStatus.BAD_REQUEST; // 선언이 필요없다.
LOGGER.error("Advice 내 exceptionHandler 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
커스텀 예외 클래스 생성에 필요한 내용
1. 에러 타입(error type): HttpStatus의 reasonPhrase
2. 에러 코드(error code): HttpStatus의 value
3. 메시지(message): 상황별 상세 메시지
커스텀 예외 구조짜기

Constants: 상수들을 통합 관리하는 클래스
열거형 ExceptionClass를 상수 개념으로 사용하기 위해 Constants내부에 선언하고 상수를 통합 관리할 것이다.
- ExceptionClass: 도메인 레벨을 메시지에 표현하기 위해 ExceptionClass 열거형 타입을 생성
- 커스텀 예외 클래스의 메시지에 어떤 도메인에서 문제가 발생했는지를 보여주는데 사용할 것이다.
public class Constants {
public enum ExceptionClass { // 도메인 레벨을 메시지에 표현하기 위해 생성
PRODUCT("Product"); // 상품도메인
private String exceptionClass;
ExceptionClass(String exceptionClass) {
this.exceptionClass = exceptionClass;
}
public String getExceptionClass() {
return exceptionClass;
}
@Override
public String toString() {
return getExceptionClass() + " Exception. ";
}
}
}
커스텀예외 클래스
import com.springboot.valid_exception.common.Constants;
import org.springframework.http.HttpStatus;
public class CustomException extends Exception{
private Constants.ExceptionClass exceptionClass;
private HttpStatus httpStatus;
public CustomException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus,
String message) {
super(exceptionClass.toString() + message);
this.exceptionClass = exceptionClass;
this.httpStatus = httpStatus;
}
public Constants.ExceptionClass getExceptionClass() {
return exceptionClass;
}
public int getHttpStatusCode() {
return httpStatus.value();
}
public String getHttpStatusType() {
return httpStatus.getReasonPhrase();
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
}
- 필드 2개
앞에서 만든 두 클래스를 사용하여 필드로 가지고
두 객체를 기반으로 예외 내용을 정의하며 생성자를 통해 클래스를 초기화 해준다.- ExceptionClass
- HttpStatus
handleException() - 커스텀 예외 처리 메서드 생성
@RestControllerAdvice
public class CustomExceptionHandler {
...
@ExceptionHandler(value = CustomException.class)
public ResponseEntity<Map<String, String>> handleException(CustomException e,
HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
LOGGER.error("Advice 내 handleException 호출, {}, {}", request.getRequestURI(),
e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", e.getHttpStatusType());
map.put("code", Integer.toString(e.getHttpStatusCode()));
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, e.getHttpStatus());
}
}
위와 같이 처리하면 예외 발생 시점에 HttpStatus를 정의해서 전달하기 때문에 클라이언트 요청에 따라 유동적인 응답 코드 설정이 가능하다.
- 예외 발생 시점에 HttpStatus 정의해서 전달하기
@GetMapping("/custom")
public void getCustomException() throws CustomException {
throw new CustomException(ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST, "getCustomException 메소드 호출");
// ExceptionClass에서 도메인, HttpStatus를 통해 고른 응답 코드, 세부메시지
}
이렇게 하면 Response Body에서 예외 발생 지점에서 설정한 값이 담겨 클라이언트로 응답하는 것을 볼 수 있다.

'spring > spring' 카테고리의 다른 글
| [Spring][스프링 부트 핵심 가이드] 서버 간 통신 (0) | 2024.03.05 |
|---|---|
| [Spring][Exception] Failed to configure a DataSource (0) | 2024.03.04 |
| [Spring][스프링부트핵심가이드] 유효성 검사(Validation) (0) | 2024.02.26 |
| [Spring][Error][Kotlin][jpa] required a bean of type ... that could not be found. (0) | 2024.02.12 |
| [Spring][Error] creating bean with name 'jpaAuditingHandler (0) | 2024.02.11 |