예외(Exception) 처리

주8·2023년 1월 16일
0

Exception

  • 자바에서는 컴파일 에러(compile-time error)와 런타임 에러(runtime error)가 발생한다.

    • 컴파일 에러: 컴파일시 발생하는 에러
    • 런타임 에러: Java 애플리케이션이 실행시 발생하는 에러
  • 에러(Error)는 수습될 수 없는 심각한 오류, 예외(Exception)는 처리 가능한 오류이다.

  • Exception은 프로그램 실행시 예상치 못한 일로 인해 발생할 수 있는 에러를 말한다.

    • Exception이 발생하는 예
    • 존재하지 않는 파일을 Open한 경우
    • 네트워크 접속에 장애가 날 경우
    • 클래스 로딩시 오류가 발생할 경우
    • 0으로 나눌 경우
  • Java에서 ‘의미적 제약(semantic constraint)’을 위반했을 때 JVM은 프로그램에게 Exception(예외)이라는 에러가 발생했다고 알린다.

  • 어떤 언어는 이런 에러가 발생했을 때 프로그램을 강제 종료한다.

  • 이러한 예외는 프로그래밍시에 흔히 발생할 수 있는 일로 이러한 일이 발생했을 때 프로그래머가 원하는 방향으로 움직이도록 만드는 일을 예외처리(Exception Handling)라고 하고, 예외처리는 애플리케이션의 정상적인 흐름을 유지할 수 있도록 오류를 처리하는 강력한 메커니즘이다.

  • 자바는 예외가 발생했을 경우, JVM에서 예외를 던지고(Throw), 예외가 발생한 지점에서 프로그래머가 지정해 놓은 위치로 제어의 비지역이동(non-local jump)이 발생한다.

    • 비지역이동(non-local jump): 비지역 이동은 제어의 흐름을 주어진 문맥의 외부로 이동시키고, 미리 선언된 지점에서 이어서 수행된다.
    • 예외, 조건, Continuation은 비지역 제어구문의 일반적인 예이다.
  • 예외는 발생한 지점에서 ‘던진다(throw)”고 하고 제어가 이행된 위치에서 ‘잡는다(catch)’라고 한다.

  • 배열의 인덱스 범위를 벗어나서 접근하려 할 때 예외가 발생한 경우다.

Runtime Exception

public class RuntimeExceptionEx01{
	public static void main(String[] args){
		int i=0;
		String strArr[]={
			"Hello World", "Array", "Example"
		};
		while(i<4){
			System.out.println(strArr[i]);
			i++;
		}
	}
}

[결과]
Hello World
Array
Example
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
Index 3 out of bounds for length 3 at
ch10.RuntimeExceptionEx01.main(RuntimeExceptionEx01.java:11)

Exception 처리 키워드

  • try: 예외가 발생할 것으로 예상되는 코드를 블럭으로 지정하는데 사용되며, try 블록 다음에는 catch 또는 finally가 와야 한다.
  • catch: 예외를 처리하는 데 사용된다. catch 블록만 단독으로 사용할 수 없고, try 블록이 앞에 와야 하며, catch 블럭이 뒤에 올 수 있다.
  • finally: 예외 발생과 상관없이 프로그림의 필요한 코드를 무조건 실행하는데 사용된다.
  • throw: 예외를 발생키기는데 사용된다.
  • throws: 예외를 선언하는데 사용되며, 메서드를 호출한 곳으로 전가 (전가된 곳에서 예외를 처리하거나 다시 throws 할 수 있다.)하며, 메서드 선언시 함께 사용된다.
  • 예외 처리 용도
    • 예외 발생시 정상 종료
    • 예외 발생시 예외 내용 보고
    • 예외 발생시 무시하고 계속 실행
    • 예외 발생시 대안적인 결과값을 대입

예외처리 try-catch 구문

  • 메서드 내에서 사용되며, 예외가 발생될 것으로 예상되거나, 예외를 throw 할 수 있는 코드를 묶어서 처리할 때 사용된다.
  • 예외가 발생할 것으로 예상되는 코드 블록을 try로 감싼다.
  • catch문에 발생할 것으로 예상되는 예외 클래스를 선언한다. Exception 클래스로 선언하기 보다는 throw될 것으로 예상되는 예외를 선언하는 것이 좋다.
  • 하나의 try블럭에 여러 catch 블럭을 선언할 수 있다.
  • finally 블록은 항상 실행되는 코드 블록이다. catch문에 잡히지 않는 예외가 발생하더라도 강제적으로 처리해야 하는 코드를 기술한다. (예: DB or Network Connection close 등)
  • 오직 System.exit() method가 호출될 때만 finally block이 실행되지 않는다.
  • try블록에서 예외가 발생하면 나머지 코드는 실행되지 않는다. 그렇기 때문에 예외를 throw하지 않는 코드를 try블록에 두지 않는 것이 좋다.
  • try ~ catch ~ finally
try{
	예외 발생 예상 코드 블록;
} catch (예외_발생_예상_클래스_타입1 타입변수명1){
	타입1 예외 발생시 처리할 내용;
} catch (예외_발생_예상_클래스_타입2 타입변수명2){
	타입2 예외 발생시 처리할 내용;
} finally {
	예외 발생과 상관없이 처리해야 할 내용(try~catch블럭에서 return문이 있어도 실행)
}
  • JVM은 먼저 예외가 처리되었는지 여부를 확인하고, 처리되지 않으면 JVM은 예외에 대한 정보, stack trace(스택 추적: 예외가 발생한 메서드의 계층 구조)를 출력하고, 프로그램을 종료시킨다.
  • 일반적으로 stack trace는 로깅 처리를 통해 로그 파일에 기록되며, 모니터링 툴에서는 로그 파일에 기록된 예외를 별도로 수집하여 확인할 수 있는 기능들을 제공한다.

try-catch-finally 처리 예

  • 배열의 인덱스 범위를 넘어서는 접근이 발생할 경우 ArrayIndexOutOfException이 발생하기 되는데, 예외 발생이 예상되는 구간을 try~catch 블럭으로 감싸고, 예외와 상관없이 무조건 실행되어야 하는 기능들은 finally 블럭에서 구현한다. (예: 리소스 해제 등)
  • 예제 소스를 실행 시 예외가 발생하면 catch 블럭에서 i 값을 0으로 초기화하게 되면서 while문이 종료되지 않고 반복적으로 실행된다.
  • finally 블럭은 예외 상관없이 실행되는 것을 확인할 수 있다.
public class ExceptionFinallyEx01{
	public static void main(String[] args){
		int i=0;

		String[] strArr = {
			"Hello world",
			"My name",
			"GilDong"
		};
		while(i < 4){
			try{
				System.out.println(strArr[i]);
				i++;
			}catch(ArrayIndexOutOfBoundsException e){
				System.out.println("index 값 재설정");
				i = 0;
			}finally{
				System.out.println("finally block 실행");
			}
		}
	}
}

[결과]
Hello world
finally block 실행 //예외와 상관없이 무조건 실행된다.
My name
finally block 실행
GilDong
finally block 실행
index 값 재설정 //catch block(i 변수값을 0으로 초기화)
finally block 실행
Hello world //while문이 다시 반복적으로 실행됨
finally block 실행
My name
...
  • try 블록에서 예외가 발생하면 나머지 코드는 실행되지 않는다. 그렇기 때문에 예외를 throw하지 않는 코드는 try 블록 밖에 구현하도록 한다.
public class RestCodeTryCatchEx1{
	public static void main(String[] args){
		try{
			int data=50/0; //예외가 throw된다.
			//상위 코드에서 예외가 발생하면 나머지 코드는 실행되지 않는다.
			System.out.println("나머지 코드");
		}catch(ArithmeticException e){
			System.out.println(e);
		}
	}
}

[결과]
java.lang.ArithmeticException: / by zero
public class RestCodeTryCatchEx1{
	public static void main(String[] args){
		try{
			int data=50/0; //예외가 throw된다.
		}catch(ArithmeticException e){
			System.out.println(e);
		}
	}
	System.out.println("나머지 코드");
}

[결과]
java.lang.ArithmeticException: / by zero
나머지 코드

Throws clause(절)

  • throws문은 자신을 호출한 곳으로 예외를 처리하지 않고 되돌릴 때 사용하는 키워드
return_type method_name throws throwExceptionClass [, otherExceptionClass...]
  • 아래의 코드 블록에서 IOException이 발생하더라도 try~catch문으로 예외를 처리하지 않고 runMethod()를 호출한 곳으로 예외를 throw하겠다는 것이다.
public void runMethod() throws IOException{
	code block;
}

Throw statement(문)

  • throw문은 메소드의 throw type으로 정의된 타입과 그 타입의 하위클래스 타입으로 예외를 생성하여 throw할 경우 사용한다.
  • throw문은 강제적으로 예외를 발생시켜야 할 경우 사용하는데 예를 들자면 애플리케이션의 예외가 아닌 비즈니스 오류로 인한 예외를 throw하여 프로그램 흐름을 제어하고자 할 경우에 자주 사용된다.
throw new throwExceptionClass("예외 내용");

public void runMethod() throws Exception{
	...
	if(조건) throw new Exception("조건 불만족");
	...
}
  • Unchecked, checked 예외 둘 다 throw가 가능하다.
  • throw 키워드로 checked 예외를 던지면 catch 블록을 사용하여 예외를 처리하거나 메서드에서 throws 선언을 사용하여 선언해야 한다.
public class TestThrowUnChk{
	public static void validate(int age){
		if(age>18){
			throw new ArithmeticException("투표 자격 없음");
		}else{
			System.out.println("투표 자격 있음");
		}
	}
	//main method
	public static void main(String[] args){
		validate(13);
		System.out.println("나머지 코드...");
	}
}

[결과]
Exception in thread "main" java.lang.ArithmeticExcemption: 투표 자격 없음 
								at ch12.testthrow.TestThrowUnChk.validate(TestThrowUnChk.java:6)
								at ch12.testthrow.TestThrowUnChk.main(TestThrowUnChk.java:14)
public class TestThrowChk{
	public static void method() throws FileNotFoundException{
		FileReader file = new FileReader("abc.txt");
		BufferedReader fileInput = new BufferedReader(file);
		throw new FileNotFoundException();
	}
	//main method
	public static void main(String[] args){
		try{
			method();
		}catch(FileNotFoundException e){
			e.printStackTrace();
		}
		System.out.println("나머지 코드...");
	}
}

[결과]
java.io.FileNotFoundException: abc.txt (No such file or directory)
나머지 코드...

Call Stack Mechanism (예외 전파)

  • 만약 예외가 발생한 메소드에서 처리를 하지 않으면, 메소드를 호출한 곳으로 throw된다.
  • 만약 main method에서 예외를 처리하지 않으면, 프로그램은 비정상적으로 종료된다.
  • main() → first() → second() call stack
    • second()에서 예외 발생 no catch throw → next method call stack first()
    • first() no catch → next method call stack main()
    • main() last method on the call stack no catch
    • runtime error 발생되면서 프로그램 종료
public class NullPointerClass{
	public static void main(Stirng[] args){
		crunch(null);
	}
	static void crunch(int[] a){
		mash(a);
	}
	static void mash(int[] b){
		System.out.println(b[0]);
	}
}

Exception in thread "main" java.lang.NullPointerException
					at ch12.callstack.NullClass.mash(NullClass.java:11)
					at ch12.callstack.NullClass.crunch(NullClass.java:8)
					at ch12.callstack.NullClass.main(NullClass.java:5)
  • main() 메서드, p() 메서드, n() 메서드 또는 m() 메서드의 호출 스택에 있는 모든 메서드에서 예외를 처리할 수 있습니다.

Exception 클래스

  • java.lang.Throwable 클래스는 Exception 및 Error의 두 하위 클래스에서 상속된 예외 계층의 루트 클래스이다.
  • Exception 및 Error의 하위 클래스들을 통틀어 ‘예외 클래스(Exception class)’라고 한다.
  • 예외처리가 가능한 Exception type class들과 발생하면 복구할 수 없는 심각한 오류인 Error type class들이 있다. (Error의 경우 OutOfMemoryError 등이 있다)
  • 전체적인 상속 구조는 java API Document를 통해 확인할 수 있다.

Overview (Java SE 11 & JDK 11 )


Method Overriding 예외 처리

  • 하위클래스에서 상위클래스의 메소드를 오버라이딩 할 때 상위클래스의 메소드에 throws절이 정의되어 있다면 오버라이딩시에 throws절에 동일한 타입의 exception class나 하위 type의 exception class type으로 정의해야만 한다.
  • 상위클래스 메소드의 throws Exception type보다 상위 type으로는 선언할 수 없다.
  • 오버라이딩시에 상위클래스 메소드의 throws절의 예외 클래스 타입에 대해 하위클래스에서 여러 개의 동일하거나 또 다른 하위 예외 클래스 타입으로 정의가 가능하다.

User-defined Exception (사용자 정의 예외)

  • 예외클래스를 상속하여 사용자 정의 예외 클래스를 정의할 수 있다.
  • 사용자 정의 예외를 상속하여 또 다른 사용자정의 예욀르 정의할 수도 있다.
public class ServerTimedOutException extends Exception{
	private int port;

	public ServerTimedOutException(String message, int port){
		//Exception 클래스에 예외 메시지 전달을 위해 super를 이용하여 상위 생성자 호출
		super(message);
		this.port = port;
	}
	public int getPort(){
		return port;
	}
}

[사용예]
throw new ServerTimedOutException("연결할 수 없습니다", 80);
  • 예제를 보면 catch block 안에서 또 다른 try~catch문(중첩)이 있으며 허용이 된다.
    • [결과]
      서버 접속시간 초과, 대체 서버 시도
      Error: 연결할 수 없습니다 포트에 연결 80
  • 사용자정의 예외를 처리하지 않고자 할 경우엔 아래와 같이 caller method로 throw할 수 있다. 실제로도 많이 사용된다.
  • 예를 들어서 현재 메서드에서는 이 예외를 처리하지 말아야 할 경우 예외를 처리해야 하는 메서드까지는 해당 예외를 throw해야 하는데 그때 throw 문을 사용한다.
try{
	connect("ProductServer");
}catch(ServerTimedOutException se){
	System.out.println("오류 캐치, rethrown");
	throw se;
}
public class ExceptionUserDefined{
	public void connect(String serverName) throws ServerTimedOutException{
		booelan success;
		int port = 80;
		
		success  = open(serverName, port);
		if(!success){
			throws new ServerTImedOutException("연결할 수 없습니다", port);
		}
	}
	public boolean open(String serverName, int port){
		//method body
		return false;
	}
	public void findServer(){
		try{
			connect("ProductServer");
		}catch(ServerTimedOutException se){
			System.out.println("서버 접속시간 초과, 대체 서버 시도");
			try{
				connect("AlterProductServer");
			}catch(ServerTimedOutException rse){
				System.out.println("Error: " + rse.getMessage() +
								"포트에 연결 " + rse.getPort());
			}
		}
	}

	public static void main(String[] args){
		ExceptionUserDefined e = new ExceptionUserDefined();
		e.findServer();
	}
}

catch 블럭 동작 방식

  • 첫번쨰 catch블럭부터 순서대로 찾아 내려가며, 일치하는 catch 블럭이 없으면 예외는 처리되지 않는다.
  • 예외의 최고 상위 클래스인 Exception 클래스를 처리하는 catch블럭은 모든 예외를 처리할 수 있으며 반드시 마지막 catch블럭으로 선언해서 예외처리를 해야 한다.
  • catch블럭에서 전달받은 예외 객체에서 자주 사용하는 메서드는 아래와 같다.
  • printStackTrace(): 예외 발생 시점의 호출 스택(Call stack)의 호출 흐름과 예외가 발생한 원인과 메시지를 콘솔에 출력한다.
  • getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
try{
	int[] arr = new int[3];
	arr[3] = 10/0;
}catch(ArithmeticException ae){
	ae.printStackTrace();
}catch(ArrayIndexOutOfBoundsException aie){
	aie.printStackTrace();
}catch(Exception e){
	e.printStackTrace();
	System.out.println("Exception message: "+
					e.getMessage());
}

[결과]
java.lang.ArithmeticException: / by zero
	at ch11.ExceptionStackTrace.main(ExceptionStackTrace.java:9)

예외 유형

  • 두 가지 유형의 예외가 있다.
  • Checked Exception
    • RuntimeException 및 Error를 제외하고 Throwable 클래스를 직접 상속하는 클래스를 Checked Exception이라고 한다. (예: IOException, SQLException 등)
    • 컴파일 타임에 체크되는 예외이다.
    • 예외 처리자의 존재 여부에 대한 컴파일시 검사는 적절하게 처리되지 않은 예외의 수를 줄일 수 있도록 설계되었다.
  • Unchecked Exception: RuntimeException을 상속하는 클래스를 Unchecked Exception(컴파일시 체크되지 않는 예외)라고 한다. (예: ArithmeticException, NullPointerException 등)
    • 런타임에 확인되는 예외이다.
    • Unchecked Exception 클래스는 RuntimeException 클래스와 하위클래스 및 Error 클래스와 그 하위 클래스를 뜻한다.
  • throws절에 지정된 Checked Exception은 메서드나 생성자의 사용자와 구현자 사이에 맺어진 계약의 일부이다. (사용자는 throws절에 선언된 예외를 처리해야 한다.)
public class A{
	public A() throws IOException{}
}

public class B extends A{
	public B() throws IOException{}
}

Unchecked Exception (비확인 예외)

  • Error를 검사하지 않는 이유
    • 에러는 프로그램내 여러 지점에서 발생할 수 있고 에러를 복구하는 것은 어렵거나 불가능하기 때문이다.
    • 이런 예외를 선언하기 시작하면 프로그램은 필요 이상으로 지저분해진다.
  • Runtime Exception을 검사하지 않는 이유
    • 자바 프로그래밍 언어를 설계할 때 런타임 예외를 선언하는 것이 프로그램의 정확성을 높이는 것에 별 도움이 되지 않는다고 판단했기 때문이다.
    • 자바 프로그래밍 언어의 많은 연산과 생성은 런타임 예외를 발생시킬 수 있다.
    • 프로그래머는 코드의 흐름상 런타임 예외가 발생할 리가 없다는 것을 확신할수 있다.
    • 하지만 컴파일러에게 주어진 정보와 컴파일러의 분석 수준이 불충분하기 때문에 컴파일러에게는 확신이 어려운 일이다.
    • 예를 들어 NullPointerException의 경우 프로그래머는 null 참조에 대한 예외가 발생하지 않을 거라 확신할 수 있지만, 컴파일러가 이를 증명하기는 어렵다.
  • Virtual Machine Error: JVM의 내부 에러나 자원의 한계로 인해 JVM의 처리가 어려워질 때 JVM은 VirtualMachineError의 하위클래스를 인스턴스화한 객체를 throw한다.

다중 Catch 블럭

  • 각각의 catch 블럭은 처리 내용이 동일할 수 있다. 이런 경우 1개의 catch 블럭으로 2개 이상의 예외를 처리할 수 있다면 효율적일 것이다.
  • 이럴 때 다중 Catch 블럭 기능을 활용하면 되는데 각 예외 타입 클래스는 | 기호로 구분해서 연결하면 된다. (자바 7 버전부터 지원)
public class MultiCatchException{
	public static void main(String[] args){
		try{
			int[] arr = new int[3];
			arr[3] = 10/0;
		}catch(ArithmeticException | ArrayIndexOutOfBoundsException ae){
			System.out.println("예외가 발생하였습니다 : " + ae.getMessage());
		}
	}
}

중첩 try-catch 블럭

  • try 블록 내에 try 블록을 중첩으로 사용할 수 있다.
  • 블록의 일부가 하나의 오류를 일으키고 전체 블록 자체가 또 다른 오류를 일으키는 상황이 발생할 수 있는데, 이런 경우 예외 처리가 중첩되어야 한다.
public class NestedTryBlockEx2{
	public static void main(String[] args){
		//outer (main) try block
		try{
			//inner try block 1
			try{
				//inner try block 2
				try{
					int arr[] = {1,2,3,4};
						System.out.println(arr[10]);
				}catch(ArithmeticException e){
					System.out.println("Arithmetic exception");
					System.out.println*" inner try block 2");
				}
			}catch(ArithmeticException e){
				System.out.println("Arithmetic exception");
				System.out.println("inner try block 1");
			}
		}catch(ArrayIndexOutOfBoundsException e4){
			System.out.println(e4);
			System.out.println(" outer (main) try block");
		}catch(Exception e5){
			System.out.println("Exception");
			System.out.println(" handled in main try-block");
		}
	}
}

[결과]
java.lang.ArrayIndexOutOfBoundsException: Index 10 out of
bounds for length 4 outer (main) try block
  • catch 블럭에서 예외 처리를 하는 과정에서도 예외가 발생할 수 있는데 이런 경우 catch 블럭 안에서 try-catch 블럭을 중첩해서 사용할 수 있다.
public class ResolveTryCatchEx2{
	public static void main(String[] args){
		try{
			int data1=50/0; //예외 발생
		}catch(Exception e){
			//catch 블럭에서 예외 발생
			int data2=50/0;
		}
		System.out.println("나머지 코드");
	}
}

[결과]
Exception in thread "main"
java.lang.ArithmeticException: / by zero at
ch12.resolveexception.NonResolveTryCatchEx2.main(NonResolveTryCatchEx2.java:8)
public class ResolveTryCatchEx2{
	public static void main(String[] args){
		try{
			int data1=50/0; //예외 발생
		}catch(Exception e){
			try{
				int data2=50/0; //예외 발생
			}catch(Exception innere){
				innere.printStackTrace();
				//예외 처리 코드 작성
			}
		}
		System.out.println("나머지 코드");
	}
}

[결과]
Exception in thread "main"
java.lang.ArithmeticException: / by zero at
ch12.resolveexception.ResolveTryCatchEx2.main(NonResolveTryCatchEx2.java:9)
나머지 코드

finally 블럭

  • finally블럭은 반드시 실행돼야 하는 file close, connection close와 같은 자원 반납 등의 “cleanup”코드 수행을 위해 사용된다.
  • 중요하게 실행되어야 하는 명령문을 finally 블록에 배치할 수 있다.
  • 예제는 catch block을 통해 catch가 안 되는 예외가 발생했을 때 finally 블럭을 통해 자원을 반납하거나 반드시 실행되어야하는 코드를 추가할 수 있다.
public class TestFinallyEx1{
	public static void main(String[] args){
		try{
			System.out.println("try block내 실행...");
			int data=25/0;
			System.out.println(data);
		}catch(NullPointerException e){
			System.out.println("catch block내 실행...");
			System.out.println(e);
		}finally{
			System.out.println("finally block 항상 실행");
		}
		System.out.println("나머지 코드...");
	}
}

[결과]
try block내 실행...
finally block 항상 실행
Exception in thread "main" java.lang.ArithmeticException: / by zero
						at ch12.testfinally.TestFinallyEx1.main(TestFinallyEx1.java:8)
profile
웹퍼블리셔의 백엔드 개발자 도전기

0개의 댓글