JAVA 예외 처리

Dev.Shinny·2022년 9월 28일
0

자바의 정석

목록 보기
12/18

프로그램 오류

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

컴파일 에러 : 컴파일 시에 발생하는 에러
런타임 에러 : 실행시에 발생하는 에러
논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

이 중 런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)' 두 가지로 구분한다.

에러(error)    : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception)  : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 막을 수 있다.

예외(Exception) 예시

  • 0으로 나누기
  • 배열의 인덱스 초과
  • 없는 파일 열기
  • 기타 등등...

예외 클래스의 계층 구조

자바에서는 실행 시 발생할 수 있는 오류(exception과 error)를 클래스로 정의하였다.

모든 예외의 최고 조상은 Exception이다. 위 그림에서 볼 수 있듯이 예외 클래스들은 다음과 같이 두 그룹으로 나눠질 수 있다.

1) RuntimeException 클래스와 그 자손들
: 프로그래머의 실수로 발생하는 예외
2) 1)을 제외한 Exception 클래스와 그 자손들
: 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외

예외 처리하기(try-catch문)

예외처리(Exception Handling)

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

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

// 괄호 생략 절대 불가.
try {
	// 예외 발생할 가능성 있는 문장
} catch ( Exception1 e1) // 처리하고자 하는 예외와 같은 타입의 참조변수 {
	// Exceptiona1이 발생했을 경우, 처리하기 위한 문장
} catch ( Exception2 e2) {
	// Exceptiona2이 발생했을 경우, 처리하기 위한 문장
} catch ( ExceptionN eN) {
	// ExceptionaN이 발생했을 경우, 처리하기 위한 문장
} 

발생한 예외의 종류와 일치하는 catch 블럭이 없으면 예외는 처리되지 않는다. catch블럭의 괄호 내에 선언된 변수는 catch 블럭 내에서만 유효하기 때문에, 모든 catch 블럭에 참조변수를 하나만 사용해도 된다.

그러나 catch 블럭 내에 또 하나의 try-catch문이 포함된 경우, 구별할 수 없기 때문에 같은 이름의 참조변수를 사용할 수 없다.

try-catch문 흐름

try-catch문에서, 예외가 발생한 경우와 발생하지 않았을 때의 흐름이 달라진다.

try 블럭 내에서 예외가 발생한 경우
1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
2. 일치하는 catch 블럭을 찾게 되면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속 수행한다.

try 블럭 내에서 예외가 발생하지 않은 경우
1. catch 블럭을 거치지 않고 전체 try-catch문을 빠져나가서 그대로 수행한다.

class ExceptionEx1{
	public 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); // 실행 x 
        } catch (ArithmeticException ae){
        System.out.println(5);
        } // try-catch문 끝
        System.out.println(6);
    }
}

printStackcTrace()와 getMessage()

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

class ExceptionEx2{
	public 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); // 실행 x 
        } catch (ArithmeticException ae){
     		ae.printStackTrace();
            System.out.println("예외 메시지 : " + ae.getMessage());
        } // try-catch문 끝
        System.out.println(6);
    }
}

멀티 catch 블럭

JDK1.7부터 여러 catch 블럭을 '|'기호를 이용해, 하나의 블럭으로 합칠 수 있게 되었고, 이를 '멀티 catch블럭'이라고 한다. '|'기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다.

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

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

(1)과 (2)는 서로 같다.

만일 멀티 catch 블럭의 '|'기호로 연결된 예외 클래스가 상속관계에 있다면 컴파일 에러가 발생한다. 왜냐면 두 예외 클래스가 부모, 자식 관계에 있다면 부모 클래스만 써주는 것과 똑같기 때문이다. 불필요한 코드는 제거하라는 의미에서 에러가 발생한다.

멀티 catch 블럭 내에서는 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조변수 e로 멀티 catch 블럭에 '|'기호로 연결된 예외 클래스들의 공통 분모인 부모 예외 클래스에 선언된 멤버만 사용할 수 있다.

예외 발생시키기(throw)

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

  1. 연산자 new를 이용해 발생시키려는 예외 클래스의 객체를 만든다.
    Exception e = new Exception ("고의 발생");
  2. 키워드 throw를 이용해서 예외를 발생시킨다.
    throw e;
class ExceptionEx3{
	public static void main(String [] args){
    	try{
			(1)
			Exception e = new Exception("고의 발생");
			throw e;
			(2)
   			throw new Exception("고의 발생"); // (1)과 같은 코드임 
            
		} catch (Exception e) {
			System.out.println("에러 메시지 : "+e.getMessage());
  		  	e.printStackTrace();
		} 
		System.out.println("프로그램 정상 종료");
    }
}

checked 예외, unchecked 예외

chedcked 예외 : 컴파일러가 예외 처리 여부를 체크(예외 처리 필수)
unchecked 예외 : 컴파일러가 예외 처리 여부를 체크 안 함(예외 처리 선택)

(1) checked 예외 _ 컴파일 안됨.
class ExceptionEx4_1{
	public static void main(String [] args){
    	throw new Exception();
    }
}
(2) unchecked 예외 _ 컴파일 가능 but 비정상적 종료
class ExceptionEx4_2{
	public static void main(String [] args){
    	throw new RuntimeException();
    }
}

예외 전가시키기(throws)

예외를 처리하는 방법은 try-catch문 외에 throws 키워드를 사용해 예외를 메서드에서 선언하는 방법이 있다.

(1)
void method() throws Exception1, Exception2, ... ExceptionN {
	메서드 내용 ...
}
(2)
// method()에서 Exception과 그 자손 예외 발생 가능 
void method() throws Exception {
	...
}

(2)는 예외의 최고 조상인 Exception클래스를 서언했다. 즉 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻이다. 오버라이딩할 때는 단순히 선언된 예외의 개수가 아니라 상속관계까지 고려해야 한다.

메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 떄, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다.

기존의 많은 언어들은 메서드에 예외선언을 어떤 상황에 어떤 종류의 예외가 발생할 가능성이 있는지 예측하기 힘들었다. 그러나 자바는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서드를 사요하는 쪽에서 이에 대한 처ㅣ를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어주고 견고한 프로그램 코드를 작성할 수 있도록 도와준다.

throws에는 ckecked 예외(RuntimeException 라인 제외)만 적는 것이 정석이다. 이는 Java API문서를 통해 사용하고자 하는 메서드의 선언부와 'Throws:'를 보고 확인할 수 있다.

예외를 메서드 throw에 명시하는 것은 예외를 처리하기 위함이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

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

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

(1)은 method1()에서 예외 처리를 했고, (2)는 method1()에서 예외를 선언하여 자신을 호출하는 메서드(main 메서드)에 예외를 전달했으며, 호출한 메서드에서는 try-catch문으로 예외처리를 했다. (1)처럼 예외가 발생한 메서드 내에서 처리되면, 호출한 메서드는 예외가 발생했다는 사실조차 모른다.

finally 블럭

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

try{
	...
}
catch ( Exception e) {
	...
} finally{
	// 예외의 발생 여부랑 관계 없이 항상 실행돼야 하는 문장
}

예외가 발생한 경우에는 'try→catch→finally' 순으로 실행되고, 예외가 발생하지 않은 경우에는 'try→finally' 순으로 실행된다. catch 블럭 수행 중에 return문을 만나도 finally 문장들은 수행된다.

(1) finally 미사용

class FinallyTest{
	public static  void main(String[] args){
    	try{
        	startInstall();
        	copyFiles();
            deleteTempFiles();
        } catch (Exception e) {
        	e.printStackTrace();
            deleteTempFiles();
        }
    }
    static void startInstall(){ 프로그램 설치에 필요한 코드 ...}
    static void copyFileds(){ 파일 복사하는 코드 ...}
	static void deleteTempFiles(){ 임시 파일들 삭제하는 코드 ...}
}

(2) finally 사용 _ 코드 중복 삭제

class FinallyTest{
	public static  void main(String[] args){
    	try{
        	startInstall();
        	copyFiles();
        } catch (Exception e) {
        	e.printStackTrace();
        } finally{
        	 deleteTempFiles(); 
        }
    }
    static void startInstall(){}
    static void copyFileds(){}
	static void deleteTempFiles(){}
}

자동 자원 반환 (resources)

JDK1.7부터 'try-with-resources'문이 새로 추가되었다. 주로 입출력에 사용되는 클래스 중에서는 사용한 후 꼭 닫아 줘야 하는 것들이 있다. 그래야 사용했던 자원이 반환되기 때문이다.

try{
	fis = new FileInputStream("score.dat");
    dis = new DataInputStream(fis);
} catch (IOException ie){
	ie.printStackTrace();
} finally{
	// 작업 중에 예외가 발생하더라도, dis가 닫히도록 finally 블럭에 삽입.
	try{
    	if(dis!=null)
        	dis.close();
    } catch(IOException ie){
    	ie.printStackTrace();
    }
}

finally 블럭 안에 try-catch문을 추가해서 close()를 실행시키고 추가적으로 발생할 수 있는 예외를 처리하도록 했다. 그러나 이렇게 코드를 짜면, 코드가 복잡해지고 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()가 호출된다. 이처럼 작동하려면 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자정의 예외

프로그래머가 직접 새로운 예외 클래스를 정의할 수 있다. 부모는 Exception 또는 RuntimeException 중에서 선택해 상속받아 만들면 된다. 그러나 가능하면 새로운 예외 클래스를 만들기보다 기존의 예외클래스를 활용하는 편이 좋다.

예외 되던지기

연결된 예외

profile
Hello I'm Shinny. A developer who try to enjoy the challenge.

0개의 댓글