본문 바로가기

프로그래밍언어/JAVA

[JAVA] 예외처리 (Exception handling)

반응형

1. 프로그램 오류

 

프로그램 실행 중에 어떠한 원인에 의해 프로그램이 오작동 하거나 비정상적으로 종료되는 경우가 있는데, 이러한 경우를 프로그램 에러 또는 오류라고 한다. 에러는 발생 시점에 따라 '컴파일 에러'와 '런타임 에러'로 나눌 수 있다. 이외에도 컴파일 에러나 런타임 에러와는 조금 달리 정상적으로 실행되지만 의도와 다른 동작을 보이는 '논리적 에러'도 있다. 

 

컴파일 에러의 경우 프로그램이 실행되기 전에 컴파일러를 통해서 오류를 잡을 수 있지만, 런타임 에러의 경우는 실행되기 전에는 알 수 없다. 이러한 런타임 에러를 방지하기 위해서는 프로그램의 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 대비를 해야한다. 

 

자바에서는 런타임에 발생할 수 있는 프로그램 오류를 에러(error)와 예외(exception), 두가지로 구분한다. 에러는 메모리 부족이나 스택오버플로우와 같이 복구할 수 없는 오류이고, 예외는 발생하더라도 수습하여 복구할 수 있는 오류이다. 에러는 발생하면 프로그램의 비정상적 종료를 막을 수 없지만 예외는 이에대한 코드를 미리 작성하여 대비할 수 있다. 

 

 

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

 

자바에서는 오류를 처리하기 위한 Error 클래스와 Exception 클래스가 존재한다. 

 

JAVA Exception 클래스 구조

 

 

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

 

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

 

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

 

자바에서는 예외를 처리하기 위해 try-catch 문을 사용한다. 

 

try { 

// 예외가 발생할 수 있는 코드가 위치한다. 

... 

} catch (Exception1 e1) { 

// Exception1 이 발생하는 경우 이를 처리하는 코드가 위치한다. 

} catch (Exception2 e2) { 

// Exception2 이 발생하는 경우 이를 처리하는 코드가 위치한다. 

}

 

하나의 try 문에는 해당 try 문 안에서 예외가 발생했을 때 이를 대비하기 위한 여러개의 catch 블럭이 올 수 있다. 예외가 발생하면 이 중 발생한 예외의 종류와 일치하는 catch 문이 수행되고, 일치하는 catch 블럭이 없다면 예외는 처리되지 않는다. 

 

 

4. try-catch 문의 흐름 

 

try-catch 문은 예외의 발생 여부에 따라 실행 흐름이 달라진다.

 

예외가 발생한 경우 
1. 발생한 예외와 일치하는 catch 블럭을 찾는다. 
2. 일치하는 catch 블럭이 존재하면 해당 블럭의 코드를 수행한 후 try-catch 문을 빠져나와 다음 코드로 넘어간다. 
3. 만약 예외가 일치하는 catch 블럭이 없는 경우 예외는 처리되지 못한다. 
 
예외가 발생하지 않은 경우 
1. catch 블럭을 수행하지 않고 전체 try-catch 문을 빠져나와 수행을 계속한다. 

 

 

5. catch 블럭 

 

catch 블럭은 소괄호 영역과 중괄호 영역으로 나눠져 있는데, 소괄호 안에는 처리하고자 하는 예외와 같은 타입의 참조변수가 선언되고 중괄호 영역에는 해당 예외 발생 시에 수행할 코드를 포함하고 있다.

 

예외가 발생하면 해당 예외 타입의 인스턴스가 생성된다. 예외가 발생한 문장이 try 문에 포함되어 있다면, 이 예외를 처리하는 catch 문을 찾는다. 첫번째 catch 문부터 순서대로 내려가면서 instaceof  연산자를 통해 catch 문에 선언된 예외 타입 변수를 확인하여 true 가 되는 블럭을 찾을 때까지 검사를 계속한다. 만약에 일치하는 블럭을 만나면 해당 블럭을 수행하고 그렇지 못하면 예외는 처리되지 못한다.

 

모든 예외 클래스는 Exception 클래스의 자손 클래스이기 때문에 catch 블럭에 Exception 타입의 변수를 선언해놓으면 모든 예외에 대해 해당 블럭이 수행된다.

 

- printStackTrace() 와 getMessage()

 

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage() 와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다. catch 블럭의 괄호에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있다. 이 참조변수는 catch 블럭 내에서만 사용가능하며 자주 사용되는 메서드는 다음과 같다.

 

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

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

 

class ExceptionTest { 
  public static void main(String argsp[]) { 
    try { 
    ... 
    } catch (Exception e) { 
    	// 참조변수 e를 통해 생성된 Exception 인스턴스에 접근하고 해당 예외 인스턴스에 대한 예외 메시지를 출력한다. 
    	e.printStackTrace();
    } 
  } 
}

 

- 멀티 catch 블럭

 

JDK1.7 부터 여러 catch 블럭을 '|' 기호를 사용하여 하나의 catch 블럭으로 합칠 수 있게 되었다. 이를 멀티 catch 블럭 이라고 한다.

 

try { ... } 
catch (ExceptionA e) { ... } 
catch (ExceptionB e) { ... }

// 위와 같은 try-catch 문을 아래와 같이 변경할 수 있다. 
try { ... }
catch (ExceptionA | ExceptionB e) { ... }

 

이때 ExceptionA 와 ExceptionB 가 상속 관계에 있다면 에러가 발생한다. 그 이유는 두 클래스 간의 관계로 인해 사실상 조상 클래스를 사용하여 예외를 처리하는 것과 같기 때문이다.

 

그리고 멀티 catch 블럭에서는 선언된 여러 예외 클래스 중 어떤 클래스의 인스턴스가 생성되었는지 알 수 없기 때문에 해당 클래스들의 공통된 조상 클래스의 메서드만 사용할 수 있다. 물론 instanceof 메서드를 사용하여 이를 구분할 수 있지만, 이러한 경우에는 멀티 catch 블럭을 사용하는 의미가 없어지기 때문에 catch 블럭 자체를 분리하여 사용하는 편이 더 편리하다.

 

 

6. 예외 발생시키기

 

자바에서는 throw 키워드를 사용하여 사용자가 임의로 예외를 발생시킬 수 있다.

 

try { 
  // 예외 클래스의 참조변수를 선언하여 throw 로 예외 발생 
  Exception e = new Exception("exception"); 
  throw e; 

  // throw 문에서 바로 Exception 객체 생성하여 예외 발생 
  throw new Exception(); 
}

 

 

 

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

 

예외를 처리하는 방법에는 try-catch 문 외에 직접 예외를 메서드에 선언하는 방법이 있다. 메서드의 선언부에서 throws 키워드를 사용하여 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 예외가 여러개인 경우에는 쉼표를 사용하여 구분한다.

 

// throws 키워드 뒤에 예외들이 붙는다. 
public void method() throws Exception1, Exception2, ... , ExceptionN { 
	...
}

 

메서드의 선언부에 미리 예외를 선언함으로써 해당 메서드를 사용할 때 처리해야할 예외들을 명시적으로 확인할 수 있다. 자바는 이러한 방식을 통해 메서드를 사용하는 쪽에서 발생 가능성이 있는 예외를 처리하도록 강요하고 있다.

 

해당 메서드를 사용하는 메서드에서 예외를 처리하지 않으면 계속해서 호출 스택에 있는 메서드들을 따라 전달되다가 마지막 main 메서드에서도 예외가 처리되지 않으면 프로그램이 종료가 된다. 그렇기 때문에 throws 로 예외가 선언되어 있는 메서드를 사용할 때는 적절한 위치에서 예외처리를 해주어야 한다.

 

 

8. finally 블럭

 

finally 블럭은 try-catch 문과 함께 사용되어서 예외의 발생 여부에 상관없이 필수적으로 실행되어야 할 코드를 포함하는 블럭이다. try-catch-finally 의 순서로 붙여서 사용한다.

 

try {
	... 
} catch (Exception e) { 
	... 
} finally { 
  // 예외의 발생 여부와 상관없이 항상 수행되는 코드가 포함된다. 
  // try-catch 문의 마지막에 위치한다. 
}

 

 

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

 

JDK1.7 부터는 try-with-resources 문이라고 하는 구문이 새로 추가되었다. 해당 구문은 try-catch 문에서 입출력과 관련된 클래스를 다룰때 유용한데, 입출력에 사용한 자원에 대해 사용이 끝나면 close() 메소드의 호출 없이도 자동으로 자원을 반환한다.

 

// try 블럭의 괄호 안에 여러 문장이 들어가는 경우 ';'를 사용하여 이를 구분해준다. 
try (fis = new FileInputStream("input.dat");
  dis = new DataInputStream(fis)) { 
  	... 
  } catch (Exception e) { 
  	... 
}

 

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

 

 

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

 

기존에 정의된 예외 클래스 외에 개발자가 직접 예외 클래스를 작성하여 사용할 수 있다. 보통 Exception 클래스를 상속 받아서 예외 클래스를 작성한다.

 

class CustomException extends Exception { 
  private final int ERR_CODE;
  CustomException(String msg) { 
    this(msg, 100);
  } 

  CustomException(String msg, int errCode) { 
    super(msg); // 조상 클래스인 Exception의 생성자를 호출한다. 
    ERR_CODE = errCode; 
  }

  public int getErrCode() { 
  	return ERR_CODE; 
  } 
}

 

 

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

 

예외가 발생하면 try-catch 문 안에서 직접 예외를 처리할 수도 있고, 선언부에서 예외 클래스를 선언하여 외부에서 처리를 하도록 할 수도 있다. 

또 다른 방법으로 한 예외에 대해서 내부와 외부 양쪽 모두에서 처리하도록 할 수도 있다. try-catch 문 내부에서 예외를 catch 한 곳에서 다시 예외를 발생시켜서 외부에서도 해당 예외를 처리하도록 하는 방법을 사용한다.

 

try { 
  method(); 
} catch (Exception e) { 
  // catch 블럭 안에서 예외를 다시 발생시켜서 메서드 외부에서 다시 예외를 처리하도록 한다.
  throw e;
}

 

이와 같은 방식은 양쪽 메서드 모두에서 처리해줘야 할 작업이 있을 때 사용한다. 이때 주의해야할 점은 try-catch 문 외에 throws 로 해당 예외를 미리 선언해주어야 한다는 것이다.

 

 

12. chained exception

 

한 예외가 다른 예외를 발생시킬 수 있다. 한 예외가 발생했을 때 initCause() 메서드를 사용하여 해당 예외를 원인 예외로 등록한 후 새로운 예외를 발생시킬 수 있다. 이렇게 구현하는 이유는 여러 예외를 원인 예외로 등록하여 후에 하나의 예외로 묶어서 처리하기 위함이다.

 

try {
	...
} catch (ExceptionA ea) { 
	// ExceptionB 예외를 생성하고 원인 예외로 ea를 등록한다. 
	ExceptionB eb = new ExceptionB();
	eb.initCause(ea);
	
	// ExceptionB 예외를 발생시킨다. 
	throws eb;
}

 

만약 여러 예외를 공통적으로 같은 로직에서 수행하기 위해서는 예외들을 상속 관계로 묶어줘야 한다. 하지만 상속 관계로 묶어서 조상 클래스 처리해버리면 정확히 어떤 예외가 발생했는지 알 수 없을 뿐 아니라 기존에 상속 관계가 아닌 예외들에 대해서 처리가 어려워진다. 이러한 이유 때문에 위와 같이 원인 예외로 등록하여 예외를 처리한다. 

 

 

반응형

'프로그래밍언어 > JAVA' 카테고리의 다른 글

[JAVA] java.util 패키지  (0) 2021.12.19
[JAVA] java.lang  (0) 2021.12.15
[JAVA] 내부 클래스 (Inner class)  (0) 2021.11.29
[JAVA] 인터페이스 (Interface)  (1) 2021.11.23
[JAVA] 추상클래스 (abstract class)  (0) 2021.11.20