임도현의 성장
[Spring-Boot] Exception 예외처리 본문
👀Exception(예외) 란?
Exception 는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황이나 문제를 의미합니다. 자바에서는 예외가 발생하면 프로그램의 정상적인 흐름을 방해할 수 있기 때문에 예외를 적절히 처리하여 프로그램이 중단되지 않고 계속 실행될 수 있도록 하는 것을 말합니다. 자바에서 Throwable 클래스를 기준으로 Error, Exception 클래스로 나뉘어집니다. 실행시 발생할 수 있는 프로그램 오류를 에러와 예외 두가지로 구분할 수 있습니다.

🧐Error(오류)와 Exception(예외)의 차이
- 오류 : 시스템 레벨에서 발생하며 복구가 거의 불가능한 문제 ex) 메모리 부족
- 예외 : 프로그램에서 발생할 수 있는 문제로 적절히 처리하면 복구가 가능한 문제 ex) 파일을 찾을 수 없음
🍇Exception의 종류
- Checked Exception (검사된 예외)
- 컴파일 단계에서 반드시 처리해야 하는 예외
- try-catch 블록으로 처리하거나 throws 키워드로 선언해야 합니다.
- 외부 환경(입출력, DB 등)과 상호작용 시 발생 가능한 예외
- IOException : 파일이 존재하지 않거나 네트워크 문제가 발생했을 때
- SQLException : 데이터베이스 작업 중 발생 잘못된 SQL 쿼리
- Unchecked Exception (검사되지 않은 예외)
- 런타임 시에만(실행중 단계) 발생하며 컴파일 단계에서 처리하지 않아도 된다.
- try-catch 블록 없이도 프로그램을 컴파일할 수 있습니다.
- 대부분 코딩 실수로 인해 발생하며 복구보다는 예방이 중요합니다.
- NullPointerException : null 참조로 메서드 호출 또는 필드 접근
- IllegalArgumentException : 메서드 호출 시 잘못된 인자 전달
📌핵심 어노테이션
- @RestControllerAdvice
- Spring에서 제공하는 어노테이션이다. Controller이나 RestController에서 발생하는 예외를 전역적으로 관리하며 처리할 수 있게 해준는 역할을한다.
- @ControllerAdvice 와의 차이점은 에러 응답을 JSON으로 내려준다 는 것이다.
- @ExceptionHandler
- 매우 유연하게 에러처리를 할 수 있는 방법을 제공한다.
- 예외 처리 상황이 발생하면 해당 Handler로 처리하겠다고 명시하는 어노테이션이다.
- 어노테이션 뒤에 괄호를 붙여 어떤 ExceptionClass를 처리할지 설정할 수 있다.
- @RestControllerAdvice로 설정된 클래스 내에서도 메소드로 정의할 수 있지만 각 Controller 안에서도 설정이 가능하다. 전역 설정보다 지역설정으로 정의한 Handler가 우선 순위를 가진다.
🧸커스텀 예외 처리 적용
예외를 받아줄 AroundHubException클래스를 생성 Exception 클래스를 상속받아 Checked Exception으로 작동 생성자 부분에서 부모 클래스 Exception의 생성자를 호출하며 예외 메시지를 구성하며 입력받은 exceptionClass와 httpStatus를 필드에 저장
public class AroundHubException extends Exception{
private Constants.ExceptionClass exceptionClass;
private HttpStatus httpStatus;
// AroundHubException 생성자
public AroundHubException(Constants.ExceptionClass exceptionClass, HttpStatus httpStatus, String message) {
super(exceptionClass.toString() + message);
this.exceptionClass = exceptionClass;
this.httpStatus = httpStatus;
}
// 몇번대 코드 ex) 400, 401
public int getHttpStatusCode() {
return httpStatus.value();
}
// 코드 타입 ex) BAD_REQUEST, NOT_FOUND
public String getHttpStatusType() {
return httpStatus.getReasonPhrase();
}
// httpStatus 객체 자체를 리턴
public HttpStatus getHttpStatus() {
return httpStatus;
}
}
상수값들을 관리하기 위한 클래스로 enum ExceptionClass로 상수값의 집합을 정의할 때 사용하며 어디 클래스인지 정의할때 사용하기 위해 구분하는 역할을 해줍니다.
public class Constants {
/**
* enum ExceptionClass : 예외 유형을 구분하기 위해 enum을 사용 그러면 toString으로 확인하면
* PRODUCT선택시 PRODUCT Exception. USERS 선택시 USERS Exception. 처험 나온다.
*/
public enum ExceptionClass {
PRODUCT("Product"),
USERS("Users");
private String exceptionClass;
ExceptionClass(String exceptionClass) {
this.exceptionClass = exceptionClass;
}
public String getExceptionClass() {
return exceptionClass;
}
@Override
public String toString() {
return getExceptionClass() + " Exception. ";
}
}
}
해당 매서드가 실행 도중에 AroundHubException을 방생시킬 수 있음을 명시적으로 선언하고 예외가 터지면 AroundHubException에 구분할 클래스하고 상태코드 에러 메세지를 담아서 상위 예외를 던집니다.
@ResponseBody
@GetMapping(value="/product2", produces="application/json; charset=UTF-8")
public void exceptionTest() throws AroundHubException {
throw new AroundHubException(
Constants.ExceptionClass.PRODUCT, HttpStatus.NOT_FOUND, "접근이 금지되었습니다.");
}
컨트롤러에서 throws로 AroundHubException을 던지면 @ExceptionHandler에서 컨트롤러에서 발생한 AroundHubException을 자동으로 받아 처리하고 클라이언트에게 적잘한 응답을 반환합니다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = AroundHubException.class)
public ResponseEntity<Map<String, String>> ExceptionHandler(AroundHubException e) {
HttpHeaders responseHeaders = new HttpHeaders();
Map<String, String> map = new HashMap<>();
map.put("error type", e.getHttpStatusType());
map.put(
"error code",
Integer.toString(e.getHttpStatusCode())); // Map<String, Object>로 설정하면 toString 불필요
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, e.getHttpStatus());
}
}
🤹♀개별 예외 처리 적용
위에 방식은 AroundHubException 이라는 Custom클래스를 만들어 하나의 @ExceptionHandler에 보내서 클라이언트한테 보내주었지만 처리를 해야 할때는 개별 예외 처리를 하여야 한다. 밑에 코드를 보면 DataIntegrityViolationException 예외가 발생되었을 때 throws로 던지며 @ExceptionHandler에서 DataIntegrityViolationException.class를 설정한 곧에서 받아주면 따로 예외 처리를 할 수 있습니다.
@ResponseBody
@DeleteMapping(value="/product/{productId}", produces="application/json; charset=UTF-8")
public ResponseEntity<?> deleteProduct(@PathVariable int productId){
try {
productRepository.deleteById(productId);
return ResponseEntity.ok("삭제 성공");
} catch (DataIntegrityViolationException ex) {
// catch 예외를 받아서 다시 예외를 던짐
throw new DataIntegrityViolationException("database constraints");
}
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Map<String, String>> handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
log.info("DataIntegrityViolationException 예외 발생");
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
}
}
😎내 생각 정리
커스텀 예외 클래로 간단한 예외들은 하나의 메서드로 일관성있게 작성하면 나중에 유지보수 할때 예외 처리를 한곳에서 수정할 수 있어 편하고 특정 예외를 처리 하는 로직을처리 하고 싶거나 세부적인 예외 처리를 해야 할때 개별 예외를 사용하면 깔끔하게 코드를 작성할 수 있을거 같다.
'Spring Boot' 카테고리의 다른 글
[Spring-Boot] Prometheus + Grafana 모니터링 구축 (1) | 2025.02.09 |
---|---|
[Spring-Boot] 스프링 데이터 JPA (2) | 2025.02.04 |
[Spring-Boot] Redis에 저장 조회 해보기 (1) | 2025.01.11 |
[Spring-Boot] Cross-Origin Resource Sharing (0) | 2024.12.25 |
[Spring-Boot] JWT Access Token Refresh Token (2) | 2024.12.24 |