| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 컴퓨터 구조
- SpringBoot
- 백엔드공부
- JWT
- restTemplate
- 개발자
- TestCode
- 백엔드스쿨
- SpringSecurity
- 컴퓨터 공학
- spring boot
- JPA
- 제로베이스
- 백엔드
- queue
- 프로그래머스
- 서버
- spring
- actuator
- 자료구조
- 스프링부트
- array
- ORM
- 연관관계
- 핵심가이드
- error
- QueryDSL
- 백준
- LinkedList
- Java
- Today
- Total
be_better
[Springboot] 유효성 검사와 예외 처리 - 예외 처리 본문
목차
예외 처리
애플리케이션을 개발할 때는 불가피하게 많은 오류가 발생하게 된다. 자바에서는 이러한 오류를 try/catch, throw 구문을 활용해 처리한다. 스프링 부트에서는 더욱 편리하게 예외 처리를 할 수 있는 기능을 제공한다.
예외와 에러
프로그래밍에서 예외(exception)란 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황을 의미한다. 예외는 개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통해 처리할 수 있다.
다음으로 에러(error)는 소프트웨어공학 관점으로는 예외와 엄연히 다르게 사용되는 용어이다. 에러는 주로 자바의 가상머신에서 발생시키는 것으로서 예외와 달리 애플리케이션 코드에서 처리할 수 있는 것이 거의 없다.
대표적인 예로 메모리 부족(OutOfMemory), 스택 오버플로(StackOverFlow) 등이 있다.
예외 클래스

모든 예외 클래스는 Throwable 클래스를 상속받는다. 그리고 가장 익숙하게 볼 수 있는 Exception 클래스는 다양한 자식 클래스를 가지고 있다. 이 클래스는 크게 Checked Exception과 Unchecked Exception으로 구분할 수 있다.
- CheckedException
- 반드시 예외 처리 필요
- 컴파일 단계에서 확인
- 대표 클래스
- IOException
- SQLException
- UnCheckedException
- 명시적 처리를 강제하지 않음
- 실행 중 단계에서 확인
- 대표 클래스
- RuntimeException
- NullpointerException
- IllegalArgumentException
- IndexOutOfBoundException
- SystemException
Checked Exception은 컴파일 단계에서 확인 가능한 예외 상황이고 IDE에서 캐치해서 반드시 예외 처리를 할 수 있게 표시해준다. 반면 Unchecked Exception은 런타임 단계에서 확인되는 예외 상황을 나타낸다. 문법상 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 에외를 의미한다.
예외 처리 방법
예외가 발생했을 때 이를 처리하는 방법은 크게 세 가지가 있다.
- 예외 복구
- 예외 처리 회피
- 예외 전환
예외 복구
예외 복구 방법은 예외 상황을 파악해서 문제를 해결하는 방식이다. 대표적으로 try/catch 구문이 있다.
try 블록에는 예외가 발생할 수 있는 코드를 작성한다. 개발자가 직접 작성한 로직이라면 예외 상황을 예측해서 try 블록에 포함시켜야 한다. 그리고 catch 블록을 통해 try 블록에서 발생하는 예외 상황을 처리하는 내용을 작성한다. 이때 catch 블록은 여러 개를 작성할 수 있다.
int a = 1;
String b = "a";
try {
System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
b = "2";
System.out.println(a + Integer.parseInt(b));
}
예외 처리 회피
예외가 발생할 시점에서 바로 처리하는 것이 아니라 예외가 발생할 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식이다. 이때 throw 키워드를 사용해 어떤 예외가 발생했는지 호출부에 내용을 전달할 수 있다.
int a = 1;
String b = "a";
try {
System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
throw new NumberFormatException("숫자가 아닙니다.");
}
예외 전환
이 방법은 앞의 두 방식을 적절하게 섞은 방식이다. 예외가 발생했을 때 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하면서 좀 더 적합한 예외 타입으로 전달할 필요가 있다. 또한 애플리케이션에서 예외 처리를 좀 더 단순하게 하기 위해 래핑(wrapping)해야 하는 경우도 있다. try/catch 방식을 사용하면서 catch 블록에서 throw 키워드를 사용해 다른 예외 타입으로 전달하면 된다.
스프링 부트의 예외 처리 방식
웹 서비스 애플리케이션에서는 외부에서 들어오는 요청에 담긴 데이터를 처리하는 경우가 많다. 그 과정에서 예외가 발생하면 예외를 복구해서 정상으로 처리하기보다는 요청을 보낸 클라이언트에 어떤 문제가 발생했는지 상황을 전달하는 경우가 많다.
예외가 발생했을 때 클라이언트에 오류 메시지를 전달하려면 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 한다. 스프링 부트에서 처리하는 방식은 크게 두가지가 있다.
- @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
- @ControllerAdvice 대신 @RestControllerAdvice를 사용하면 결과값을 JSON 형태로 반환할 수 있다.
- @ExceptionHandler를 통해 특정 컨트롤러의 예외를 처리

@RestControllerAdvice를 활용한 핸들러 클래스 생성
@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(
RuntimeException e, HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
log.info("Advice 내 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);
}
}
@RestControllerAdvice나 @ControllerAdvice 어노테이션은 @Controller나 @RestController에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능을 수행한다. 즉, 다음과 같이 별도 설정을 통해 예외를 관제하는 범위를 지정할 수 있다.
@RestControllerAdvice(basePackages = "com.springboot.valid_exception")
@ExceptionHandler는 @Controller나 @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의할 때 사용한다. 어떤 예외 클래스를 처리할지는 value 속성으로 등록한다.
예제에서는 RuntimeException이 발생하면 처리하도록 코드를 작성했으므로 RuntimeException에 포함되는 각종 예외가 발생할 경우를 모두 포착하여 처리한다.
ExceptionController 클래스
@RestController
@RequestMapping("/exception")
public class ExceptionController {
@GetMapping
public void getRuntimeException() {
throw new RuntimeException("getRuntimeException 메서드 호출");
}
}
실행 결과

log

이처럼 컨트롤러에서 던진 예외는 @ControllerAdvice 또는 @RestControllerAdvice가 선언되어 있는 핸들러클래스에서 매핑된 예외 타입을 찾아 처리하게 된다.
두 어노테이션은 별도 범위 설정이 없으면 전역 범위에서 예외를 처리하기 때문에 특정 컨트롤러에서만 동작하는 @ExceptionHandler 메서드를 생성해서 처리할 수도 있다.
컨트롤러 클래스 내 handleException() 생성
@Slf4j
@RestController
@RequestMapping("/exception")
public class ExceptionController {
@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;
log.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);
}
}
이처럼 해당 클래스에 국한해서 예외 처리를 할 수 있다.
log

예외 처리 우선순위
컨트롤러 또는 @ControllerAdvice 클래스 내에 동일하게 핸들러 메서드가 선언된 상태에서는
예를 들면) Exception 클래스보다 좀 더 구체화 되어있는 NullPointerException 클래스가 우선순위를 갖게 된다.
다음으로, @ControllerAdvice의 글로벌 예외 처리와 @Controller 내의 컨트롤러 예외 처리에 동일한 타입의 예외 처리를 하게 되면 범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위를 갖게 된다.
커스텀 예외
개발할 때의 대부분의 예외 상황에서는 자바에서 이미 적절한 상황에 사용할 수 있도록 제공하는 표준 예외(Standard Exception)를 사용하면 해결된다. 그럼에도 커스텀 예외(Custom Exception)를 만들어 사용하는 이유는 다음과 같다.
커스텀 예외를 만들어서 사용하면 네이밍에 개발자의 의도를 담을 수 있기 때문에 이름만으로도 어느 정도 예외 상황을 짐작할 수 있다.
또한 커스텀 예외를 사용하면 애플리케이션에서 발생하는 예외를 개발자가 직접 관리하기가 수월해지고 표준 예외를 상속받은 커스텀 예외들을 개발자가 직접 코드로 관리하기 때문에 책임 소재를 애플리케이션 내부로 가져올 수 있게 된다.
표준 예외를 사용하면 의도하지 않은 예외 상황도 정해진 예외 처리 코드에서 처리하기 때문에 어디에서 문제가 발생했는지 확인하기가 어렵다. 그러나 커스텀 예외로 관리하면 의도하지 않았던 부분에서 발생한 예외는 개발자가 관리하는 예외 처리 코드가 처리하지 않으므로 개발 과정에서 혼동할 여지가 줄어든다.
커스텀 예외 클래스 생성하기
커스텀 예외 클래스를 생성하는 데 필요한 내용은 다음과 같다
- 에러 타입(error type): HttpStatus의 reasonPhrase
- 에러 코드(error code): HttpStatus의 value
- 메시지(message): 상황별 상세 메시지

애플리케이션에서 가지고 있는 도메인 레벨을 메시지에 표현하기 위한 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. ";
}
}
}
열거형은 별도로 생성해도 무관하지만 상수 개념으로 사용하기 때문에 앞으로의 확장성을 위해 Constants라는 상수들을 통합 관리하는 클래스를 생성하고 내부에 ExceptionClass를 선언한다.

커스텀 예외 클래스 생성 CustomException.java
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;
}
}
ExceptionHandler 클래스에 CustomException 예외 처리 코드 추가
@ExceptionHandler(value = CustomException.class)
public ResponseEntity<Map<String, String>> handleException(
CustomException e, HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
log.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를 정의해서 전달하기 때문에 클라이언트 요청에 따라 유동적인 응답 코드를 설정할 수 있다는 장점이 있다.
테스트를 위해 ExceptionController 수정
@GetMapping("/custom")
public void getCustomException() throws CustomException {
throw new CustomException(
Constants.ExceptionClass.PRODUCT,
HttpStatus.BAD_REQUEST,
"getCustomException 메서드 호출"
);
}
실행 결과

log

'Springboot > 이론' 카테고리의 다른 글
| [Springboot] 액추에이터 활용하기 - 액추에이터 기능 (0) | 2023.11.29 |
|---|---|
| [Springboot] 액추에이터 활용하기 - 엔드포인트 (0) | 2023.11.29 |
| [Springboot] 유효성 검사와 예외 처리 - 유효성 검사 (0) | 2023.11.28 |
| [Springboot] 연관관계 매핑 - 영속성 전이 (0) | 2023.11.19 |
| [Springboot] 연관관계 매핑 - 다대다 매핑 (0) | 2023.11.18 |