본문 바로가기
Java

자바 Exception

by 스르나 2021. 10. 9.

최근에 외부에 라이브러리와 API를 제공하는 작업을하다 느낀 자바 Exception에 대한 처리 방법을 정리한 글이다.

 

자바에는 원치않는 상황(에러 발생 상황, 예기치못한 값을 다뤄야하는 상황)을 다루기 위해 Exception이 있다.

 

Exception은 잘 다루는것, 잘 이용하는 것 2가지 모두 중요하다고 생각한다.

 

우선 잘 다루는 것이란 다른 사람이 만든 라이브러리나, 메소드를 이용하려하는데 거기서 Exception이 발생했을시 어떤식으로 처리할지에 대한 것이다

 

다름으로 잘 이용하는 것은 라이브러리, 메소드를 직접 만들어서 다른사람에게 제공할 때 그곳에서 Exception을 던질것인지, 혹은 로직처리도중 Exception이 발생할 경우 이를 잡아서 에러코드를 만들것인지에 대한 것이다.

 

 

그럼 2가지를 잘하기 위해서 먼저 알아야 할것은 알아보자.

 

1. 예외의 종류

 

출처 :  https://itblackbelt.wordpress.com/2015/02/17/checked-vs-unchecked-exception-in-java-example/


예외에는 크게 Error, Exception으로 분류를 할 수 있는데 Error같은 경우는 개발자가 크게 신경쓸 필요는 없다. 이유는 개발자의 역량(코드로 대처)으로 처리하기 어려운 부분들이기 때문에다. 위 그림에서도 보면 OOM, StackOverflow같은 것은 코드로 대처하기는 어려운 부분들이다.

 

하지만, Exception은 다르다. 이부분은 개발자가 직접 신경써서 관리를 해야한다.

 

출처: https://www.scientecheasy.com/2020/08/checked-unchecked-exceptions-in-java.html/

바로 checked exception, unchecked exception이다. 앞으로는 편의상 확인 예외, 미확인 예외로 부르겠다.

 

먼저 확인 예외와 미확인 예외의 가장큰 차이점은 catch 혹은 throws로 처리를 반드시 해야하는 가에 있다.

 

확인 예외는 이름처럼 반드시 catch 혹은 throws로 반드시 처리를 해줘야한다. 반대로 미확인 예외는 그렇지 않다.

 

2가지로 구분이 되는 이유는 미확인 예외는 모두 RuntimeException을 상속받고 확인 예외는 Exception을 상속 받고 있기 때문이다. RuntimException은 Exception을 상속받고 있지만 RuntimeException은 자바에서 특별하게 취급 받고 있다.

 

일단 확인 예외와 미확인 예외의 구현체를한번 보자.

 

확인 예외(IOExcpetion)

public
class IOException extends Exception {
    static final long serialVersionUID = 7818375828146090155L;

    /**
     * Constructs an {@code IOException} with {@code null}
     * as its error detail message.
     */
    public IOException() {
        super();
    }

    /**
     * Constructs an {@code IOException} with the specified detail message.
     *
     * @param message
     *        The detail message (which is saved for later retrieval
     *        by the {@link #getMessage()} method)
     */
    public IOException(String message) {
        super(message);
    }
    
    
    					.
                            .
                            .

 

미확인 예외

public
class NullPointerException extends RuntimeException {
    private static final long serialVersionUID = 5162710183389028792L;

    /**
     * Constructs a {@code NullPointerException} with no detail message.
     */
    public NullPointerException() {
        super();
    }

    /**
     * Constructs a {@code NullPointerException} with the specified
     * detail message.
     *
     * @param   s   the detail message.
     */
    public NullPointerException(String s) {
        super(s);
    }
}

 

 

2. 예외를 다루는 방법

맨위에서 예외를 다루는 방법은 다른 사람이 만든 라이브러리 혹은 메소드에서 발생한 예외를 처리하는 것이라고 했다.

 

즉, 특정 메소드를 사용하려고하는데 여기서 throws로 예외를 던지거나, 설명에 특정 상황에서 catch로 예외에 대한 처리를 한다고 써있는 경우다.

 

먼저 한가지 시나리오를 만들어보자. 나는 사용자의 프로필을 화면상에 출력하기 위해 개발을 진행하는 중이고, 동료는 사용자의 이름과 관련된 라이브러리를 만들어서 나에게 제공해 주었다고 생각하자.

 

동료가 준 라이브러리의 내용이다.

 

public class NameLibrary {

    public static void printName(String name) throws NameLengthException{
        if(name.length() < 5){
            throw new NameLengthException(name + "length under 5");
        }

        System.out.println("user name is "+name);
    }
}

public class NameLengthException extends Exception {

    public NameLengthException(String msg){
        super(msg);
    }
}

 

보면 해당 라이브러리의 printName은 이름의 길이가 5미만이라면  throws로 예외를 던진다.

 

해당 라이브러리를 사용하면 나는 예외에 대한 처리를 해야한다. 이때 이런 예외를 처리하기 위한 다양한 방법이 존재한다.

 

 

2 - 1 예외 복구

먼저 가장 고전적인 방법으로 try - catch문을 이용하는 방법이다.

 

public class Profile {

    public static void printProfile(){
        String name = "AB";
        
        while (true){
            try {
                NameLibrary.printName(name);
                name = name.trim();
                break;
            }catch (NameLengthException e){
                int emptyLength = 5 - name.length();

                for(int i = 0; i<emptyLength; i++){
                    name+=" ";
                }
            }
        }
        
    }
}

위 코드에서 NameLibrary에서 던진 NameLengthException을 대처하기 위해 catch문을 사용했다.

 

단순히 catch문으로 예외를 잡은 것도 있지만 try - catch문이 while문안에 있다는 것도 함께 보일 것이다. 이유는 라이브러리를 통해 출력해야하는 부분을 반드시 출력하기  위해서이다. 그래서 catch문안에서 이름이 짧아 출력하지 못한 경우이니 이름의 길이를 공백 문자를 이용해서 늘려준 것이다.

 

이글의 핀트에서 조금 벗어나지만 한가지 짚고 넘어가자면 단순한 경우라서 위처럼 while문안에서 장애 복구를 한것인데, 이런식으로 장애 복구를 하려고 하면 이는 반드시 테스트코드를 작성해서 여러 케이스에 대해 확인을 해보자. 자칫 잘못하면 무한 루프에 빠지는 경우가 생길 수 있으니 말이다.

 

 

2 - 2 예외 회피

 

예외가 생겼을때 이를 내가 바로 잡지 않고, 내가 만든것을 다음 사람에게 넘긴다고 생각하면 된다.

 

모든 예외를 내가 만들고 있는 라이브러리에서 처리하라는 법은 없다. 어떤것은 내가 바로잡아 처리를 해야 하겠지만, 어떤것은 내가 처리하기에는 다소 애매한 부분이 있을 수 있다.

 

처리하기 애매한 경우의 예시를 한가지 들어보자.

 

나는 DB에서 데이터를 조회, 삽입, 삭제를하는 DAO를 개발한다고 생각해보자. DAO는 주로 Service단에서 사용한다. 조회를할떄는 Service단에서 넘어온 조회 Key를 이용하는데 이떄 이 Key가 올바르지 않다 DB에서 에러를 넘긴다면 이떄 이 에러를 DAO에서 처리해야 할까?? 주로 DAO에서 처리하기 보단 DAO에서는 에러를 상위 계층(여기에서는 Service)로 넘길 것이다.

 

다음이 예외 회피의 방식이다.

 

DB

public class DB {

    private Map<String, String> users;

    public DB(){
        users = new HashMap<>();
        users.put("james93","Daniel James");
        users.put("romero_nice","romero");
    }



    public String getNameByLoginId(String loginId) throws Exception{
        if(users.get(loginId) == null){
            throw new Exception();
        }
        return users.get(loginId);
    }
}

 

DAO

public class Dao {
    private DB db = new DB();

    public String getUserNameByLoginId(String loginId) throws Exception{
        return db.getNameByLoginId(loginId); // ??
    }
}

 

2 -3 예외 전환

 

다음으로는 예외 전환이다. 위처럼 추상화 단계가 하위인 곳에서 넘어온 예외를 내가 만든 라이브러리에서 적당한 예외(주로 미리 약속된 예외)로 받아서 상위 추상단계로 넘기는 것이다.

 

이방법을 이용하는 경우가 예외를 처리하는 방법중 대다수가 사용하는 방법일 것이다. 이유는 우리는 서로 개발하면서 특정 상황에 대한 예외를 미리 약속하는 경우가 많기 때문에 그것을 에러코드, 에러 메세지로 정해서 넘겨주기 때문이다.

 

그럼 수정된 DAO를 보자.

 

public class Dao {
    private DB db = new DB();

    public String getUserNameByLoginId(String loginId) throws UserNotFoundException{

        try {
            return db.getNameByLoginId(loginId);
        }catch (Exception e){
            e.printStackTrace();
            throw new UserNotFoundException(loginId+ " not found");
        }
    }
}

 

위처럼 UserNotFoundException이라는 적절한 예외를 상위로 넘겨준다면 상위에서 처리를 하는 것이 예외 전환이다.

 

추가적으로 하위단계에서 여러개의 예외를 throw하고 있다면 상위는 이를 일일이 catch문으로 잡는데 이는 가독성이 좋지않다. 이럴때는 차라리 throw를 여러개 던지는 클래스를 한번 wrapping해서 사용하는 것이 좋다.(이 또한 예외 전환으의 한 방법이다.)

 

예시에서 예외를 이상하게 막 던지지만 이해가 가기 쉽게하기 위해서다. 이해하고 보자

 

public class Student {
    private String name;
    private int age;
    private String schoolName;

    public void print() throws IOException, ClassNotFoundException, IllegalAccessException{
        if(name==null || name.equals("")){
            throw new IOException();
        }

        if(age< 0){
            throw new ClassNotFoundException();
        }

        if(schoolName == null | schoolName.equals("")){
            throw new IllegalAccessException();
        }
    }
}

위에서 student의 print 메소드를 사용하기 위해서는 throw로 던진 예외 3가지를 잡아야 한다.

이는 상위 단계에서 부담스러운 일이다. 이런 문제를 wrapping으로 해결한 방법은 다음과 같다.

 

public class WrappingStudent {
    
    private final Student student;
    
    public WrappingStudent(Student student){
        this.student = student;
    }
    
    public void print(){
        try{
            student.print();
        }catch (IOException | IllegalAccessException | ClassNotFoundException e){
            throw new IllegalStateException("student state is wrong");
        }
    }
}

위처럼 한번 감싸서 사용한다면 한가지 예외로 대응이 가능하다.

 

3. 예외를 이용하는 법

 

우리는 개발을 하다 예외를 던져야 하는 경우를 만날 떄가 있다. 이떄 이 예외를 확인 예외로 할지, 미확인 예외로 할지에대한 기준을 여기서 알아보자.

 

우선 위에서 설명했지만 확인 예외와, 미확인 예외의 가장 큰차이는 상위에서 처리를 해야하는가에 대한 것이다.

 

확인 예외 처리를 사용한다면 상위에서 catch나 throws를 이용하여 대처를 해야한다. 어찌보면 가장 안정적인 방법이다. 대부분의 상황에 대한 대처를 코드로 처리하고 최종적으로 사용자에게 무엇이 잘 못 되었는지 알려줄 수 있기 때문이다.

 

반대로 미확인 예외는 catch나 throws를 이용하지 않고 그냥 사용한다. 만약 문제가 생겨 미확인 예외에 걸린다면 로그를 찍고 해당 스레드가 종료 된다.

 

이렇게 보면 확인 예외가 더 좋아 보인다. 하지만 꼭 그렇지는 않다. 유지보수의 관점에서 보면 확인 예외는 상당히 골치아파질 수 있다. 이유는 하위 추상단계에서 던진 예외가 상위에서 처리를 하다 보면 OCP원칙에서 벗어날 수 있기 때문이다.(위에서 Student 클래스에서 예외가 추가 될때마다 WrappingStudent도 동시에 추가를 해줘야한다.) 심지어 확인예외를 제공하지 않는 언어들도 있다. 그렇다고 해서 그런 언어들이 안전성이 떨어지는가? 절대 아니다. 그런의미로 미확인 예외를 사용한다고해서 우리의 애플리케이션이 불안전한 앱이되는 것은 아니다.

 

그렇다면 확인 예외를 사용할 떄와 미확인 예외를 사용할 때를 구분 짓는 기준은 어떤것일까?

기준은 두 예외의 차이점에 힌트가 있다. 바로 예외를 처리, 복구해야하는가에 달려있다.

 

 

 

 

 

'Java' 카테고리의 다른 글

자바 Optional  (0) 2021.05.03
String,StringBuilder,StringBuffer  (0) 2021.03.10
Collection Framework 정리표  (0) 2021.02.25
자바 synchronized 이해  (0) 2021.02.24
JVM  (0) 2021.02.14