Reading/자바의 정석

Chapter 8. 예외처리

조화이트 2023. 3. 25. 01:16
728x90

1. 프로그램 오류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다.

이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

이를 발생 시점에 따라 ‘컴파일 에러’와 ‘런타임 에러’로 나눌 수 있다.

  • 컴파일 에러: 컴파일 시 발생하는 에러
  • 런타임 에러: 실행 시 발생하는 에러

여기에 추가로 컴파일도 잘 되고 실행도 잘 되지만 의도한 것과 다르게 동작하는 것을 논리적 에러라고 한다.

런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다.

자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 ‘에러(error)’와 예외(exception)’ 두 가지로 구분하였다.

에러는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.

예외는 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.

 

2. 예외 클래스의 계층 구조

모든 예외의 최고 조상은 Exception 클래스이며 상속 계층도를 Exception 클래스로부터 도식화하면 다음과 같다.

위 그림에서 볼 수 있듯이 예외 클래스들은 다음과 같이 두 그룹으로 나누어 질 수 있다.

  1. Exception클래스와 하위 클래스들(RuntimeException과 그 하위클래스들 제외)
  2. RuntimeException클래스와 하위 클래스들

 

2번과 같은 RuntimeException 클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들이다.

  • 배열의 범위를 벗어난 경우(ArrayIndexOutOfBoundsException)
  • 값이 null인 참조변수의 멤버를 호출하는 경우(NullPointerException)
  • 클래스 간의 형 변환을 잘못한 경우(ClassCastException)
  • 정수를 0으로 나누려고 하는 경우(ArithmeticException)

반대로 1번과 같은 Exception 클래스들은 주로 외부의 영향으로 발생할 수 있는 것들로서 프로그램의 사용자들의 동작에 의해서 발생하는 경우가 많다.

  • 존재하지 않는 파일의 이름을 입력한 경우(FileNotFoundException)
  • 클래스의 이름을 잘못 적은 경우(ClassNotFoundException)
  • 입력한 데이터 형식이 잘못된 경우(DataFormatException)

 

3. 예외 처리하기 - try-catch 문

예외처리(exception handling)란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이며, 예외처리의 목적은 예외의 발생으로 인한 실행 중인 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행 상태를 유지할 수 있도록 하는 것이다.

try {
	// 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
	//Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
	//Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception3 e3) {
	//Exception3이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}

발생한 예외의 종류와 일치하는 단 한 개의 catch 블럭만 수행되며 발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.

 

4. try-catch문에서의 흐름

  • try 블럭 내에서 예외가 발생한 경우
    1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
    2. 일치하는 catch 블럭을 찾게 되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch 문을 빠져나가서 그 다음 문장을 계속해서 수행한다.
  • try 블럭 내에서 예외가 발생하지 않은 경우
    catch 블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.
class ExceptionEx6 {
	public static void main(String args[]) {
		System.out.println(1);
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(0/0); <-예외 발생
			System.out.println(4); //출력되지 않음
		} catch (Exception e) {
			System.out.println(5);
		}
		System.out.println(6);
	}
}

//결과
1
2
3
5
6

7번 라인에서 예외가 발생해서 catch 블럭 수행 후 try-catch 문을 빠져 나갔기 때문에 4는 출력이 되지 않았다.

 

5. 예외의 발생과 catch 블럭

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.

catch블럭의 괄호()에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있다.

 

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

public class Main {
    public static void main(String[] args){
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
            System.out.println("예외메시지 : " + ae.getMessage());
        }
        System.out.println(6);
    }
}

//결과
1
2
3
java.lang.ArithmeticException: / by zero
	at Study.Main.main(Main.java:7)
예외메시지 : / by zero
6

JDK1.7부터 여러 catch블럭을 ‘|’ 기호를 이용해서 하나의 catch블럭으로 합칠 수 있게 되었으며, 이를 멀티 catch 블럭이라고 한다.

try {
	...
} catch (ExceptionA e) {
	e.printStackTrace();
} catch (ExceptionB e2) {
	e2.printStackTrace();
}

//멀티 catch 블럭 사용
try {
	...
} catch (ExceptionA | ExceptionB e) {
	e.printStackTrace();
}

만일 멀티 catch 블럭에서 연결된 예외 클래스가 상속 관계에 있다면 컴파일 에러가 발생한다.

상위 클래스만 작성해도 예외 처리가 가능하기 때문에 불필요한 코드는 제거하라는 의미에서 발생하는 것이다.

 

6. 예외 발생시키기

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

  1. 먼저 연산자 new 를 이용해서 발생시키려는 예외 클래스의 객체를 만든다. Exception e = new Exception(”고의로 발생시켰음”);
  2. 키워드 throw를 이용해서 예외를 발생시킨다. throw e;
  3. 혹은 1번과 2번을 합쳐서 throw new Exception(”고의로 발생시켰음”);
class ExceptionEx11 {
	public static void main(String[] args) {
		throw new RuntimeException();
	}
}

이 예제는 예외처리를 하지 않았음에도 성공적으로 컴파일이 되지만 실행하면 RuntimeException이 발생하여 비정상적으로 종료된다.

RuntimeException 클래스들은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는 것이다.

 

7. 메서드에 예외 선언하기

메서드에 예외를 선언하려면 메서드의 선언부에 throws 키워드를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다.

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

다만 이렇게 예외를 선언하면 자손타입의 예외까지도 발생할 수 있다는 점에 주의해야 한다.

또한 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만, 예외를 단순히 전달만 하는 것이기 때문에 결국 어느 한 곳에서는 반드시 try-catch문으로 예외처리를 해주어야 한다.

 

8. finally 블럭

finally 블럭은 예외의 발생 여부에 상관 없이 실행되어야 할 코드를 포함시킬 목적으로 사용된다.

try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.

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

 

9. 자동 자원 반환 - try-with-resources문

입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 줘야 하는 것들이 있다. 그래야 사용했던 자원(resources)이 반환되기 때문이다.

try {
	fis = new FileInputStream("score.dat");
	dis = new DataInputStream(fis);
} catch (IOException ie) {
	ie.printStackTrace();
} finally {
	dis.close();
}

여기서 문제는 close()가 예외를 발생시킬 수 있다는 데 있다. 그래서 위의 코드는 아래와 같이 해야 올바른 것이 된다.

try {
	fis = new FileInputStream("score.dat");
	dis = new DataInputStream(fis);
} catch (IOException ie) {
	ie.printStackTrace();
} finally {
	try {
		if(dis!=null) {
			dis.close();
	} catch(IOException ie) {
			ie.printStackTrace();
	}
}

하지만 이렇게 하면 우선 코드가 복잡해져서 보기에 좋지 않고 try 블럭과 finally 블럭에서 모두 예외가 발생하면 try 블럭의 예외는 무시된다는 것이다.

이 점을 개선하기 위해 사용하는 것이 try-with-resources문이다.

try (**FileInputStream fis = new FileInputStream("score.dat");
	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 ie) {
	ie.printStackTrace();
}

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

그 다음에 catch 블럭 또는 finally 블럭이 수행된다.

 

10. 사용자 정의 예외 만들기

필요에 따라 새로운 예외 클래스를 정의하여 사용할 수 있다.

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;
	}
}

이렇게 함으로써 MyException이 발생했을 때, catch 블럭에서 getMessage()와 getErrCode()를 사용해서 에러코드와 메시지를 모두 얻을 수 있을 것이다.

 

기존의 예외 클래스는 주로 Exception을 상속 받아서 ‘checked예외’로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속해서 작성하는 쪽으로 바뀌어가고 있다.

 

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

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써 양쪽에서 나눠서 처리되도록 할 수 있다.

심지어는 단 하나의 예외에 대해서도 양쪽에서 처리하도록 하는 것이 가능하다.

이것은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 ‘예외 되던지기’라고 한다.

 

먼저 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외를 처리해주고 catch문에서 필요한 작업을 행한 후에 throw문을 사용해서 예외를 다시 발생시킨다.

다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리한다.

이 방법은 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용한다.

class ExceptionEx17 {
	public static void main(String[] args) {
		try {
			method1();
		} catch (Exception e) {
		System.out.println("main메서드에서 예외가 처리되었습니다.");
		}
	}

	static void method1() throws Exception {
		try {
			throw new Exception();
		} catch (Exception e) {
			System.out.println("method1메서드에서 예외가 처리되었습니다.");
			throw e;
		}
	}
}

//결과
method1메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.

 

12. 연결된 예외(chained exception)

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 ‘원인 예외(cause exception)’ 라고 한다.

try {
	startInstall() {
	copyFiles();
} catch (SpaceException e) {
	InstallException ie = new InstallException("설치 중 예외 발생");
	ie.initCause(e);
	throw ie;
} catch (MemoryException me) {
	...

발생한 예외를 그냥 처리하면 될 텐데, 왜 원인 예외로 등록해서 다시 예외를 발생시키는 걸까?

  1. 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해
  2. checked예외를 unchecked예외로 바꿀 수 있도록 하기 위해
728x90
반응형