[Java] 예외처리

황세호·2021년 5월 24일
0

Java

목록 보기
5/6
post-thumbnail

예외처리


프로그램 오류

  • 컴파일 에러 : 컴파일 시에 발생하는 에러

  • 런타임 에러 : 프로그램을 실행 시에 발생하는 에러

  • 논리적 에러 : 실행은 되나 의도와 다르게 프로그램이 작동하는 에러

  • 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류

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

예외 클래스의 계층 구조

자바에서는 실행 시 발생할 수 있는 오류 (Exception, Error)를 클래스로 정의하였다. Exception과 Error클래스 역시 Object클래스의 자손들이다.

  • RuntimeException은 보통 프로그래머의 실수에 의해서 발생할 수 있는 예외들이다.
    - 배열의 범위 초과
    - 값이 null인 참조변수의 멤버를 호출
    - 클래스의 형변환 오류
    - 0으로 나눗셈 구현

  • Exception은 주로 외부의 영향으로 발생할 수 있는 예외들이다.
    - 존재하지 않는 파일의 이름을 입력
    - 클래스의 이름을 잘못 입력
    - 입력 데이터 형식이 잘못된 경우

예외처리하기 - try-catch문

예외처리 (exception handling)란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이다.

  • 정의 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
  • 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
try{
	// 예외가 발생할 가능성이 있는 구간.
} catch(Exception e1) {
	// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception e2) {
	// Exception2가 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}

예외의 발생과 catch블럭

예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다. (ArithmeticException이 발생할 경우, ArithmeticException 인스턴스가 생성) 첫번째 catch블럭부터 차례로 내려가면서 catch블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사하게 되는데, 검사결과가 true인 catch블럭을 만날 때까지 검사는 계속된다.

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

printStackTrace()와 getMessage()

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

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

멀티 catch블럭

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

/* 기존 catch문 */
try {
	...
} catch (ExceptionA e) {
	e.printStackTrace();
} catch (ExceptionB e) {
	e.printStackTrace();
}

/* 멀티catch 적용 후 */
try {
	...
} catch (ExceptionA | ExceptionB e) {  // 만약 ExceptionA와 ExceptionB가 조상과 자손의 관계에 있다면 컴파일 에러 발생!
	e.printStackTrace();
}
  • 멀티 catch로 이어진 예외 클래스가 조상과 자손의 관계라면, 자손 클래스 하나를 예외처리 해주는 것과 동일하므로, 불필요한 코드는 제거하라는 의미에서 에러가 발생하는 것이다.
  • 멀티 catch는 하나의 catch블럭으로 예외를 처리하는 것이기 때문에, 실제로 어떤 예외가 발생한 것인지 알 수 없다.
try {
	...
} catch (Exception A | Exception B e) {
	e.methodA();  // 에러. ExceptionA에 선언된 methodA()는 호출불가

	if (e instanceof ExceptionA) {
		ExceptionA e1 = (ExeptionA)e; // OK. exceptionA에 선언된 메소드 호출가능
		e1.methodA();
	} else {  // if(e instanceof ExceptionB)
		...
	}
}

위와 같이 instanceof로 어떤 예외 클래스인지 확인 후 처리할 수 있지만 이렇게까지 하면서 catch블럭을 합칠 일은 거의 없을 것이다.

여러 catch블럭을 멀티 catch블럭으로 합치는 경우는 대부분 코드를 간단히 하는 정도의 수준일 것이므로 이러한 제약에 대해 너무 고민하지 않기 바란다.

예외 발생시키기

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

  1. 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음

  2. 키워드 throw를 이용해서 예외를 발생시킨다.

try {
	throw new Exception ("고의로 발생시킴");  // 에러 메시지 넣어줌
} catch (Exception e) {
	System.out.println(e.getMessage());  // 에러 메시지 불러올 수 있음
	e.printStackTrace();
}

메서드에 예외 선언하기

try-catch문을 사용하는 것 외에, 예외를 메서드에 선언하는 방법이 있다.(일반적으로 RuntimeException클래스들은 적지 않는다.)

void method() throws Exception {
	// 메서드의 내용
}

자바에서는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서드를 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.

어느 경우에는, 두 메서드가 예외처리를 분담할 수도 있다.

class ExceptionEx {
	public static void main(String[] args) {
		try {
			File f = createFile(args[0]);
			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(""))
			throws new Exception("파일이름이 유효하지 않습니다.");
		File f = new File(fileName);
		f.createNewFile(f);
		return f;
	}

	static void createNewFile(File e) {
		try {
			f.createNewFile();
		} catch (Exception e) {

		}
	}
}

finally 블럭

class Test{
	public static void main(String args[]){
    	Test.method1();
        System.out.println("method1()의 수행을 마치고 main 메서드로 돌아왔습니다.");
    }
    
    static void method1(){
    	try{
            	System.out.println("method1()이 호출되었습니다.");
            	return;
        } catch(Exeption e){
            	e.printStackTrace();
        } finally {
            	System.out.println("method1()의 finally 블록이 실행되었습니다.");
        }
    }
}
위의 코드를 보면 알다시피, method1()의 try 문에서 return문으로 메서드를 종료시키려고 해도, finally 블록은 실행되는 것을 알 수 있다.

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

JDK 1.7부터 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("점수의 총합은");
} catch (IOException ie) {
	ie.printStackTrace();
}

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

이처럼 try-with-resources문에 의해 자동으로 close()가 호출될 수 있으려면, 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

AutoCloseable을 구현하는 클래스 만들기

내가 만든 클래스가 try-with-resources으로 자원이 해제되길 원한다면 AutoCloseable을 Implements해야 한다. 아래 코드에서 CustomResource 클래스는 AutoCloseable을 구현하였다. main에서는 이 객체를 try-with-resources로 사용하고 있다.

public static void main(String args[]) {
	try(CustomResource cr = new CustomResource()) {
    		cr.doSomething();
    	} catch (Exception e) {
        }
}

private static class CustomResource implements AutoCloseable {
	public void doSomething() {
    		System.out.println("Do something...");
    	}
        
        @Override
        public void close() throws Exception {
        	System.out.println("CustomResource.close() is called);
        }
}

위 코드에서는 close()가 호출될 때 로그를 출력하기 때문에 close()가 실제로 호출되어, CustomResource객체가 반환되는 것을 확인할 수 있다.

연결된 예외 (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) {
	...
}

여기서 initCause()는 Exception 클래스의 조상인 Throwable클래스에 정의되어 있기 때문에 모든 예외에서 사용가능하다.

  • Throwable initCause (Throwable cause) : 지정한 예외를 원인 예외로 등록
  • Throwable getCause() : 원인 예외를 반환

발생한 예외를 그냥 처리하면 될 텐데, 굳이 원인 예외로 등록해서 다시 예외를 발생시키는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

그렇다고 해서 InstallException을 SpaceException과 MemoryException의 조상으로 해서 catch블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 생긴다.

Checked Exception vs Unchecked Exception

Checked Exception

  • 확인 시점 : 컴파일 시점
  • 처리 여부 : 반드시 예외 처리 해야 한다.
  • 트랜잭션 처리 : 예외 발생시 롤백하지 않음
  • 종류 : IOException, ClassNotFoundException 등

Unchecked Exception

  • 확인 시점 : 런타임 시점
  • 처리 여부 : 명시적으로 하지 않아도 된다.
  • 트랜잭션 처리 : 예외 발생시 롤백해야 함
  • 종류 : NullPointerException, ClassCastException 등
profile
Developer

0개의 댓글