[Java] 예외 처리

하윤철·2024년 7월 24일

프로그래밍에 있어서 예외처리는 필수적이다. 예외를 처리하지 않는 서비스는 없다. 그러므로 예외 처리 방법들에 대해 알아보자.

예외 처리 방법

Try - Catch (finally)

try {
	  정상로직1();
	  예외로직();	 //예외가 발생하면 바로 catch문으로 감
	  정상로직2(); // 정상로직2는 실행 안된다.
} catch (Exception e) {
} finally{ //무조건적으로 실행되는 로직들
}

Throws

둘은 비슷하지만 엄연히 다른 함수이다.

Throw

예외를 만들 때. 즉 내가 예외를 발생 시키고 싶은 곳에서 강제로 예외 발생시킴.

→ Java 관점에서는 예외가 아님

    public static void checkI(int i) {
        if (i < 0) {
            // i 가 음수이면 명시적으로 예외를 발생시킴
            throw new IllegalArgumentException("에러 발생");
        }
        System.out.println("에러 없음");
    }

Throws

오류가 발생하면 해당 메소드를 호출한 쪽으로 에러처리를 던지는 것.

→ 내가 처리하는게 아닌 나를 부른 애가 처리하는 것

    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(포장지)

Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스이다.

Optional은 왜 쓸까?

반환타입을 바꾸기 위해 등장 (return null을 방지하기 위해) 등장했던 만큼 NPE와 관련이 많다.

  • NEP(NullPointerException)가 뜬다면 바로 런타임에러로 프로그램이 멈추므로 NPE를 줄여야한다.
    → NULL이라면 우리만의 에러 처리 방식으로 처리 하겠다. (ex.아이디가 없습니다.)
  • Spring의 경우 NPE가 발생하면 500에러가 발생하는데, 이렇게 되면 클라이언트는 왜 서버에러가 뜨는지 모르기에 Optional<T> 를 사용하여 커스텀 에러처리를 진행한다
  • if(A == null)의 사용을 줄일 수 있다. → 물론 if( A ≠ null ) 로 판별 할 수 있지만 Optional<T> 이 가독성도 좋고 개발자들이 까먹고 NULL 처리를 안할수도 있기에 Optional<T> 을 쓰는 것이 더 좋을것이다.
  • Optional로 감싸면 1계층이라도 NULL확인을 줄일 수 있다. → ex. Repository에서는Optional<T> 객체를 넘겨주고 Service에서 NULL유무를 판별한다.

    💡왜 Repository에서는 NULL 처리를 안해?
    Repository는 JDBC, Redis 등등 을 사용함에 따라 구조가 달라질수도 있다.
    그렇게 되면 바뀔때마다 코드를 다 찾아서 바꿔야 하는데 그럴바에 Service에서만 처리하는 것이 낫다.

    일반 Repository에서 null확인 → 전달 → Service에서도 또 null 확인
    	    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 메소드들

💡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을 쓰는 이유가 없어진다. 따라서 우리가 새로 만든 에러 처리 로직을 사용하자.

  • orElse(Object object): 기존에 있던 객체
    → Optional 안이 null이면 기존에 있던 객체를 대입
  • orElseGet( ()→{} ): 새로운 객체를 생성해달라고 요청하는 함수.
    Optional 안이 null이면, 디폴트 값으로 새로운 객체를 대입
  • orElseThrow( ()→{} ): 새로운 예외 객체를 생성해달라 요청하는 함수.
    값이 없을 경우(null) 예외 객체로 처리한다.

Exception


Exception과 Error는 트리구조로 되어 있다.

Checked & Unchecked Exception

Checked Exception

Compile 시점에 발생
-> 예외 처리 반드시 해아함

어디서 처리해야 할까?

두가지 방법이 있다.
1. 예외 터진 곳에서 직접 try-catch로 처리
2. throws를 사용해서 책임 전가 후 예외를 전달받은 곳에서 예외 처리

Unchecked Exception

runtime 시점에 발생 (call stack)
-> 링킹 흐름/호출 스택 안에서 어디에서든 처리를 하면 된다.

잘못된 정보를 알아보자


검색하면 자주 나오는 표이다. 뭐가 틀린 것일까.

Roll-Back을 한다?

해당 표에서 “예외 발생시 트랜잭션 처리” 부분은 Spring에 관한 내용이지 Java에 대한 내용이 아니다.

Java에서는 Roll-Back을 자동으로 해주지 않는다. Spring ≠ Java이다.

예외 클래스를 구체화 하자

  • 에러 클래스를 직접 만들고 구체화 하면 에러를 잡기가 쉬워진다. → 추적하기가 쉽다.
  • 어떤 에러인지 알 수 있다. → 의미를 가지게 할 수 있다.

그럼 예외 처리를 어디서 할까?

일반적인 자바 코드라면...

예외가 발생한 곳에서 처리하는 것이

Spring이라면...

개인 성향일수도 있지만, Controller단에서 해야 된다고 생각한다.

  • 올바른 HTTP Status Code를 보내줘야한다.
  • 사용자에게 에러처리 결과를 보여줘야한다

이러한 과정들을 담당하는 ResponseEntityController에서 담당하니 Controller에서 예외처리를 해야한다고 생각한다.

ExceptionHandler

Handler란? : 자동으로 실행되는 메소드

@ExceptionHandler는 예외 발생시 자동으로 호출되는 메소드
-> 보통 Controller에서 다룬다.

💡 왜 Controller에서 다룰까?
Controller는 Call Stack의 최하단에 위치하고 있기 때문에 어떤 에러(Unchecked Exception)가 발생하더라도 마지막에 Controller에서 에러를 처리하면 되기 때문이다.

예시 코드

@ExceptionHandler(value = RoomNotFoundException.class)
    public String catchRoomNotFoundException(RoomNotFoundException e) {
        return "Room not found";
    }

GlobalExceptionHandler

@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를 사용하는 등으로 그 자리에서 오류를 수정해주는 것이 좋다.

profile
선순환을 만드는 개발자

0개의 댓글