Java/객체지향

[객체지향][자바의 정석] - 8.1 예외처리(Exception Handling)

얄루몬 2021. 12. 27. 18:37

8.1 예외처리(Exception Handling)

 

1. 프로그램 오류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

이를 발생시점에 따'컴파일 에러(compile - time error)''런타임 에러(runtime error)'로 나눌 수 있는데, 글자 그대로 '컴파일 에러'는 컴파일 할 때 발생하는 에러이고 프로그램 실행 도중에 발생하는 에러를 '런타임 에러'라고 한다.
이외에도 '논리적 에러(logical error)'가 있는데, 컴파일도 잘되고 실행도 잘되지만 의도한 것과 다르게 동작하는 것을 논리적 에러라고 말한다.
에러 이름 에러 발생시점
컴파일 에러 컴파일 시에 발생하는 에러
런타임 에러 실행 시에 발생하는 에러
논리적 에러 실행은 되지만, 의도와 다르게 동작하는 것

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

 

에러의 종류

  • 에러는 메모리 부족과 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습할 수 있는 비교적 덜 심각한 것이다.
  • 에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.

 

 

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

자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의해놨다. 앞서 배운 것처럼 모든 클래스의 조상은 Object클래스이기 때문에 Exception과 Error 클래스 역시 Object클래스의 자손들이다.

 

예외클래스 계층도
Exception클래스와 RuntimeException클래스

 

예외 클래스들은 두 그룹으로 나눌 수 있다

▶ Exception클래스와 그 자손들 - 사용자의 실수와 같은 외적인 요인에 발생하는 예외
▶ RuntimeException클래스와 그 자손들 - 프로그래머의 실수로 발생하는 예외

 

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

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

  • 예외처리(Exception Handling)의
    • 정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
    • 목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외는 JVM의 '예외처리기'가 받아서 예외의 원인을 화면에 출력한다. 

try {
	//예외가 발생할 가능성이 있는 문장들을 넣는다. 
} catch (Exception1 e1) {
	//Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
	//Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception3 e3) {
	//Exception3이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
public class ExceptionEx1 {
	public static void main(String[] args) {
		try {
			try {  } catch (Exception e) {  }
		} catch(Exception e) {
			try {   } catch (Exception e) {}  //에러 변수 e가 중복 선언되었음
		}
		try {
			
		} catch (Exception e) {
		} //try - catch의 끝
	}//main메서드의 끝
}

위의 코드처럼 try-catch문은 하나의 메스드 내에 여러 개의 try-catch문이 사용될 수 있음을 보여준다. 또한 try 블럭 또는 catch블럭에 또다른 try-catch문이 포함될 수 있다.

 

public class ExceptionEx2 {
	public static void main(String arge[]) {
		int number =100;
		int result =0;
		
		for (int i=0; i < 10; i++) {
			result = number/(int)(Math.random()*10);//7번라인
			System.out.println(result);
		}
	}
}

위의 코드는 number에 저장된 값 100을 0~9 사이의 임의의 정수로 나눈 결과를 출력하는 일을 10번 반복하는 코드이다. random()을 사용했기 때문에 매번 실행할 때마다 결과가 다르지만, 대부분의 경우 10번이 출력되기 이전에 예외가 발생해서 프로그램이 비정상적으로 종료될 것이다. 

  • ArithmeticException - 산술연산과정에서의 오류에 발생하는 오류(정수는 0으로 나누는 것을 금지한다.)

 

<산술연산 과정에서 오류가 발생하는 것을 예외처리로 둔 코드>

public class ExceptionEx3 {
	public static void main(String arge[]) {
		int number =100;
		int result =0;
		
		for (int i=0; i < 10; i++) {
			try {
				result = number/(int)(Math.random()*10);//7번라인
				System.out.println(result);
			} catch(ArithmeticException e) {
				System.out.println("0");
			}
		}
	}
}
100
50
11
20
25
16
33
11
0 --> 예외 발생상황이지만 예외처리로 0이 출력 됨. 
11

*또한 try {} catch{}문에서는 괄호 생략이 불가하다.

 

4. try - catch문에서의 흐름

try - catch문에서, 예외가 발생한 경우와 발생하지 않았을 때의 흐름(문장의 실행순서)이 달라지는데, 아래에 이 두 가지 경우에 따른 문장 실행순서를 정리하였다. 

  • try 블럭 내에서 예외가 발생한 경우
    • 발생한 예외와 일치하는 catch블럭이 있는지 확인한다
    • 일치하는 catch 블럭을 찾게 되면, 그 catch 블랙 내의 문장들을 수행하고 전체 try - catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다. (프로그램 비정상 종료)
  • try 블럭 내에서 예외가 발생하지 않은 경우
    • catch 블럭을 거치지 않고 전체 try - catch문을 빠져나가 수행을 계속한다.
public class Java8_4_1 {
	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) {
			if (ae instanceof ArithmeticException) 
				System.out.println("True");
			System.out.println("ArithmeticException");
		} catch(Exception e) { //모든 예외를 처리
			System.out.println("Exception");
		}
		System.out.println(6);
	}
}
<실행 결과>
1
2
3
True
ArithmeticException
6

 

< 예외처리를 안 해서 비정상적인 종료를 하는 코드 >

//예외처리가 안 되어서 비정상적으로 종료가 됨.
public class Java8_4_2 {
	public static void main(String args[]) {
		System.out.println(1);
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(args[0]);
			System.out.println(4);
		} catch (ArithmeticException ae) {
			if (ae instanceof ArithmeticException) 
				System.out.println("True");
			System.out.println("ArithmeticException");
		}
		System.out.println(6);
	}
}

 

<예외 발생에 정상적인 예외 처리를 진행하여 정상 종료 하는 코드>

//예외가 발생했지만 처리가 되었기에 정상종료임
public class Java8_4_3 {
	public static void main(String args[]) {
		System.out.println(1);
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(args[0]); // ArrayIndexOutOfBoundsException 에러
			System.out.println(4); // 실행되지 않는다.
		} catch (ArithmeticException ae) {
			if (ae instanceof ArithmeticException) 
				System.out.println("True");
			System.out.println("ArithmeticException");
		} catch(ArrayIndexOutOfBoundsException e) {
			System.out.println("ArrayIndexOutOfBoundsException");
		}
		System.out.println(6);
	}
}

 

5. 예외의 발생과 catch블럭

  • 내용이 같은 catch블럭을 하나로 합친 것

부모자식 관계에서는 멀티 catch블럭 말고 저 위의 그림처럼 사용해야 한다.

 

  • 멀티 catch 사용 시 공통 멤버만 사용해야 한다. 어떤 객체를 가르킬지 모르는 상황에서 한 객체 안에 있는 메서드를 사용하는 것은 불가.
  • 정 사용하고 싶다면 아래 if문을 참고해서 사용해야 한다. 

 

6. 예외 발생시키기

  1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다.
    1. Exception e = new Exception("고의로 발생시켰음");
  2. 키워드 throw를 이용해서 예외를 발생시킨다.
    1. throw e;
public class Java8_6_1 {

	public static void main(String[] args) {
		// TODO 자동 생성된 메소드 스텁
		
		try {
			Exception e = new Exception("고의로 발생시켰음");
			throw e; // 예외를 발생시킴
			// throw new Exception("고의로 발생시켰음");
		} catch(Exception e) {
			System.out.println("에러 메시지 : " + e.getMessage());
			e.printStackTrace();
		}
		System.out.println("프로그램이 정상 종료 되었음.");
		
	}

}
에러 메시지 : 고의로 발생시켰음
java.lang.Exception: 고의로 발생시켰음
프로그램이 정상 종료 되었음.
at javajungsuk.Java8_6_1.main(Java8_6_1.java:9)

👉 try문에서 던진 예외를 catch 블럭에서 잡았기 때문에 정상 종료 되었다. 

 

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

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

이 두개 이외에도 다른 메서드가 있지만 제일 많이 사용되어 이 두개를 소개해줌. 

참조변수가 예외 객체를 가르킨다. (ArithmeticException ae) --: ae 참조변수가 예외 객체를 가르킴.

 

8. final 블럭

  • 예외 발생여부와 관계없이 수행되어야 하는 코드를 넣는다.
  • try - catch문의 마지막에 finally 코드를 넣는다.
  • 임시파일 삭제와 같이 매 코드 아래의 수행되어야 하는 코드 같은 경우엔 finally 블럭을 넣어 코드 중복을 제거해준다.

 

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

 

 

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

  • 우리가 직접 예외 클래스를 정의할 수 있다.
  • 조상은 Exception과 RuntimeException 중에서 선택할 수 있다.
    • Exception - 사용자가 발생시키는 예외
      • 예외 처리를 필수로 해야하기 때문에 try - catch문을 사용해야 한다.
    • RuntimeException - 프로그래머의 실수로 발생시키키는 예외 
      • 예외 처리를 선택적으로 할 수 있기 때문에 try - catch문을 선택해서 사용하면 된다.
      • 또한 에외 처리를 선택적으로 할 수 있기에 조상을 RuntimeException을 선택하는 것이 좋다.

 

 

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

  • 예외를 처리한 후에 다시 예외를 발생시키는 것
  • 호출한 메서드와 호출된 메서드 양쪽 모두에서 예외처리하는 것

예외 되던지기

 

//예외 되던지기

public class Java8_11_2 {

	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; //다시 예외를 발생시킨다. 
		}
	}
}

 

12. 연결된 예외(chained exeption)

  • 한 예외가 다른 예외를 발생시킬 수 있다
  • 예외 A가 예외 B를 발생시키면 A는 B의 원인 예외(cause exception)다

이때 A와 B를 연결된 예외라고 한다.

<연결된 예외란? >

  1. 예외 발생 
  2. catch에서 예외처리를 안 하고 새로운 예외를 만듬
  3. 만든 예외(B) 안에 발생한 예외(A)를 넣어서 다시 되던짐
  4. 이때 B안에 A를 넣고 되던진 예외를 우리는 연결된 예외라고 한다.

< 연결된 예외를 쓰는 이유는? >

 

1.여러 예외를 하나로 묶어서 다루기 위해서

    - 예외가 많아지면 catch블럭이 수없이 많아지기 때문에 이를 해결하기 위해서 하나로 묶어 사용한다.

public class Java8_12_3 {
	try {
		startInstall();
		copyFiles();
	} catch(SpaceException e) {
		InstallExcption ie = new InstallException("설치 중 예외발생"); // 예외 발생
		ie.initCause(e); //InstallExcption의 원인 예외를 SpaceException으로 저장
		Throw ie;
	} catch(MemoryException me) {
		...
	}
}

원인 예외를 저장해서 사용하는 방법

연결된 예외 - 예외를 묶어 사용하는 방법

 

2. checked 예외를 un checked 예외로 변경하려 할 때

    - 필수처리를 선택처리로 바꾸려 할 때 사용한다.

 

연결된 예외를 사용하는 이유2 - checked예외를 unchecked예외로 변경하려 할 때

 

13. checked예외, unchecked예외

  • checked - 컴파일러가 예외 처리 여부를 체크(예외 처리 필수 try - catch 필수)
    • Exception( )
  • unchecked - 컴파일러가 예외 처리 여부를 체크 안 함(예외 처리 선택 try - catch 선택)
    • RuntimeException
import javax.management.RuntimeErrorException;

public class Java8_14_1 {

	public static void main(String[] args) {
		try { //Exception과 그 자손은 반드시 예외처리를 해줘야 한다.
			throw new Exception();
		} catch(Exception e) {}
		
		//RuntimeException과 그 자손은 예외처리를 해주지 않아도 컴파일 된다.
		throw new RuntimeException();
	}

}

 

14. 오류 (Error)

  • 컴파일 에러
  • 런타임 에러
    • Error - 심각
    • Exception - 덜 심각
      • Exception과 자손 : checked 예외
      • RuntimeException과 자손 : Unchecked 예외
  • 논리적 에러

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

  • try - catch
  • 예외 선언하기 -> 예외 떠넘기기(알리기) throw/ 예외 발생 - throw
  • 은폐 덮기 (빈 catch 블럭)

예외 선언하기 (예외 떠넘기기)
JVM에서의 예외 처리

-> 폭탄 돌리기마냥 예외를 처리하지 않고 떠넘긴 뒤 마지막에 JVM에서 처리 한다.

 

< 메서드에 예외 선언하기 >

import java.io.File;

//파일 생성에 실패한 코드
public class Java8_11_1 {
	public static void main(String[] args) {
		try {
			File f = createFile("");
			System.out.println(f.getName()+" 파일이 성공적으로 생성되었습니다.");
		} catch(Exception e) {
			System.out.println(e.getMessage()+" 다시 입력해 주시기 바랍니다.");
		}
	}//main 메서드 끝
	static File createFile(String fileName) throws Exception {
		if (fileName == null || fileName.equals(""))
			throw new Exception("파일이름이 유효하지 않습니다.");
		File f = new File(fileName); //File클래스 객체를 만든다
		f.createNewFile();
		return f; //생성된 객체의 참조를 반환한다. 
	}
}
import java.io.File;
//파일 생성에 성공한 코드 
public class Java8_11_1 {
	public static void main(String[] args) {
		try {
			File f = createFile("test2.txt");
			System.out.println(f.getName()+" 파일이 성공적으로 생성되었습니다.");
		} catch(Exception e) {
			System.out.println(e.getMessage()+" 다시 입력해 주시기 바랍니다.");
		}
	}//main 메서드 끝
	static File createFile(String fileName) throws Exception {
		if (fileName == null || fileName.equals(""))
			throw new Exception("파일이름이 유효하지 않습니다.");
		File f = new File(fileName); //File클래스 객체를 만든다
		f.createNewFile();
		return f; //생성된 객체의 참조를 반환한다. 
	}
}

< 제목 없음 파일이 성공적으로 생성되었습니다 코드 >

import java.io.File;

public class Java8_11_1 {
	public static void main(String[] args) {
		try {
			File f = createFile("");
			System.out.println(f.getName()+" 파일이 성공적으로 생성되었습니다.");
		} catch(Exception e) {
			System.out.println(e.getMessage()+" 다시 입력해 주시기 바랍니다.");
		}
	}//main 메서드 끝
	static File createFile(String fileName) throws Exception {
		 try {
			 if (fileName == null || fileName.equals(""))
					throw new Exception("파일이름이 유효하지 않습니다.");
		 }catch(Exception e) {
			 fileName = "제목 없음";
		 }
		File f = new File(fileName); //File클래스 객체를 만든다
		f.createNewFile();
		return f; //생성된 객체의 참조를 반환한다. 
	}
}