트러블 슈팅

[예외 처리] getMessage를 출력했는데 null이 왜 나와?

오렌지색 귤 2022. 1. 25. 15:08
반응형

제가 오늘 겪었던 문제에 대해서 아주 간단하게 만든 예제 코드를 작성해봤습니다.

 

어떤 실수였는지 한번 유추해보시면 좋을 것 같습니다.

 

클래스 다이어그램

 

클래스 구조를 한 눈에 알아보기 쉽게 해드리기 위해서 Intellij의 내부 기능을 활용해 다이어그램을 만들어 봤습니다.

 

 

BookManagerImpl 클래스는 IBookManager 인터페이스의 구현체이며, add()sell() 메서드를 구현하고 있습니다.

 

Book 클래스는 isbn(String), title(String), quantity(int) 필드를 가지며, BookManagerImpl 클래스 내부의 books 리스트에 저장될 수 있습니다.

 

RuntimeException을 상속한 custom 예외인 ISBNNotFoundExceptionQuantityException 클래스를 생성했습니다.

 

BookTest의 메인 메서드에서는 책을 manager에 저장해두고, 저장해둔 수량보다 많은 양을 팔려고 함으로써 예외가 적절하게 처리되는 지 테스트합니다.

 

내부 코드

 

아주 간략하게 작성한 코드입니다.

 

public class Book {
    public String isbn;
    public String title;
    public int quantity;

    public Book(String isbn, String title, int quantity) {
        this.isbn = isbn;
        this.title = title;
        this.quantity = quantity;
    }
}
public interface IBookManager {

    void add(Book book);

    void sell(String isbn, int quantity);
}
public class BookManagerImpl implements IBookManager {

    private List<Book> books = new ArrayList<>();
    private static BookManagerImpl bm = new BookManagerImpl();

    public static BookManagerImpl getBookManager() {
        return bm;
    }

    @Override
    public void add(Book book) {
        if (book != null) {
            books.add(book);
        }
    }

    @Override
    public void sell(String isbn, int quantity) {
        System.out.println("******************도서 판매:" + isbn + "," + quantity + "개*********************");
        Book book = books.stream().filter(b -> b.isbn.equals(isbn)).findAny().orElse(null);
        try {
            checkIsbn(book);
            checkQuantity(quantity, book);

            book.quantity -= quantity;
            System.out.println(book);
        } catch (ISBNNotFoundException e) {
            System.out.println(e.getMessage());
        } catch (QuantityException e) {
            System.out.println(e.getMessage());
        }
    }

    private void checkQuantity(int quantity, Book book) {
        if (book.quantity < quantity) {
            throw new QuantityException();
        }
    }

    private void checkIsbn(Book book) {
        if (book == null) {
            throw new ISBNNotFoundException();
        }
    }
}
public class ISBNNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 7574345453521L;

    public ISBNNotFoundException() {
        System.out.println("해당 도서의 ISBN이 존재하지 않습니다.");
    }
}

public class QuantityException extends RuntimeException {

    private static final long serialVersionUID = 131215312321L;

    public QuantityException() {
        System.out.println("수량이 부족합니다");
    }
}
public class BookTest {

    public static void main(String[] args) {
        BookManagerImpl manager = BookManagerImpl.getBookManager();
        manager.add(new Book("21424", "Java Pro", 10));

        manager.sell("21424", 11);
    }
}

 

 

예상 출력문과 실제 출력문

 

******************도서 판매:21424,11개*********************
수량이 부족합니다

 

다음과 같은 출력문이 콘솔에 나타나기를 바랬습니다.

 

하지만 실제 출력문은 아래와 같았습니다.

 

******************도서 판매:21424,11개*********************
수량이 부족합니다
null

 

 

응..? null 형이 왜 거기서 나와...?

 

 

해결

 

디버깅 및 주석 처리

 

디버깅 및 주석 처리를 하면서 어떤 문장에서 null이 출력되는지 확인했습니다.

 

바로 BookManagerImpl 클래스의 sell() 메서드에서 checkQuantity(quantity, book); 문장에서 "수량이 부족합니다"라는 출력과 함께 null이 출력되었습니다.

 

getMessage()의 문제인가?

 

결국에는 완전한 헛다리였지만, 처음에는 람다식으로 인해 null 상태인 book을 custom exception인 QuantityException이 마음대로 출력해버린다고 생각했습니다.

 

그래서 Oracle의 공식 문서에서 RuntimeExceptiongetMessage()을 자세히 찾아봤습니다.

 

하지만 어디에도 주어진 출력문을 제외한 다른 객체를 메세지에 추가해서 넣어준다는 말은 없었습니다.

 

같은 기능을 나타내는 getLocalizedMessage()를 호출해도, RuntimeException 대신 Exception을 상속받아도 null은 계속 출력되었습니다.

 

Intellij 의 코드 타고 들어가기 기능

 

공식 문서에는 나타나지 않는 문제를 해결하기 위해 내부 코드를 봐야겠다는 생각을 했습니다.

 

따라서 Intellij의 코드 타고 들어가기(?) 기능을 활용해서 RuntimeException 클래스를 본 순간 가장 위쪽에 쓰여진 생성자들은 저를 벙찌게 만들었습니다.

 

public class RuntimeException extends Exception {
    static final long serialVersionUID = -7034897190745766939L;

    public RuntimeException() {
        super();
    }

    public RuntimeException(String message) {
        super(message);
    }

    public RuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    ... // 이하 생략
}

 

어..? 나는 QuantityException에서 System.out.println으로 "수량이 부족합니다"를 출력하기만 했던 것이 떠올랐다..

 

그렇다... catch된 exception은 실제로 아무런 메세지를 받지 못했고... 기본 생성자가 호출되어 null인 상태였던 것이다..

 

"수량이 부족합니다"라는 출력문을 자바 표준 출력 메서드로 출력해놓고, e.getMessage()로 출력한 줄 알고 있었던 것이었다.

 

나의 실수였음을 깨달은 후, 아래의 코드로 수정했다.

 

수정한 코드

 

public class ISBNNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 7574345453521L;

    public ISBNNotFoundException() {
        super("해당 도서의 ISBN이 존재하지 않습니다.");
    }
}

public class QuantityException extends RuntimeException {

    private static final long serialVersionUID = 131215312321L;

    public QuantityException() {
        super("수량이 부족합니다");
    }
}

 

조상(여기서는 RuntimeException)의 생성자에 내가 원하는 메세지를 매개 변수로 주었다.

 

내가 넣어준 메세지는 추후 try ~ catch 문에서 getMessage() 메서드를 통해 출력 될 것이다.

 

결과

 

 

******************도서 판매:21424,11개*********************
수량이 부족합니다
반응형