[자바의 정석 기초편] 예외처리

JEREGIM·2023년 1월 24일
0

📌프로그램 오류

컴파일 에러(compile-time error) : 컴파일할 때 발생하는 에러

논리적 에러(logical error) : 작성 의도와 다르게 동작하는 에러

런타임 에러(runtime error) : 실행할 때 발생하는 에러

  • Java의 런타임 에러
  1. 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  2. 예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
    -> 에러(error)는 어쩔 수 없지만, 예외(exception)는 처리하자.

예외처리(exception handling)의 정의와 목적

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

    상속계층도

  • Exception 클래스와 그 자손들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
    - IOException : 입출력 예외
    - ClassNotFoundException : 클래스 파일(*.class)이 존재하지 않는 예외

  • RumtimeException 클래스와 그 자손들 : 프로그래머의 실수로 발생하는 예외
    - ArithmeticException : 산술 계산 예외(0으로 나눌 때)
    - ClassCastException : 형변환 예외
    - NullPointerException : 널포인터 예외(가리키는 객체가 null 일 때)
    - IndexOutOfBoundsException : 배열 범위를 벗어날 때 발생하는 예외


📌예외 처리하기 try-catch 문

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

class Ex8_1 {
	public static void main(String[] args) {
			System.out.println(1);			
			try {
				System.out.println(2);
				System.out.println(3);
			} catch (Exception e) {
				System.out.println(4);
			}
			System.out.println(5);
	}
}

결과
1
2
3
5

  • catch 블럭을 거치지 않고 전체 try-catch 문을 빠져나가서 수행을 계속한다.

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

class Ex8_2 {
	public static void main(String[] args) {
			System.out.println(1);
			try {
				System.out.println(0/0);
				System.out.println(2);
			} catch (ArithmeticException ae) {
				System.out.println(3);
			}
			System.out.println(4);
	}
}

결과
1
3
4

  • 0으로 나누면 산술계산 예외(ArithmeticException) 발생
  • 예외가 발생하면 예외와 일치하는 catch 문으로 가서 블럭 내의 문장들을 수행한 후 try-catch 문을 빠져나가 다음 문장들을 수행한다. 예외와 일치하는 catch 문이 없으면 프로그램이 비정상 종료 된다.
  • System.out.printf("2 "); : try 블럭 내에서 예외가 발생하면 그 다음 문장들은 실행되지 않는다.
class Ex8_4 {
	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

  • catch (Exception e){ System.out.println("Exception"); } : Exception 은 모든 예외의 최고 조상이기 때문에 모든 예외 처리가 가능하다. 그래서 마지막 catch 블럭에 쓰인다.

📌printStackTrace()와 getMessage()

printStackTrace() : 예외 발생 당시의 호출스택(Call stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.

getMessage() : 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

class Ex8_5 {
	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 Ex8_5.main(Ex8_5.java:8)
예외 메시지 = / by zero
6

  • 예외가 발생하면 예외 객체가 생성된다.
  • 예외와 일치하는 catch 문을 찾고 참조변수 ae가 예외 객체(ArithmeticException 인스턴스)를 가리킨다.
  • 참조변수 ae의 유효범위(scope)는 catch 문 블럭 내이다.
  • ae.printStackTrace(); : java.lang.ArithmeticException: / by zero
    at Ex8_5.main(Ex8_5.java:8)
  • System.out.println("예외 메시지 : " + ae.getMessage()); :
    예외 메시지 = / by zero

📌멀티 catch 블럭

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

try {                               try {
	...                             	...
} catch (ExceptionA e) {            } catch (ExceptionA | ExceptionB e) {
	e.printStackTrace();     ->      	e.printStackTrace();
} catch (ExceptionB e2) {           }
	e2.printStackTrace();
}    
  • 코드의 중복을 제거

제약조건

  1. 부모, 자식 관계의 catch 블럭은 멀티 catch 블럭으로 사용 불가능

    • 부모 타입의 참조변수만으로도 사용 가능하기 때문에
  2. 참조변수로 사용할 수 있는건 두 예외객체의 공통멤버만 사용 가능

    • instanceof 연산자를 통해 확인하고 형변환 후 사용할 수 있다.
try {
	...
} catch (ExceptionA | ExceptionB e) {
	e.methodA(); // 에러
    
    if(e instanceof ExceptionA) {
    	ExceptionA e1 = (ExceptionA) e; // 형변환
        e1.methodA();
    } else {
    	...
    }
}    
  • ExceptionA와 ExceptionB의 공통 멤버만 사용
  • 사용하고 싶다면 instanceof 연산자를 통해 확인하고 형변환을 통해 사용
  • 잘 사용하지 않는다. 이렇게 쓸바엔 멀티 catch 블럭을 사용하지 말고 그냥 따로 쓰는게 좋다.

📌예외 발생시키기

  1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든다.
  2. 키워드 throw를 이용해서 예외를 발생시킨다.
class Ex8_6 {
	public static void main(String[] args) {
		try {
			Exception e = new Exception("고의로 발생");
			throw e;	 // 예외를 발생시킴
		//  throw new Exception("고의로 발생"); 

		} catch (Exception e)	{
			System.out.println("에러 메시지 : "+e.getMessage());
			e.printStackTrace();
		}
		System.out.println("프로그램이 정상 종료되었습니다.");
	}
}
  • throw new Exception("고의로 발생"); 위의 두 문장을 한 줄로 줄일 수 있다.

📌checked 예외, unchecked 예외

checked 예외(Exception과 자손) : 컴파일러가 예외 처리 여부를 체크(예외 처리(try-catch) 필수)

unchecked 예외(RuntimeException과 자손) : 컴파일러가 예외 처리 여부를 체크 안함(예외 처리 선택)

  • 그렇지 않으면 거의 모든 블럭에 예외 처리를 해줘야하기 때문에
class Ex8_7 {
	public static void main(String[] args) {
		throw new RuntimeException();
	}
}

결과
Exception in thread "main" java.lang.RuntimeException
at Ex8_7.main(Ex8_7.java:3)

  • unchecked 예외는 컴파일 에러가 발생하지 않는 것 뿐이지, 실행 시 예외는 발생한다.

📌메서드에 예외 선언하기

예외를 처리하는 방법 : try-catch 문, 예외 선언하기

  • try-catch 문 : 직접 예외 처리
  • 예외 선언하기 : 예외 떠넘기기(알리기)
  • 은폐(무시) : 빈 catch 문

메서드가 호출 시 발생 가능한 예외를 호출하는 쪽에 알리는 것

static void startInstall() throws SpaceException, MemoryException {
	if(!enoughSpace())
    	throw new SpaceException("설치할 공간이 부족합니다.");
    if(!enoughMemory())
    	throw new MemoryException("메모리가 부족합니다.");
}        
  • 메서드에 예외 선언할 때는 메서드 선언부에 throws, 예외를 발생시키는 키워드는 throw 잘 구별하자.

예외 선언할때는 예외 처리가 필수인 checked 예외만 선언해준다. unchecked 예외는 안적는다.

예외를 선언만 해서는 안된다. 메서드를 호출하는 곳이든, 메서드를 수행하는 곳이든지 예외를 처리해줘야 한다.

import java.io.*;

class Ex8_10 {
	public static void main(String[] args) {
		try {
			File f = createFile("");
			System.out.println( f.getName()+" 파일이 성공적으로 생성되었습니다.");
		} 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() 메서드에서 예외를 선언하고 main() 메서드에서 예외 처리
import java.io.*;

class Ex8_10 {
	public static void main(String[] args) {
			File f = createFile("");
			System.out.println( f.getName()+" 파일이 성공적으로 생성되었습니다.");
	}

	static File createFile(String fileName)  {
		try {
			if (fileName == null || fileName.equals(""))
				throw new Exception("파일 이름이 유효하지 않습니다.");
		} catch (Exception e) {
			fileName = "제목없음.txt";
		}
		File f = new File(fileName);
		try {
			f.createNewFile();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		return f;
	}
}

결과
제목없음.txt 파일이 성공적으로 생성되었습니다.

  • createFile() 메서드에서 예외 처리
  • main() 메서드에서는 예외 발생 여부를 알 수 없다.

예외 처리를 예외가 발생한 곳에서 해줘야 하는지, 아니면 메서드를 호출한 곳에서 해줘야 하는지 적절히 판단해서 예외를 처리해줘야 한다.


📌finally 블럭

예외 발생 여부와 상관없이 수행되어야 하는 코드를 넣는다.

try-catch 문의 맨 마지막에 위치해야한다.

try 블럭 안에 return 문이 있어서 try 블럭을 벗어나갈 때도 finally 블럭은 실행된다.

try {                               try {
	startInstall();						startInstall();
    copyFiles();						copyFiles();
    deleteTempFiles();				} catch (Exception e) {
} catch (Exception e) {       ->    	e.printStackTrace(); 
	e.printStackTrace();            } finally {
    deleteTempFiles();					deleteTempFiles(); // 코드의 중복 제거
}									}

📌사용자 정의 예외 만들기

직접 예외 클래스를 정의할 수 있다.

사용자 정의 예외 만드는 법

class MyException extends Exception {
	MyException(String msg) { // 문자열을 매개변수로 받는 생성자
    	super(msg); // 조상인 Exception 클래스의 생성자를 호출
    }
}
  1. 조상을 Exception 과 RuntimeException 중에서 선택
  2. 문자열을 매개변수로 받는 생성자를 만든다.

📌예외 되던지기(Exception re-throwing)

예외를 처리한 후 다시 예외를 발생시키는 것

호출한 메서드와 호출된 메서드 양쪽 모두에서 예외 처리하는 것

class Ex8_12 {
	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 메서드에서 예외가 처리되었습니다.


📌연결된 예외(Chained exception)

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

class Ex8_13 {
	public static void main(String[] args) {
		try {
			install();
		} catch(InstallException e) {
			e.printStackTrace();
		} catch(Exception e) {
			e.printStackTrace();		
		}
	}

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

	static void startInstall() throws SpaceException, MemoryException {
		if(!enoughSpace()) {
			throw new SpaceException("설치할 공간이 부족합니다.");
		}

		if (!enoughMemory()) {
			throw new MemoryException("메모리가 부족합니다.");
//			throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
		}
	}

   static void copyFiles()       { /*  */ }
   static void deleteTempFiles() { /*  */ }
   
   static boolean enoughSpace() {
		return false;
   }
   static boolean enoughMemory() {
		return true;
   }
}

class InstallException extends Exception {
	InstallException(String msg) {
	   super(msg);	
   }
} 

class SpaceException extends Exception {
	SpaceException(String msg) {
	   super(msg);	
   }
} 

class MemoryException extends Exception {
	MemoryException(String msg) {
	   super(msg);	
   }
}
catch (SpaceException e)	{
			InstallException ie = new InstallException("설치 중 예외 발생");
			ie.initCause(e); // SpaceException 을 원인 예외로 지정
			throw ie;
		}
  • SpaceException 예외는 InstallException 예외의 원인 예외

  • SpaceException 예외와 InstallException 예외는 연결된 예외

  • SpaceException 예외를 InstallException 예외 안에 포함시킴

연결된 예외를 사용하는 이유

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

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

결과
InstallException: 설치 중 예외 발생
at Ex8_13.install(Ex8_13.java:17)
at Ex8_13.main(Ex8_13.java:4)
Caused by: SpaceException: 설치할 공간이 부족합니다.
at Ex8_13.startInstall(Ex8_13.java:31)
at Ex8_13.install(Ex8_13.java:14)
... 1 more

  • InstallException: 설치 중 예외 발생 -> 발생 예외(대략 정보)

  • Caused by: SpaceException: 설치할 공간이 부족합니다. -> 원인 예외(세부 정보)

  • 예외가 발생된 대략적인 정보와 세부적인 정보를 볼 수 있어서 보는 사람 입장에서 보기 편하다.

  • 예외를 처리하는 입장에서도 여러 예외를 묶어서 다루는게 더 간단해진다.

2. checked 예외(예외 필수)를 unchecked 예외(예외 선택)로 변경하려고 할 때

static void startInstall() throws SpaceException, MemoryException {
	if(!enoughSpace()) {
		throw new SpaceException("설치할 공간이 부족합니다.");
	}

	if (!enoughMemory()) {
		throw new MemoryException("메모리가 부족합니다.");
	}
}
  • MemoryException 을 unchecked 예외로 바꾸려면 조상을 RuntimeException 으로 바꾸면 된다. 그러나 이미 MemoryEception이 다른 곳에서도 많이 사용되고 있다면 바꾸기 어렵기 때문에 이때 연결된 예외를 사용한다.
static void startInstall() throws SpaceException {
	if(!enoughSpace()) {
		throw new SpaceException("설치할 공간이 부족합니다.");
	}

	if (!enoughMemory()) {
		throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
	}
}
  • RuntimeException 을 만들고 MemoryException 을 원인 예외로 등록

  • unchecked 예외로 바뀌었기 때문에 선언부에서 MemoryException 삭제

  • 처음 예외를 만들 때와 현재 환경이 많이 달라졌기 때문에 checked 예외 중에서도 불필요하게 try-catch 문을 쓸 필요가 없는 예외가 생기게 됨. 이럴 때 연결된 예외를 사용

0개의 댓글