Roel Notebook

[Java] 9주차 과제: 예외 처리

by Roel Downey
728x90
반응형

 

스터디 링크 : 링크 

 

예외(Exception)와 에러(Error)

프로그램을 사용하다가 프로그램이 비정상적으로 종료되는 경험을 해본 적이 있을 것이다. 이러한 결과를 초래하는 원인을 프로그램 에러(Error) 또는 오류라고 한다.

 

에러는 크게 컴파일 에러와 런타임 에러로 구분할 수 있다.

컴파일 에러는 말 그대로 컴파일 과정에서 일어나는 에러이고,

런타임 에러는 실행 과정에서 일어나는 에러이다.

 

컴파일 에러는 기본적으로 자바 컴파일러가 문법 검사를 통해서 오류를 잡아내 준다. 우리는 컴파일러가 알려주는 오류를 수정하면 성공적으로 컴파일을 해서 프로그램을 실행할 수 있다.

그러나 컴파일이 문제없이 되더라도 실행 과정(runtime)에서 오류가 발생할 수 있는데, 이런 런타임 에러를 방지하기 위해서는 프로그램 실행 도중 일어날 수 있는 모든 경우의 수를 고려하여 대비할 필요가 있다.

 

자바에서는 런타임 에러를 예외(Exception)와 에러(Error) 두 가지로 구분하여 대응하고 있다.

에러는 메모리 부족(OutOfMemoryError), 스택오버플로우(StackOverFlowError)처럼 JVM이나 하드웨어 등의 기반 시스템의 문제로 발생하는 것이다. 발생했을 때를 대비해서 프로그래머가 뭔가 할 수 있는게 없다. 발생하는 순간 무조건 프로그램은 비정상 종료되기 때문에 애초에 발생하지 않도록 해야 한다.

예외는 발생하더라도 프로그래머가 미리 적절한 코드를 작성해서 프로그램이 비정상적으로 종료되지 않도록 핸들링 해줄 수 있다.

 

예외 처리 방법

try-catch

예외 처리를 위해서는 try-catch 구문을 이용하며 그 구조는 다음과 같다.

try {
    // 예외가 발생할 가능성이 있는 코드
} catch (Exception1 e1) {
    // Exception1이 발생했을 때, 이를 처리하기 위한 코드
} catch (Exception2 e2) {
    // Exception2가 발생했을 때, 이를 처리하기 위한 코드
} catch (ExceptionN eN) {
    // ExceptionN이 발생했을 때, 이를 처리하기 위한 코드
}

try 블럭에는 여러 개의 catch 블록이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch 블록만 수행된다. catch 블럭 안의 ExceptionN은 예외 클래스이며 eN은 해당 클래스의 인스턴스를 가리키는 참조 변수이다. 

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            // 1을 0으로 나눴으므로 예외가 발생한다.
            System.out.println(1 / 0);
            
        } catch (IllegalArgumentException e) {
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        } catch (ArithmeticException e) {
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        } catch (NullPointerException e) {
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        }
    }
}

Output

> Task :ExceptionDemo.main()
java.lang.ArithmeticException
/ by zero

BUILD SUCCESSFUL in 243ms
2 actionable tasks: 2 executed

위처럼 참조 변수를 통해서 발생한 예외 클래스의 인스턴스를 참조할 수 있다.

해당 인스턴스에는 발생한 예외에 대한 정보가 담겨 있어, 이를 통해 Message, StackTrace 등 여러 정보를 얻어올 수 있다. 

0으로 숫자를 나눌 경우 ArithmeticException이 발생하는 것을 확인할 수 있다. 이때 앞에 IllegalArgumentException이 포함된 catch 블록은 try 블록에서 발생한 예외가 속한 클래스가 아니므로 실행되지 않은 것을 확인할 수 있다. 또 NullPointerException이 포함된 catch 블록은 앞에서 ArithmeticException 블록이 예외를 잡아냈으므로 실행되지 않는다.

printStackTrace() : 예외 발생 당시의 호출스택에 있었던 메서드 정보와 예외 메시지를 화면에 출력한다.
getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

 

참조 변수 중복

catch 블럭 안에 다시 try-catch 구문을 사용할 수 있는데, 이때 상위 catch 블록 안에 참조 변수의 이름이 중복되어서는 안 된다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA();

        } catch (RuntimeException e) {
            try {
                methodB();
            } catch (IllegalArgumentException e) {   // 에러 발생: 해당 변수의 이름을 e로 할 수 없음
                ...
            }
        }
    }
}

 

try-catch문의 흐름

 try-catch문은 예외가 발생하는 경우와 아닌 경우로 나눠서 실행 흐름을 살펴볼 수 있다.

 

예외가 발생하지 않는 경우

class ExceptionDemo {
  System.out.println(1);
  System.out.println(2);
  try {
    System.out.println(3);
    System.out.println(4);
  } catch ( Exception e) {
    System.out.println(5);
  }
  System.out.println(6);
}

 

Output

1
2
3
4
6

try 블록 내 코드를 실행 중에 예외가 발생하지 않으면 catch 블록을 스킵한 후 try-catch문 뒤에 있는 문장을 실행한다.

 

예외가 발생하는 경우

class ExceptionDemo {
  System.out.println(1);
  System.out.println(2);
  try {
    System.out.println(3);
    System.out.println(1/0); //예외 발생 : ArithmeticException 
    System.out.println(4);
  } catch (IllegalArgumentException e) {
    System.out.println(5);
  } catch (ArithmeticException e) {
    System.out.println(6);
  } catch (NullPointerException e) {
    System.out.println(7);
  }
  System.out.println(8);
}

 

Output

1
2
3
6
8

 

try 블록 내의 문장을 실행하다가 예외가 발생하는 경우 해당 예외의 인스턴스를 생성하고 첫 번째 catch 블록으로 이동한다. 따라서 try블록 안에 예외가 발생한 문장 다음의 코드는 실행되지 않는다.

첫 번째 catch 블록부터 차례대로 살펴보면서 괄호() 내에 선언된 참조 변수의 종류와 생성된 예외 클래스의 인스턴스에 instancof 연산자를 이용해서 검사를 한다. 검사 결과가 false이면 다음 블록으로 이동하고, true이면 해당 catch 블록의 코드를 실행한 후 try-catch문을 탈출한다. 만약 catch문 안에서 해당 예외가 처리되지 않으면 프로그램은 종료된다.

 

Multicatch block

JDK 1.7부터 여러 catch block을 하나로 합칠 수 있게 되었다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            System.out.println(1 / 0);
        } catch (IllegalArgumentException | ArithmeticException e) {
            System.out.println(e.getMessage());
        }
    }
}

 

단 이때, 나열된 예외 클래스들이 부모-자식 관계에 있다면 오류가 발생한다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            System.out.println(1 / 0);
        } catch (RuntimeException | ArithmeticException e) {
                 // 에러 발생: ArithmeticException은 RuntimeException을 상속받는 클래스이다.
            System.out.println(e.getMessage());
        }
    }
}

왜냐하면, 자식 클래스로 잡아낼 수 있는 예외는 부모 클래스로도 잡아낼 수 있기 때문에 사실상 코드가 중복된 것이나 마찬가지이기 때문이다. 이때 컴파일러는 중복된 코드를 제거하라는 의미에서 에러를 발생시킨다.

또한 멀티캐치는 하나의 블록으로 여러 예외를 처리하는 것이기 때문에 멀티 캐치 블록 내에서는 발생한 예외가 정확이 어디에 속한 것인지 알 수 없다. 그래서 참조 변수 e에는 '|'로 연결된 예외들의 공통 조상 클래스에 대한 정보가 담긴다.

 

throw

throw 키워드를 이용해서 고의로 예외를 발생시킬 수도 있다. 예를 들어 사용자가 "바보"라는 닉네임을 사용하지 못하게 하고 싶다면 다음과 같이 예외를 발생시켜 프로그램을 중단시킬 수 있다.

public class ExceptionDemo {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("아이디를 입력하세요");
        String userName = scanner.nextLine();

        try {
            if (userName.equals("바보")) {
                throw new IllegalArgumentException("부적절한 이름입니다.");
            }
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

Output

> Task :ExceptionDemo.main()
아이디를 입력하세요
바보
부적절한 이름입니다.

BUILD SUCCESSFUL in 6s
2 actionable tasks: 2 executed

new 키워드로 예외 인스턴스를 생성하고, throw 키워드로 해당 예외를 발생시킨 것이다. throw new ... 부분은 다음 문장을 축약한 것이다.

IllegalArgumentException e = new IllegalArgumentException("부적적한 이름입니다.");
throw e;

 

throws

throws 키워드를 통해 메서드에 예외를 선언할 수 있다. 여러 개의 메서드를 쉼표로 구분해서 선언할 수 있다. 형태는 다음과 같다.

void method() throws Exception1, Exception2, ... ExceptionN {
	// 메서드 내용
}

thorws는 메서드 선언부에 예외를 선언해둠으로써 해당 메서드를 사용하는 사람들이 어떤 예외를 처리해야 하는 지를 알려주는 역할을 한다.

throws 자체는 예외의 처리와는 관계가 없다. throws로 예외가 선언된 메서드를 사용할 때, 사용자가 각자 알아서 예외를 처리해줘야 한다. 즉 throws는 해당 메서드에서 예외를 처리하지 않고, 해당 메서드를 사용하는 쪽이 예외를 처리하도록 책임을 전가하는 역할을 한다.

public class ExceptionDemo {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.println("파일 이름을 입력하세요");
            String fileName = scanner.nextLine();
            
            File f = createFile(fileName);
        
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }

    static File createFile(String fileName) throws Exception {
        if (fileName == null || fileName.equals("")) {
            throw new Exception("파일이름이 유효하지 않습니다.");
        }

        File f = new File(fileName);
        f.createNewFile();
        return f;
    }
}

 createFile이라는 메서드는 파일 이름을 입력받아서 파일을 생성하는 역할을 한다. 만약 파일 이름이 null이거나 빈 문자열이라면 예외를 던진다.

createFlie 메서드 내에서는 해당 예외에 대한 처리를 하지 않고, 단지 thorws로 예외를 선언하기만 했다. 그래서 createFile을 사용하는 사람들이 각자 상황에 맞게 예외를 어떻게 처리할지 선택해야 한다.

 

finally

finally는 try-catch와 함께 예외의 발생 여부와 상관없이 항상 실행되어야 할 코드를 포함시킬 목적으로 사용된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.

try {
	// 예외가 발생할 가능성이 있는 문장을 넣는다.
} catch {
    // 예외 처리를 위한 문장을 넣는다.
} finally {
    // 예외 발생 여부와 상관없이 항상 실행되어야 할 문장을 넣는다.
}

예외가 발생한 경우에는 try -> catch -> finally 순으로 실행되고, 예외가 발생하지 않은 경우에는 try - finally 순으로 실행된다.

한 가지 짚고 넘어갈 점은 finally 블록 내의 문장은 try, catch 블록에 return문이 있더라도 실행된다는 것이다. 

public class ExceptionDemo {

    public static void main(String[] args) throws Exception {
        methodA();
        System.out.println("methodA가 복귀한 후 실행될 문장");
    }

    static void methodA() {
        try {
            System.out.println("트라이 블록 수행");
            return;
        } catch (Exception e) {
            System.out.println("캐치 블록 수행");
        } finally {
            System.out.println("파이널리 블록 수행");
        }
    }
}

Output

> Task :ExceptionDemo.main()
트라이 블록 수행
파이널리 블록 수행
methodA가 복귀한 후 실행될 문장

BUILD SUCCESSFUL in 257ms
2 actionable tasks: 2 executed

methodA를 보면 try문에서 "트라이 블록 수행"을 출력하고 분명 return을 하는데도, 그전에 finally 블록이 수행되는 것을 확인할 수 있다.

catch 블록을 수행하다가 return문을 만날 때에도 finally 블록이 수행되고 리턴한다. 

 

예외 계층 구조

자바에서는 실행 시 발생할 수 있는 오류(Exception & Error)를 클래스로 정의하고 있다. 예외와 에러의 상속계층도는 다음과 같다.

https://madplay.github.io/post/java-checked-unchecked-exceptions

 

Exception과 Error는 Throwable이라는 클래스를 상속받고 있으며 Throwable은 Object를 직접 상속받고 있다. 

 

Checked Exceptions VS Unchecked Exceptions

위 그림을 살펴보면 Exception을 상속받는 클래스들 중에서 RuntimeException을 제외하고는 모두 Checked Exception이라고 표시되어 있다.

Checked Exception은 컴파일 시점에서 확인될 수 있는 예외이다. 만약 코드 내에서 Checked Exception을 발생시킨다면, 해당 예외는 반드시 처리되거나, 해당 코드가 속한 메서드의 선언부에 예외를 선언해줘야 한다.

예를 들어 Checked Exception 중에 하나인 IOException을 발생시키는 메서드를 선언했다고 치자.

 

class ExceptionDemo {
  public static void main(String[] args) {
    methodA();
  }
  
  static void methodA() {
    throw new IOException();
  }
}

 

이 코드는 애초에 컴파일 자체가 안된다. IOException은 Checked Exception이기 때문에 컴파일 단계에서 예외가 확인이 된다. 따라서 위 코드를 컴파일하려면 try-catch로 예외를 처리하거나 thorws로 예외를 던져줘야 한다.

 

class ExceptionDemo {
  public static void main(String[] args) throws IOException {
    methodA();
  }
  
  static void methodA() throws IOException{
    throw new IOException();
  }
}

 

이렇게 예외를 던져주면 컴파일은 가능하다.

 

Unchecked Exception은 컴파일 단계에서 확인되지 않는 예외이다. Java에서는 RuntimeException과 그 하위 클래스, 그리고 Error와 그 하위 클래스가 이에 속한다. 이 예외들은 컴파일러가 예외를 처리하거나 선언하도록 강제하지 않는다. 프로그래머가 알아서 처리를 해야 한다. 예를 들어 위의 예시를 RuntimeException으로 바꾸면 컴파일 에러가 발생하지 않는다.

 

class ExceptionDemo {
  public static void main(String[] args) {
    methodA();
  }
  
  static void methodA() {
    throw new RuntiomeException();
  }
}

 

예외를 처리하거나 던지지 않아도 컴파일이 잘 된다.

그렇다면 왜 이렇게 예외를 두 타입으로 나눠놨을까? 오라클 공식 문서에서 이에 대해 설명하고 있다.

 

요약하자면, 예외는 메서드의 파라미터나 반환 값만큼이나 중요한 공용 인터페이스 중 하나이다.

메서드를 호출하는 쪽은 그 메서드가 어떤 예외를 발생시킬 수 있는가에 대해 반드시 알아야 한다. 따라서 Java는 checked exception을 통해 해당 메서드가 발생시킬 수 있는 예외를  명세하도록 강제하고 있다.

그럼 Runtime Exception은 왜 예외를 명세하지 않아도 되도록 했을까? Runtime Exception은 프로그램 코드의 문제로 발생하는 예외이다. 따라서 클라이언트 쪽(메서드를 호출하는 쪽)에서 이를 복구(or 회복)하거나 대처할 수 있을 거라고 예상하긴 어렵다. 또 Runtime Exception은 프로그램 어디서나 매우 빈번하게 발생할 수 있기 때문에 모든 Runtime Exception을 메서드에 명시하도록 강제하는 것은 프로그램의 명확성을 떨어뜨릴 수 있다.

따라서 클라이언트가 exception을 적절히 회복할 수 있을 것이라고 예상되는 경우 checked exception으로 만들고, 그렇지 않은 경우 unchecked exception으로 만드는 것이 좋다.

 

사용자 정의 예외 만들기

기존에 정의된 예외 클래스 외에 필요에 따라 새로운 예외를 정의할 수 있다. Exception 클래스를 상속받거나, 필요에 따라 알맞은 예외 클래스를 상속받아 만든다.

public class ExceptionDemo {

    public static void main(String[] args) throws SpaceException {
        methodA(5);
    }

    static void methodA(int space) throws SpaceException {
        if (space < 1) {
            throw new SpaceException("공간 부족");
        }
    }
}

class SpaceException extends Exception {
    public SpaceException(String message) {
        super(message);    // 조상 클래스인 Exception의 생성자 호출
    }
}

 

예외 되던지기(exception re-throwing)

한 메서드에서 발생할 수 있는 예외가 여러 개인 경우, 일부는 메서드 내부에서 처리하고 일부는 선언부에 지정해서 메서드를 호출한 쪽에서 처리하도록 할 수 있다.

또 하나의 예외에 대해서도 양쪽에서 처리하도록 할 수 있는데 이를 '예외 되던지기'라고 한다. catch문에서 throw를 사용해 예외를 다시 던지는 방식으로 구현 가능하다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA();
        } catch (Exception e) {
            System.out.println("main에서 예외 처리");
        }
    }

    static void methodA() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("methodA에서 예외 처리");
            throw e;
        } 
    }
}

 

Chained Exception

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다. 원인 예외는 initCause()로 지정할 수 있다. initCause()는 Throwable 클래스에 정의되어 있기 때문에 모든 예외 클래스에서 사용할 수 있다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void methodA(int num) throws IOException{
        try {
            if (num == 0) {
                throw new IllegalArgumentException();
            }
        } catch (IllegalArgumentException e) {
            IOException ioException = new IOException();
            ioException.initCause(e);  // IOException의 예외를 IllegalArgumentException으로 지정
            throw ioException;
        }
    }
}

Output

> Task :ExceptionDemo.main()
java.io.IOException
	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:21)
	at exceptiondemo.ExceptionDemo.main(ExceptionDemo.java:9)
Caused by: java.lang.IllegalArgumentException
Caused by: java.lang.IllegalArgumentException

	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:18)
	... 1 more

 

이런 식으로 원인 예외를 등록해서 예외를 발생시키면 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루는 것이 가능하다. 서로 연결되는 예외는 상속 관계가 아니어도 상관없다.

또 이 방식을 이용해서 checked exception을 unchecked exception으로 바꾸는 것도 가능하다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void methodA(int num){
        try {
            if (num == 0) {
                throw new IllegalArgumentException();
            }
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
    }
}

Output

java.lang.RuntimeException: java.lang.IllegalArgumentException
	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:21)
	at exceptiondemo.ExceptionDemo.main(ExceptionDemo.java:9)
Caused by: java.lang.IllegalArgumentException
	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:18)
	... 1 more
Caused by: java.lang.IllegalArgumentException

 

RuntimeException(Throwable cause) // 원인 예외를 등록하는 생성자

 

온라인 스터디 

  1. 여러줄을 썼을때 주의해야할점.

    1. 순서가 중요함.
    2. 계층구조이고, 순차적으로 읽음.
    3. RuntimeException → IlligalException 순으로 → 컴파일러 에러가남.
  2. 너무 포괄적인것을 쓰지말것 

    1. Exception을 던지는 경우가 드물다.
    •  
    • 커스텀 best pratice

        1. 기존에 있는거를 활용해라.
        • IlligalArgumentException은 쓰기 좋음.
        1. 근본적인 에러를 생성자에 전달하지 않으면, 정보가 없어진다. (원인파악이 힘들어짐)
        • 예외변경할때 반드시 그 cause를 넣어둘것
        • cause에 대해서 알아보자.
        • 예외를 만들때 생성자를 만든다.
          • cause가 있는 버전으로 만들어라!

          • 예외를 던질 때, 어떤 문맥에서 던졌는지 반드시 넣게 하도록 하라.

          • root cause에 해당하는것을 담아서전달. (변경전달)

          • 그냥 단수 던지는거면 메세지만 넘길 수 있지.

    • printStackTrace()

    • 예외처리비용

      • stacktrace를 메모리에 담고 있어야한다.
      • 예외처리를 가지고 비즈니스로직을 처리하지 말라라는 말이 있다. 예외를 쓴다는거 자체가 비용이 비쌈. stack trace가 담김. stacktrace에 대한걸 알아두기.
      • logical 하면, 안쓰는게 좋다.

        Q : 실무에서 exception선언할때 e.printStackTrace(); 이 함수가 보안에 위배된다는데 왜 그런지 알 수 있을까요?

        1. 스택에 대한 정보가 그대로 노출된다고 하던데.

        2. 로그를 어떻게 관리하냐의 문제

          1. 로그관리의문제. 외부노출의 문제.
          2. 다른 보안이슈로는.. 개발자임에도 불구하고 log로 출력하면 안되는 정보가 있음. (개발자들도 불가, 코딩할때도 주의해야함)
          3. 스택트레이스 자체는 디버깅할때 좋음.
        3. 멀티캐치 (1.7) 부터

          1. 다른 에러를 잡는데, 처리하는게 비슷하다.
          2. 다만 둘이 상속관계라면 에러납니다.
          3. 멀티캐치 후에 getCause()를 사용하면?
            1. NullpointerException | IlligalException
              1. try블럭에서 하는거에 달린거다. (캐치랑 상관없음)
        4. checked excpetion

          1. 코틀린은 checked exception이 없음.
          2. 왜 checked exception을 쓰는가?
            1. 런타임 계열은 코딩으로 뭔가 할 수 없는경우. (에러가 나도 뭔가 할 수 있는경우)
            2. checked는 뭔가를 할 수 있는 경우.
        5. 사용자 정의를 만들때 주의해야할것

          1. 기존에 제공하는 예외를 알아둘것.
        6. try-with-resources를 많이 씁시다.

          1. 회사사람들이 좋아함. (써먹으세요)

          2. 이펙티브 자바를 쓸까? 아니면 어떻게?

          3. finally를 줄여줍니다.

728x90
반응형

'Java' 카테고리의 다른 글

[Java] 11주차 과제: Enum  (0) 2021.02.04
[Java] 10주차 과제: 멀티쓰레드 프로그래밍  (0) 2021.01.23
[Java] 8주차 과제: 인터페이스  (0) 2021.01.05
[Java] 7주차 과제: 패키지  (0) 2020.12.28
[Java] 6주차 과제: 상속  (0) 2020.12.22

블로그의 정보

What doing?

Roel Downey

활동하기