예외 처리
애플리케이션 개발 과정에서 다양한 오류와 예외 상황에 직면하게 됩니다. 이를 처리하기 위해 자바는 try/catch 및 throw 구문을 제공합니다. 스프링 부트는 이보다 진화된 예외 처리 메커니즘을 제공하여 개발자에게 더 편리하게 예외 처리를 도와줍니다.
예외 (Exception)
애플리케이션의 정상적인 동작을 방해하는 상황을 나타냅니다. 예를 들면, 잘못된 입력값, 유효하지 않은 참조 값 등이 있습니다.
예외는 개발자가 코드 내에서 직접 처리할 수 있으므로, 적절한 코드 설계와 로직을 통해 미리 예상하고 처리할 수 있습니다.
에러 (Error)
에러는 예외와 비슷한 개념으로 여겨지는 경우가 많지만, 실제로는 근본적으로 다른 현상입니다.
에러는 주로 자바의 가상머신 (JVM)에서 발생하며, 대부분의 경우 애플리케이션 레벨에서 직접 해결할 수 없는 문제입니다.
대표적인 예로는 메모리 부족 (OutOfMemoryError)이나 스택 오버플로우 (StackOverFlowError) 등이 있습니다.
이러한 에러는 발생 후에 처리하기보다는 애플리케이션 코드를 검토하고 예방하는 방식으로 접근해야 합니다.
예외 클래스와 그 분류
자바의 예외 처리 체계는 다양한 예외 클래스를 통해 구성되며, 이들은 특정 상속 구조를 따릅니다.
예외 클래스의 상속 구조
java.lang.Object
└─ Throwable
├─ Error
└─ Exception
├─ IOError
├─ IOException
├─ Checked Exception (예: IOException, SQLException)
└─ RuntimeException (Unchecked Exception)
├─ NullPointerException
├─ IllegalArgumentException
├─ IndexOutOfBoundsException
└─ ... (기타 등등)
모든 예외 클래스는 Throwable 클래스에서 파생됩니다.
그 중 Exception 클래스는 자바에서 가장 일반적으로 발생하는 예외 유형을 포함하며,
크게 Checked Exception과 Unchecked Exception으로 분류됩니다.
Checked Exception
컴파일 단계에서 확인 가능한 예외입니다. 즉, 이러한 예외 상황은 개발 도구나 IDE에서 확인할 수 있으며, 이를 처리하지 않으면 코드가 컴파일되지 않습니다.
대표적인 예로는 IOException, SQLException 등이 있습니다.
Unchecked Exception (또는 RuntimeException)
런타임에서 발생하는 예외입니다. 컴파일러는 이러한 예외를 감지할 수 없으며, 이는 프로그램 실행 중에만 발생합니다.
예를 들면, NullPointerException, IllegalArgumentException, IndexOutOfBoundsException 등이 있습니다.
예외 처리 방법
예외 복구
발생한 예외를 직접 처리하여 프로그램을 정상적인 상태로 돌리는 방법입니다.
이 방법은 주로 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));
}
예외 처리 회피
발생한 예외를 직접 처리하는 것이 아니라, 해당 예외를 호출한 메서드로 전달하여 처리하도록 합니다.
이때 throws 키워드를 사용해 메서드의 호출자에게 예외 처리의 책임을 위임합니다.
public void someMethod() throws SomeException {
if (someCondition) {
throw new SomeException("An error occurred!");
}
}
예외 전환
발생한 예외를 다른 예외로 변환하여 던지는 방법입니다.
이는 주로 의미 있는 예외 메시지나 다른 예외 유형으로 전환할 필요가 있을 때 사용됩니다.
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
public CustomException(String message, Throwable cause) {
super(message, cause);
}
}
public void someMethod() {
try {
// 이 부분에서 다른 예외가 발생할 수 있음
externalLibraryCall();
} catch (ExternalLibraryException e) {
throw new CustomException("Our custom message for the error", e);
}
}
public void externalLibraryCall() throws ExternalLibraryException {
// 외부 라이브러리 메서드 호출
}
위의 예시에서 externalLibraryCall 메서드는 외부 라이브러리에서 제공하는 메서드라고 가정하며, 이 메서드가 ExternalLibraryException 예외를 발생시킬 수 있다고 가정합니다. 이때, 우리의 애플리케이션에서는 해당 예외를 직접 사용하기보다 CustomException으로 전환하여 처리하는 것을 선호할 수 있습니다.
someMethod에서는 try-catch 블록을 사용하여 ExternalLibraryException 예외를 잡아내고, 이를 우리의 사용자 정의 예외인 CustomException으로 전환하여 다시 던집니다. 여기서 CustomException의 두 번째 생성자를 사용하여 원래의 예외도 함께 첨부합니다. 이를 통해 스택 트레이스에서 원래의 예외 정보도 확인할 수 있게 됩니다.
스프링 부트의 예외 처리 방식
@RestControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외 처리
@RestControllerAdvice는 전역 범위에서 예외를 처리하는 클래스에 지정되는 어노테이션입니다.
그 안에서 @ExceptionHandler를 사용하여 특정 예외 유형을 처리할 수 있습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<String> handleCustomException(CustomException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
이 예제에서 CustomException은 사용자 정의 예외이며, 모든 컨트롤러에서 발생할 경우 해당 핸들러 메서드로 처리됩니다. 또한, 모든 다른 예외는 Exception 클래스 핸들러 메서드에서 처리됩니다.
@ExceptionHandler를 통해 특정 컨트롤러의 예외 처리
특정 컨트롤러에서만 발생하는 예외를 처리하려면 해당 컨트롤러 내에서 @ExceptionHandler를 사용합니다.
@RestController
@RequestMapping("/api/items")
public class ItemController {
@GetMapping("/{id}")
public ResponseEntity<Item> getItem(@PathVariable String id) {
// 로직 ...
throw new ItemNotFoundException("Item not found with id: " + id);
}
@ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<String> handleItemNotFoundException(ItemNotFoundException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
}
}
이 예제에서 ItemNotFoundException은 ItemController 내부에서만 처리됩니다.
두 방식 모두 사용자에게 의미있는 오류 메시지와 적절한 HTTP 상태 코드를 반환하는 방법으로 예외를 처리합니다. 전역 예외 핸들러(@RestControllerAdvice)는 모든 컨트롤러에서 발생할 수 있는 예외를 처리하는 데 유용하며, 컨트롤러별 예외 핸들러(@ExceptionHandler)는 특정 컨트롤러와 관련된 예외만 처리하고 싶을 때 유용합니다.
커스텀 예외
커스텀 예외의 필요성
명시성: 예외 이름만 보고도 어떤 문제가 발생했는지 쉽게 알 수 있습니다.
유지보수성: 커스텀 예외를 사용하면 애플리케이션 내에서 발생하는 예외를 명확하게 관리할 수 있습니다.
유연성: 개발자가 예외 처리 로직을 통일하거나 다양한 예외 응답을 제공할 수 있습니다.
커스텀 예외 클래스 생성하기
커스텀 예외를 생성하는 기본적인 방법은 Java의 표준 예외 클래스를 상속받아 새로운 예외 클래스를 정의하는 것입니다. 이 때 주로 RuntimeException이나 Exception 클래스를 상속받습니다.
public class UserNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UserNotFoundException() {
super("User not found.");
}
public UserNotFoundException(String message) {
super(message);
}
}
public User getUserById(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("User with id " + id + " not found.");
}
return user;
}
사용자가 존재하지 않을 때 발생하는 UserNotFoundException이라는 커스텀 예외를 생성하고
이 커스텀 예외는 사용자를 찾을 수 없을 때 발생시킬 수 있으며, 기본 메시지 또는 특정 메시지를 포함하여 예외를 생성할 수 있습니다.
'BACKEND > SPRING' 카테고리의 다른 글
스프링부트 유효성 검사와 예외처리 - 유효성 검사 (0) | 2023.08.27 |
---|---|
연관관계 매핑 - 연관관계 매핑 종류와 방향 (0) | 2023.08.20 |
Spring Data JPA 활용 - JPA Auditing 적용 (0) | 2023.08.13 |
Spring Data JPA 활용 - @Query 어노테이션과 QueryDSL (0) | 2023.08.13 |
Spring Data JPA 활용 - 정렬과 페이징 처리 (0) | 2023.08.13 |