프로그래밍에 있어서 예외처리는 필수적이다. 예외를 처리하지 않는 서비스는 없다. 그러므로 예외 처리 방법들에 대해 알아보자.
try {
정상로직1();
예외로직(); //예외가 발생하면 바로 catch문으로 감
정상로직2(); // 정상로직2는 실행 안된다.
} catch (Exception e) {
} finally{ //무조건적으로 실행되는 로직들
}
둘은 비슷하지만 엄연히 다른 함수이다.
예외를 만들 때. 즉 내가 예외를 발생 시키고 싶은 곳에서 강제로 예외 발생시킴.
→ Java 관점에서는 예외가 아님
public static void checkI(int i) {
if (i < 0) {
// i 가 음수이면 명시적으로 예외를 발생시킴
throw new IllegalArgumentException("에러 발생");
}
System.out.println("에러 없음");
}
오류가 발생하면 해당 메소드를 호출한 쪽으로 에러처리를 던지는 것.
→ 내가 처리하는게 아닌 나를 부른 애가 처리하는 것
public static void checkI(int i) throws Exception {
if (i < 0) {
// i 가 음수이면 명시적으로 예외를 발생시킴
throw new IllegalArgumentException("에러 발생");
}
System.out.println("에러 없음");
}
에러가 발생하게 된다면 checkI()를 호출한 메소드가 에러를 처리해야 한다.
Unchecked: 런타임 시점 (ex. NoSuchElementException )
throws를 쓰지 않아도 흐름 상 어딘가에서 catch해주면 된다.
Checked: 컴파일 시점 (ex. IOException )
throws를 쓰지 않으면 컴파일 에러 발생. → 어떤 클래스끼리 호출하는지 흐름을 모름.(컴파일 시점이니)
💡NULL 값을 반환해야하면 NULL 객체를 반환해주자
기존 클래스를 상속받는 NULL객체를 생성해서 반환해주자.class NullRoom extends Room{...} // 메소드들도 오버리이드해서 처리해주자
Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스이다.
반환타입을 바꾸기 위해 등장 (return null을 방지하기 위해) 등장했던 만큼 NPE와 관련이 많다.
Optional<T> 를 사용하여 커스텀 에러처리를 진행한다Optional<T> 이 가독성도 좋고 개발자들이 까먹고 NULL 처리를 안할수도 있기에 Optional<T> 을 쓰는 것이 더 좋을것이다.Optional<T> 객체를 넘겨주고 Service에서 NULL유무를 판별한다.일반 Repository에서 null확인 → 전달 → Service에서도 또 null 확인💡왜 Repository에서는 NULL 처리를 안해?
Repository는 JDBC, Redis 등등 을 사용함에 따라 구조가 달라질수도 있다.
그렇게 되면 바뀔때마다 코드를 다 찾아서 바꿔야 하는데 그럴바에 Service에서만 처리하는 것이 낫다.
public Accommodation getProduct(int id) {
Optional<Accommodation> found
if (accommodationTable.get(id) != null)
return accommodationTable.get(id);
return null;
} Optional Repository → 포장해서 던짐 → Service에서만 확인 public Optional<Accommodation> getProduct(int id) {
Optional<Accommodation> foundedAccommodation = Optional.ofNullable(accommodationTable.get(id));
return foundedAccommodation;
}💡Optional.get(), isPresent()은 사용을 지양하자
//안에 값 가져오기 public T get() { if (value == null) {throw new NoSuchElementException("No value present");} return value; } public boolean isPresent() {return value != null;} //값이 있나 없나Optional 내부에 선언된 get()을 보면 우리가 짤 수 있는 코드와 다를 바 없으며 단순히 null인지 아닌지 판별만 해준다. 즉 Optional을 쓰는 이유가 없어진다. 따라서 우리가 새로 만든 에러 처리 로직을 사용하자.

Exception과 Error는 트리구조로 되어 있다.
Compile 시점에 발생
-> 예외 처리 반드시 해아함
두가지 방법이 있다.
1. 예외 터진 곳에서 직접 try-catch로 처리
2. throws를 사용해서 책임 전가 후 예외를 전달받은 곳에서 예외 처리
runtime 시점에 발생 (call stack)
-> 링킹 흐름/호출 스택 안에서 어디에서든 처리를 하면 된다.

검색하면 자주 나오는 표이다. 뭐가 틀린 것일까.
해당 표에서 “예외 발생시 트랜잭션 처리” 부분은 Spring에 관한 내용이지 Java에 대한 내용이 아니다.
Java에서는 Roll-Back을 자동으로 해주지 않는다. Spring ≠ Java이다.
예외가 발생한 곳에서 처리하는 것이
개인 성향일수도 있지만, Controller단에서 해야 된다고 생각한다.
이러한 과정들을 담당하는 ResponseEntity는 Controller에서 담당하니 Controller에서 예외처리를 해야한다고 생각한다.
Handler란? : 자동으로 실행되는 메소드
@ExceptionHandler는 예외 발생시 자동으로 호출되는 메소드
-> 보통 Controller에서 다룬다.
💡 왜 Controller에서 다룰까?
Controller는 Call Stack의 최하단에 위치하고 있기 때문에 어떤 에러(Unchecked Exception)가 발생하더라도 마지막에 Controller에서 에러를 처리하면 되기 때문이다.
@ExceptionHandler(value = RoomNotFoundException.class)
public String catchRoomNotFoundException(RoomNotFoundException e) {
return "Room not found";
}
@RestControllerAdvice // 모든 컨트롤러 에서 예외 발생시 인터셉트
public class GlobalExceptionHandler {
@ExceptionHandler(value = RoomNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public List<Room> catchRoomNotFoundException(RoomNotFoundException e) {
log.error("Exception class 에러: {}", e.getClass());
System.out.println(e.getMessage() + "방이 존재하지 않습니다.");
e.printStackTrace();
return new RequestRoomListDto().getRooms();
}
...
여러 컨트롤러에서 발생한 오류를 관리한다.
아니다.
만약 오류 발생 후 디폴트 값 혹은 객체를 반환해야한다면 오류가 발생한 곳에서 직접 try catch를 사용하는 등으로 그 자리에서 오류를 수정해주는 것이 좋다.