Programming/Java

[Java] try-catch 예외처리

kmindev 2023. 10. 16. 09:35

남궁성님의 Java의 정석(3rd Edition)을 보고 정리한 글입니다.

1. 에러의 종류

에러의 종류에는 논리적 에러,컴파일 에러, 런타입 에러가 있다.

 

a. 논리적 에러

  • 개발자 실수(논리적 오류)로 인해 실행은 되지만, 의도와 다르게 동작하는 것

b. 컴파일 에러

  • 컴파일 시점에 발생하는 에러
    • ex) 문법을 잘못 작성해서 발생하는 에러
    • ex) 타입체크 에러

c. 런타임 에러

  • 실행 시 발생하는 에러

런타임 시 발생할 수 있는 오류에는 2가지가 있다.

  • 에러(error)
    • 프로그램 코드에 의해서 수습될 수 없는 심각한 오류로 시스템이 종료되어야 할 수준
    • ex) StackOverflowError: 재귀가 지속되어 호출 깊이가 깊어져 발생하는 오류
    • ex) OutOfMemoryError: JVM 메모리 부족으로 객체를 할당할 수 없을 때 발생하는 오류
  • 예외(exception)
    • 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류로 개발자의 실수나, 사용자의 영향에 의해 발생
    • 개발자가 미리 예측하여 방지할 수 있기 때문에 예외처리를 해야한다.
    • ex) NullPointException: 객체가 필요한 경우인데 null을 사용할 경우 발생할 수 있는 예외
    • ex) IllegalArgumentException: 메서드가 허가되지 않거나 부적절한 인수를 받았을 경우 발생할 수 있는 예외

컴파일 에러는 컴파일 시 발생하는 에러이기 때문에 실행 전에 수정하면 되지만, 런타임 에러의 경우는 실행 도중에 발생하는 에러이기 때문이 이를 대비해야 한다.


2. 예외 클래스 계층 구조

  • 자바에서는 Exception와 Error를 클래스로 정의했다.
  • Throwable 클래스는 Object를 제외하고 Excetion, Error 클래스의 최상위 클래스로 대표적인 2가지 메서드
    • public String getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지 반환
    • public void printStackTrace(): 예외가 발생tl 호출 스택에 있었던 메서드 정보와 예외 메시지 출력

 

a. Exception 계층 구조

Exception 클래스는 두 개의 그룹으로 나눌 수 있다.

  • Exception 클래스와 그 자손(checked exception)
    • RuntimeException 클래스를 상속하지 않은 예외 클래스
    • 반드시 명시적으로 예외처리를 해야한다.
      • ex) ClassNotFoundException: 클래스를 찾지 못할 때 발생하는 예외
public class Main {
    public static void main(String[] args) {
        System.out.println("hello");
        throw new Exception(); // 예외처리 필수
    }
}

실행결과
java: unreported exception java.lang.Exception; must be caught or declared to be thrown

 

  • RuntimeException과 그 자손(unchecked exception)
    • 실행 중(runtime)에 발생할 수 있는 예외
    • 주로 프로그래머의 실수에 의해서 발생하는 예외
    • 예외 처리를 강제적으로 하지 않아도 된다.
  • ex)ArrayIndexOutOfBoundException: 배열의 범위를 벗어날 때 발생하는 예외
public class Main {
    public static void main(String[] args) {
				System.out.println("hello");
        throw new RuntimeException(); // 예외처리를 하지 않아도 정상적으로 컴파일이 되어 main메서드 실행 
    }
}

실행결과
hello
Exception in thread "main" java.lang.RuntimeException
	at org.example.exception.Main.main(Main.java:11)

3. 예외 처리를 위한 try-catch

try {
		// 예외가 발생할 가능성 있는 문장
} catch (Exception1 e1) {
    // Exception1이 발생했을 경우, 이를 처리하기 위한 문장
} catch (Exception2 e2) {
		// Exception2가 발생했을 경우, 이를 처리하기 위한 문자
}
  • if, for문과 달리 { }(괄호)를 생략할 수 없다.
  • catch 블럭을 여러개 사용하여 다양한 Exception에 대한 처리를 할 수 있다.

 

try {
		try {
    } catch (Exception e) {
    }
} catch (Exception e) {
		try {
    } catch (Exception e) { // 에러 발생(참조변수 명)
    }
}
  • try-catch 블럭안에 또 다른 try-catch문을 사용할 수 있다.
  • catch 블럭 내의 try-catch문에 선언되어 있는 참조변수 명은 서로 달라야 한다.

4. try-catch문의 흐름

 

a. try 블럭 내에서 예외가 발생한 경우

  1. 발생한 예외와 일치하는 catch블럭이 있는지 확인
  2. 일치하는 catch 블럭을 찾는다. (못 찾을 경우 예외가 처리되지 않음)
  3. catch 블럭 내의 문장을 수행한다.
  4. try-catch 블럭을 빠져나가서 수행을 계속한다.

 

b. try 블럭 내에서 예외가 발생하지 않은 경우

  1. try-catch 블럭을 빠져나가서 수행을 계속한다.

참고: catch 블럭(예외클래스)를 찾을 때 instancof 연산자를 이용해서 검사하기 때문에 자식 타입의 예외가 발생하면 부모 타입의 예외 클래스의 catch 블럭에 일치

 

 

예외가 발생해서 catch 블럭을 찾은 경우

System.out.println(1);
try {
	System.out.println(2);
	System.out.println(0 / 0); // ArithmeticException 발생
	System.out.println(3); // 실행되지 않음.
} catch (Exception e) {
	System.out.println(4);
}
System.out.println(5);

실행결과
1
2
4
5

 

 

예외가 발생했지만 catch 블럭을 찾지 못하는 경우

  • 예외를 처리하지 못한 것이기 때문에 메서드 호출 스택을 따라 올라가 처리되지 않은 예외로 간주되어 비정상적으로 종료.
System.out.println(1);
try {
	System.out.println(2);
	System.out.println(0 / 0); // ArithmeticException 발생
	System.out.println(3);  // 실행되지 않음.
} catch (NullPointerException e) {
	System.out.println(4);
}
System.out.println(5);

실행결과
1
2
Exception in thread "main" java.lang.ArithmeticException: / by zero at org.example.exception.Main.main(Main.java:11)**

 

 

try- catch 예제1

System.out.println(1);
try {
		System.out.println(2);
    System.out.println(0 / 0); // ArithmeticException 발생
    System.out.println(3);
} catch (Exception e) {
		System.out.println(4);
} catch (ArithmeticException e) {  // 에러 발생
		System.out.println(5);
}
System.out.println(6);
  • ArithmeticException은 Exception의 자식 클래스이다.
  • catch문에서 자식 Exception은 부모 Exception보다 뒤에 올 경우 컴파일 에러가 발생한다.

 

try- catch 예제2

System.out.println(1);
try {
		System.out.println(2);
    System.out.println(0 / 0); // ArithmeticException 발생
    System.out.println(3);
} catch (ArithmeticException e) {
		System.out.println(4);
} catch (Exception e) { 
		System.out.println(5);
}
System.out.println(6);

실행 결과
1
2
4
6
  • ArithmeticException이 발생할 경우 ArithmeticException catch 블럭과 Exception catch 블럭둘 다 해당된다.
  • 하지만 자식 Exception(먼저 만나는 catch) 블럭 문장만 수행한다.

 

c. try 블럭 내에서 예외가 발생하지 않은 경우

  1. catch 블럭을 거치지 않고 try-catch 블럭을 빠져나가서 수행을 계속한다.
System.out.println(1);
try {
		System.out.println(2);
    System.out.println(1 / 1);
    System.out.println(3);
} catch (ArithmeticException e) {
		System.out.println(4);
}
System.out.println(6);

실행결과
1
2
1
3
6

5. 멀티 catch 블럭

JDK1.7부터 여러 catch 블럭을 ‘|’ 기호를 이용해서 하나의 블럭에 여러 Exception을 처리할 수 있도록 추가되었다.

try {
		// ...
} catch (ArithmeticException | NullPointerException e) {
}

try {
		// ...
} catch (ArithmeticException | Exception e) { // 에러 발생
}
  • 부모와 자식관계에 있다면 컴파일 에러가 발생한다.
  • ‘|’ 기호는 논리 연산자가 아닌 기호이다.

6. 예외 발생시키기

throw 키워드를 사용해서 개발자가 고의로 예외를 발생시킬 수 있다.

Exception e = new Exception("예외 발생"); // 1. 예외 클래스의 객체를 만든다.

throw e; // 2. throw 키워드를 이용해서 예외를 발생시킨다.

 

throw 예제

try {
		Exception e = new Exception("예외 발생");
    throw e;
} catch (Exception e) {
		e.printStackTrace();
}

실행결과
java.lang.Exception: 예외 발생 at org.example.exception.Main.main(Main.java:9)**

7. 메서드 예외 선언하기

throws 키워드를 사용해서 메서드에 예외를 선언할 수 있다.

호출된 메서드로 예외를 던지는 역할을 한다.

  • Checked Exception의 경우 상위 메서드로 예외를 던질 때 명시적으로 throws를 사용해야 한다.
  • UnChecked Exception의 경우에는 예외처리를 강제 하지 않기 때문에 예외 처리를 하지 않을 경우 상위 메서드로 예외를 던져 throws 키워드와 동일하게 동작한다.
// throws 기본 사용방법
void method() throws Exception1, Exception2 {
}

// 오버라이딩 시 적용
public class Parent {
    void method() throws Exception {
    }
}

public class Child extends Parent {
    @Override
    void method() throws Exception {
        super.method();
    }
}
  • ,(쉼표)로 구분해서 여러개의 예외를 선언할 수 있다.
  • 부모 클래스의 메서드에 예외가 선언되어 있으면 오버라이딩 시에도 적용된다.

메서드 예외 예제

public class Main {
    public static void main(String[] args) throws Exception {
        method1();
    }

    private static void method1() throws Exception {
        method2();
    }

    private static void method2() throws Exception {
        throw new Exception();
    }
}

실행결과
Exception in thread "main" java.lang.Exception
	at org.example.exception.Main.method2(Main.java:16)
	at org.example.exception.Main.method1(Main.java:12)
	at org.example.exception.Main.main(Main.java:8)
  • method2()에서 예외가 발생했는데 예외를 처리하지 않고 method1()로 예외를 던진다.
  • method1()도 예외를 처리하지 않아 main() 메서드에 예외를 던진다.
  • main() 메서드도 예외를 처리하지 않아 프로그램이 종료되고, jvm의 예외처리기가 예외를 받아서 에러의 원인을 화면에 출력한다.

8. finally 블럭

finally문은 try-catch문 끝에 선택적으로 덧붙여 사용할 수 있다.

try {
		// 예외 발생 가능성 있는 문장
} catch (Exception e) {
		// 예외 처리를 위한 문장
} finally {
		// 예외 발생여부에 관계없이 항상 수행되는 문장
}

 

예외 발생할 경우

try → catch → finally

 

예외 발생 x 경우

tty → fianaaly

 

try-catch 문장 수행중에 return 문을 만나도 finally 문장은 수행된다.

 public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("method1 ()의 try 블럭이 실행 되었습니다.");
            return;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("method1()의 finally 블럭이 실행되었습니다.");
        }

        System.out.println("method1()이 이미 종료되었습니다.");
    }
}

**실행결과**
method1 ()의 try 블럭이 실행 되었습니다.
method1()의 finally 블럭이 실행되었습니다.
  • return을 만나면 실행중인 메서드는 종료되지만, finally 블럭이 실행되고 나서 메서드가 종료된다.

9. try-with-resource문 (자동 resource 반환)

 

try-catch-finally문을 사용한 resource 반환 예제

public class Main {
    public static void main(String[] args) {
        FileInputStream fis;
        DataInputStream dis = null;
        int score = 0;
        int sum = 0;

        try {
            fis = new FileInputStream("src/score.txt");
            dis = new DataInputStream(fis);

            while (true) {
                score = dis.readInt();
                System.out.println(score);
                sum += score;
            }
        } catch (EOFException e) {
            System.out.println("점수의 총합은 " + sum + "입니다");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if(dis != null) {
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

파일로부터 데이터를 읽는 작업이 끝나면 DataInputStream을 close 하는 코드이다. 코드가 복잡하다… try-with-resources문을 사용하면 개선할 수 있다.

 

try-with-resource문을 사용한 자동 resource 반환 예제

public class Main {
    public static void main(String[] args) {
        int score = 0;
        int sum = 0;

        try (FileInputStream fis = new FileInputStream("src/score.txt");
        DataInputStream dis = new DataInputStream(fis)) {
            
            while (true) {
                score = dis.readInt();
                System.out.println(score);
                sum += score;
            }
        } catch (EOFException e) {
            System.out.println("점수의 총합은 " + sum + "입니다");
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
}

try 괄호() 안에 객체를생성하지는 문장을 넣으면 이 객체는 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동으로close()가 호출된다.


10. 사용자 정의 예외

필요에 따라 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception 클래스(Checked Exception), RuntimeException 클래스(Unchecked Exception)로부터 상속받아 예외 클래스를 사용한다.

 

사용자 정의 예외 예제

public class MyException extends Exception {
    private final int ERR_CODE;  // 에러 코드 값을 저장하기 위한 필드

    MyException(String msg, int errCode) {
        super(msg);
        ERR_CODE = errCode;
    }

    MyException(String msg) {
        this(msg, 100);
    }

    public int getErrCode() {
        return ERR_CODE;
    }
}

public class Main {
    public static void main(String[] args) {

        try {
            System.out.println("hello");
            throw new MyException("내가 만든 예외");
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}

실행결과
hello
org.example.exception.MyException: 내가 만든 예외
	at org.example.exception.Main.main(Main.java:13)

checked exception은 try-catch 문 등을 사용하여 반드시 예외를 처리해야 하기 때문에 코드가 복잡해진다. 상황에 따라 다르겠지만, 일반적으로 코드 복잡성 등을 고려해서 unchecked exception을 사용하는 것이 좋다.