이번 장에서 유효성 검사 등 여러가지를 다뤘었지만 나는 예외 처리에 집중하여 포스팅 해보겠다. 개발을 진행하면서 커스텀 예외를 어떻게 만들고 사용할지에 대해 항상 고민했었는데, 이 책이 아주 잘 설명해주었다. 그래서 한번 확실히 정리하려고 한다.
예외(Exception)란 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 애플리케이션이 정상적으로 동작하지 못하는 상황을 의미한다. 예외는 개발자가 직접 처리할 수 있는 것이므로 미리 코드 설계를 통해 처리할 수 있다.
에러는 주로 자바의 가상머신에서 발생시키는 것으로 어플리케이션 코드에서 거의 처리할 수 없다. 예를 들어 StackOverFlow, OutOfMemory 등이 있다. 이는 발생 시점에서 처리하는게 아니라 문제가 발생하지 않도록 원천적으로 차단해야 한다.
모든 예외 클래스는 Throwable
클래스를 상속 받는다.
예외들은 Checked Exception
과 Unchecked Exception
으로 구분할 수 있다.
Checked Exception | Unchecked Exception | |
---|---|---|
처리 여부 | 반드시 예외 처리 필요 | 명시적 처리를 강제하지 않음 |
확인 시점 | 컴파일 단계 | 실행 중 단계 |
대표적인 예외 클래스 | IOException, SQLException | RuntimeException, NullPointException, IllegalArgumentException, IdenxOutOfBoundException, SystemException |
분류 | RuntimeException 상속 받지 않음 | RuntimeException을 상속 받음 |
Checked
와 Unchecked
의 가장 큰 차이점은 컴파일 단계에서 확인 가능하냐, Runtime에서 확인 가능하냐의 차이이다. 즉, Unchecked
의 경우에는 문법상 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외를 의미한다.
책에서는 자세히 다루지 않은 내용이지만 궁금해서 찾아보았다.
Throw
는 개발자가 의도적으로 예외를 발생시킬 때 사용한다.
한번 예시를 보자
public void updateMemberName(String memberId, String name) {
Optional<Member> member = memberRepository.findById(memberId)
if(member.isEmpty()) {
throw new MemberNotFoundException()
}
member.setName(name);
}
위와 같이 개발자가 직접 원하는 타이밍에 예외를 던지게 해준다.
Throws
는 해당 메서드 내에서 예외처리를 하지 않고, 메서드 호출부로 예외를 전가한다. 즉, 호출부에서 예외처리를 하도록 던진다.
한번 예시를 보자.
public class FileExample {
public static void main(String[] args) {
try {
readFile("example.txt");
} catch (FileNotFoundException e) {
System.out.println("파일을 찾을 수 없습니다: " + e.getMessage());
}
}
public static void readFile(String filename) throws FileNotFoundException {
File file = new File(filename);
Scanner scanner = new Scanner(file); // 파일을 열 때 FileNotFoundException이 발생할 수 있음
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
}
scanner.close();
}
위 코드는 readFile
메서드에서 throws FileNotFoundException
을 작성하여 예외 발생 시, 호출부로 예외를 전가하고 있다. new Scanner(file)
에서 예외 발생 시, main
메서드로 예외가 전가되고, 이는 main
의 catch
부분에서 예외를 처리한다.
보통 웹 서비스에서는 예외가 발생하면 요청을 보낸 클라이언트에 어떤 문제가 발생했는지 상황을 전달하는 경우가 많다.
예외가 발생했을 때 클라이언트에 오류 메세지를 전달하려면 각 레이어에서 발생한 예외를 컨트롤러로 전달해야 한다. 이렇게 전달받은 예외를 스프링 부트에서 처리하는 방식으로 크게 두가지가 있다.
우선 코드를 보면서 해석해보자.
CustomExceptionHandler
@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;
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getResponsePhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
@RestControllerAdvice
와 @ControllerAdvice
는 @Controller
, @RestController
에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있게 하는 기능을 수행한다.
@ExceptionHandler
는 @Controller
, @RestController
가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의할 때 사용한다.
@(Rest)Controller
가 붙은 클래스에서 발생하는 RuntimeException
은 위 코드에서 처리된다.
또한 @(Rest)Controller
는 범위를 지정할 수 있고, @ExceptionHandler
는 배열 형식으로 예외를 받을 수 있다.
@RestControllerAdvice
와 @ControllerAdvice
는 응답을 JSON 형태로 보내느냐의 차이이다.
만약 특정 컨트롤러 클래스 내에
@ExceptionHandler
를 사용한 메서드를 선언하면 해당 클래스에 국한해서 예외처리를 할 수 있다.
자바에서도 많은 예외를 제공하지만, 예외의 타입과 이름만으로는 해당 예외가 어떤 예외인지 인지하기 어려운 경우가 많다. 이런 경우에는 개발자가 커스텀 예외를 만들어 사용할 수 있다.
이번에 만들어볼 커스텀 예외 클래스를 생성하는 데 필요한 내용은 다음과 같다.
Error Type: HttpStatus의 reason phrase
Error Code: HttpStatus의 Code
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. ";
}
}
}
ExceptionClass라는 열거형은 커스텀 예외 클래스에서 메세지 내부에 어떤 도메인에서 문제가 발생했는지 보여주는데 사용된다.
위 코드에서는 Product 도메인을 예시로 작성하였다.
CustomException
@Getter
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);
//super: Exception의 생성자 호출 -> Exception은 Throwable을 상속 받고, Excpetion의 생성자도 Throwable의 생성자 호출
this.exceptionClass = exceptionClass;
this.httpStatus = httpStatus);
}
}
CustomException을 처리하는 handleException 메서드
@ExceptionHandler(value = CustomException.class)
public ResponseEntity<Map<String, String>> handleException(CustomExcpetion e, HttpServletRequest request) {
HttpHeaders responseHeaders = new HttpHeaders();
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());
}
예외를 발생시키는 Controller
@GetMapping("/custom")
public void getCustomException() throws CustomException {
throw new CustomException(ExceptionClass.PRODUCT, HttpStatus.BAD_REQUEST, "getCustomException 호출");
}
코드를 살펴보자면,
/custom으로 GET 요청을 할 시, CustomException을 던짐
@ExceptionHandler는 CustomException이 발생하면 처리를 담당
이렇게 커스텀 예외를 만들 수 있고, 원할 시 다른 응답 필드 등을 설정할 수 있다.