자바의 예외처리 방법
자바에서 예외 처리를 하는 방법에 대하여 다룹니다. 개념을 설명하고, 직접 코드를 작성합니다. 추가적으로 Exception과 Error의 차이, 일반 Exception과 RuntimeException의 차이, 커스텀 예외를 만드는 방법등 자바에서 예외 처리를하며 궁금할 수 있는 내용들을 담고있습니다.
학습 목표
· 예외(Exception)란 무엇인가? Exception과 Error의 차이는?
· 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
· 자바가 제공하는 예외 계층 구조, RuntimeException과 RE가 아닌 것의 차이는?
· 커스텀(사용자 정의) 예외 만드는 방법
예외(Exception)란 무엇인가? Exception과 Error의 차이는?
컴퓨터 하드웨어의 오동작 또는 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 자바에서는 에러(Error)라고 한다. 에러는 JVM 실행에 문제가 생겼다는 것이므로, 프로그램은 실행 불능이 된다. 개발자는 이런 에러에 대처할 방법이 없다.
자바에서 에러 이외에 예외(Exception)라고 부르는 오류가 있다. 예외란 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 오류다. 예외가 발생되면 프로그램이 종료된다는 점에서 에러와 동일하지만, 예외는 예외 처리(Exception Handling)를 통해 프로그램을 종료하지 않고 정상 실행 상태를 유지할 수 있다.
자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
프로그램에서 예외가 발생했을 때 갑작스러운 종료를 막고, 정상 실행을 유지할 수 있도록 처리하는 코드를 예외 처리 코드라고 한다.
자바 컴파일러는 소스 파일을 컴파일할 때 일반 예외가 발생할 가능성이 있는 코드를 발견하면, 컴파일 오류를 발생시켜 개발자에게 강제적으로 예외 처리 코드를 작성하도록 요구한다.
예를 들어 Class.forName() 메소드는 매개값으로 주어진 클래스가 존재하지 않으면 ClassNotFoundException 예외를 발생시켜 개발자로 하여금 예외 처리 코드를 작성하도록 요구한다.
그러나 실행 예외(Runtime Exception)는 컴파일러가 체크해주지 않는다. 때문에 예외 처리 코드를 개발자의 경험을 바탕으로 작성해야 한다. ArrayIndexOutOfBoundsException 등의 실행 예외가 이런 경우다.
(실행 예외와 일반 예외의 구조적 차이는 잠시 후 설명한다.)
public class TryCatchFinallyExample {
public static void main(String[] args){
try{
String data = args[0];
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("실행 매개값의 수가 부족합니다.");
}
}
}
예외 처리 코드는 try-catch-finally 블록을 이용한다. try-catch-finally 블록은 생성자 내부와 메소드 내부에서 작성되어 일반 예외와 실행 예외가 발생할 경우 예외 처리를 할 수 있도록 해준다.
try 블록에는 예외 발생 가능 코드가 위치한다. try 블록에서 예외가 발생하면 즉시 실행을 멈추고 catch 블록으로 이동하여 예외 처리 코드를 실행한다. 그리고 finally 블록 코드를 실행한다. finally 블록은 예외 여부와 상관없이 실행되며, 심지어 try 블록과 catch블록에서 return문을 사용하더라도 실행된다.
try 블록의 코드가 예외 발생 없이 정상 실행되면 catch 블록의 코드는 실행되지 않고 finally 블록의 코드를 실행한다. finally 블록은 옵션으로 생략 가능하다.
다중 catch
try 블록 내부는 다양한 종류의 예외가 발생할 수 있다. 발생되는 예외별로 예외 처리 코드를 다르게 하는 다중 catch 블록을 작성해 이를 해결할 수 있다.
public class TryCatchFinallyExample {
public static void main(String[] args){
try{
String data = args[0];
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("실행 매개값의 수가 부족합니다.");
}catch(NumberFormatException e){
System.out.println("문자열을 숫자로 변환할 수 없습니다.");
}
}
}
catch 블록이 여러 개라 할지라도 하나의 catch 블록만 실행된다. try 블록에서 동시 다발적으로 예외가 발생하지 않고, 하나의 예외가 발생하면 즉시 실행을 멈추고 해당 catch 블록으로 이동하기 때문이다.
다중 catch 블록을 작성할 때 주의할 점은 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 한다는 것이다. try 블록에서 예외가 발생하면, 예외를 처리할 catch 블록을 위에서부터 차례대로 검색된다. 만약 상위 예외 클래스의 catch 블록이 위에 있다면, 하위 예외 클래스의 catch 블록은 실행될 수 없다. 따라서 아래 코드는 잘못된 코드이다.
public class TryCatchFinallyExample {
public static void main(String[] args){
try{
String data = args[0];
}catch(Exception e){
System.out.println("예외가 발생합니다.");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("실행 매개값의 수가 부족합니다.");
}
}
}
멀티 catch
자바 7부터 하나의 catch 블록에서 여러 개의 예외를 처리할 수 있도록 멀티 catch 기능을 추가했다. 작성 방법으로 catch 괄호() 안에 동일하게 처리하고 싶은 예외를 ‘|’로 연결한다.
public class TryCatchFinallyExample {
public static void main(String[] args){
try{
String data = args[0];
}catch(ArrayIndexOutOfBoundsException | NumberFormatException e){
System.out.println("실행 매개값의 수가 부족하거나 숫자로 변환할 수 없습니다.");
}catch(Exception e){
System.out.println("알수 없는 예외 발생");
}
}
}
자동 리소스 닫기
자바에서 리소스 객체(각종 입출력 스트림, 소켓, 각종 채널)는 사용 후 close() 메소드를 호출해서 안전하게 닫아야 한다. 여기서 리소스는 데이터를 읽고 쓰는 객체다. 예를 들어 파일의 데이터를 읽는 FileInputStream 객체, 파일에 쓰는 FileOutputStream가 리소스 객체다.
리소스 객체를 안전하게 닫기 위해 자바 6 이전에는 다음과 같은 코드를 사용했다.
FileInputStream fis = null;
try{
fis = new FileInputStream("file.txt");
}catch(IOException e){
System.out.println("IOException 발생:"+e);
}finally {
if(fis != null){
try{
fis.close();
}catch(IOException e){
System.out.println("IOException 발생:"+e);
}
}
}
finally 블록에서 다시 try-catch를 사용해서 close() 메소드를 예외 처리해야 하므로 다소 복잡하다. 자바 7에 새로 추가된 try-with-resources를 사용하면 예외 발생 여부와 상관없이 안전하게 리소스 객체를 닫을 수 있다.
try(FileInputStream fis = new FileInputStream("file.txt")){
}catch(IOException e){
System.out.println("IOException 발생:"+e);
}
위 코드에서는 close()를 명시적으로 호출하지 않는다. try 블록이 정상적으로 실행이 완료했거나 도중에 예외가 발생하면, 자동으로 FileOutputStream의 close() 메소드가 호출된다. try{}에서 예외가 발생하면 close()로 리소스를 닫고 catch 블록을 실행한다.
복수 개의 리소스를 사용 한다면 다음과 같이 작성한다.
try(FileInputStream fis = new FileInputStream("file.txt");
FileOutputStream fos = new FileOutputStream("file2.txt")){
}catch(IOException e){
System.out.println("IOException 발생:"+e);
}
try-with-resources를 사용하기 위해서 리소스 객체는 java.lang.AutoCloseable 인터페이스를 구현하고 있어야 한다. AutoCloseable()에는 close() 메소드가 정의되어 있다. try-with-resources는 이 close() 메소드를 자동 호출한다.
다음은 AutoCloseable을 구현하는 FileInputStreamSC 클래스를 try-with-resource에 선언하는 예제다.
public class FileInputStreamSC implements AutoCloseable {
private String file;
public FileInputStreamSC(String file){
this.file = file;
}
@Override
public void close() throws Exception {
System.out.println("파일 닫기");
}
}
public class TryWithResourceExam {
public static void main(String[] args){
try(FileInputStreamSC fis = new FileInputStreamSC("file.txt")){
}catch (Exception e){
System.out.println("예외 처리");
}
}
}
실행 결과, 자동으로 FileInputStreamSC의 close()가 호출된다.
예외 떠넘기기
메소드 내부에 예외가 발생할 수 있는 코드를 작성할 때 try-catch 블록으로 예외를 처리하는 것이 기본이다. 하지만 경우에 따라 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다. 이때 throws 키워드를 사용한다.
throws 키워드는 메소드 선언부 끝에 작성되며, 떠넘길 예외 클래스를 쉼표로 구분해서 나열한다.
public class ThrowsExam {
public static void main(String[] args){
Map map = null;
try{
int num = convertToNum(map);
}catch (NullPointerException | NumberFormatException e){
System.out.println("예외 발생:"+e);
}
}
public static int convertToNum(Map map) throws NullPointerException, NumberFormatException{
int num = Integer.parseInt(map.get("str").toString());
return num;
}
}
main() 메소드에서도 throws 키워드를 사용해서 예외를 떠넘길 수 있다. 이때는 JVM이 예외를 처리한다. JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.
public class ThrowsExam {
public static void main(String[] args) throws NullPointerException, NumberFormatException{
Map map = null;
int num = convertToNum(map);
}
public static int convertToNum(Map map) throws NullPointerException, NumberFormatException{
int num = Integer.parseInt(map.get("str").toString());
return num;
}
}
자바가 제공하는 예외 계층 구조, RuntimeException과 RE가 아닌 것의 차이는?
예외는 두 가지 종류가 있다. 일반 예외(Exception)과 실행 예외(Runtime Exception).
일반 예외는 자바 소스를 컴파일하는 과정에서 예외 처리 코드가 필요한지 검사하기 때문에 컴파일러 체크 예외, checked Exception라고도 한다. 반대로 실행 예외는 컴파일하는 과정에서 예외 처리 코드를 검사하지 않는 예외이기 때문에 unchecked Exception이라고 한다.
두 가지 예외는 모두 예외 처리가 필요하다. 자바에서는 예외를 클래스로 관리한다. JVM은 프로그램을 실행하는 중 예외가 발생하면 해당 예외 클래스로 객체를 생성한다. 그리고 예외 처리 코드에서 예외 객체를 이용할 수 있도록 한다. 모든 예외 클래스들은 java.lang.Exception 클래스를 상속받는다.
일반 예외 클래스는 Exception을 상속받지만, RuntimeException을 상속받지 않는 클래스들이다.
실행 예외는 RuntimeException을 상속 받는 클래스들이다. RuntimeException 역시 Exception을 상속받지만, JVM은 RuntimeException을 상속했는지 여부를 보고 실행 예외를 판단한다.
실행 예외(RuntimeException)
실행 예외는 자바 컴파일러가 체크를 하지 않기 때문에 오직 개발자의 경험에 의해서 예외 처리 코드를 삽입해야 한다. 만약 개발자가 실행 예외에 대해 예외 처리 코드를 넣지 않으면, 해당 예외 발생시 프로그램은 종료된다.
이러한 실행 예외는 다음과 같은 종류가 있다.
🧨 NullPointerException
객체 참조가 없는 상태, 즉 null 값을 갖는 참조 변수로 객체 접근 연산자 도트(.)를 사용했을 때 발생한다.
public class NullPointerExceptionExample {
public static void main(String[] args){
String data = null;
System.out.println(data.toString());
}
}
위 코드의 data 변수는 null 값을 가지고 있는데 String 객체의 toString() 메소드를 호출하여 NullPointerException이 발생했다.
🧨 ArrayIndexOutOfBoundsException
배열에서 인덱스 범위를 초과하여 사용할 경우 발생한다.
public class ArrayIndexOutOfBoundsExceptionExample {
public static void main(String[] args){
String data1 = args[0];
String data2 = args[1];
System.out.println("args[0]: " + data1);
System.out.println("args[1]: " + data2);
}
}
위 예제는 두 개의 실행 매개값을 주지 않았기 때문에 args[0], args[1] 인덱스를 사용할 수 없다.
🧨 NumberFormatException
프로그램을 개발하다 보면 문자열 데이터를 숫자로 변경하는 경우가 많다. Integer, Double과 같은 Wrapper 클래스의 정적 메소드인 parseXXX() 메소드를 이용하면 문자열을 숫자로 변환할 수 있다.
이 메소드들은 매개값인 문자열이 숫자로 변환될 수 있다면 숫자를 리턴하지만, 숫자로 변환될 수 없는 문자가 포함되어 있다면, java.lang.NumberFormatException을 발생시킨다.
public class NumberFormatExceptionExample {
public static void main(String[] args){
String data1 = "100";
String data2 = "a100";
int result1 = Integer.parseInt(data1);
int result2 = Integer.parseInt(data2);
}
}
위 예제의 data1 변수의 “100” 문자열은 숫자로 변환이 가능하지만, data2 변수의 “a100” 문자열은 숫자로 변환할 수 없기 때문에 NumberFormatException이 발생한다.
🧨 ClassCastException
타입 변환(Casting)은 상위 클래스와 하위 클래스 간에 또는 구현 클래스와 인터페이스 간에 발생한다.
이러한 관계가 아니면 타입 변환을 할 수 없다. 억지로 타입 변환을 시도할 경우 ClassCastException이 발생한다. 다음은 올바른 타입 변환을 보여준다.
public class ClassCastExceptionExample {
public static void main(String[] args){
Animal animal = new Dog();
Dog dog = (Dog) animal;
RemoteControl rc = new Television();
Television tv = (Television) rc;
}
}
그러나 대입된 객체가 아닌 다른 클래스 타입으로 타입 변환을 하면 ClassCastException이 발생한다.
public class ClassCastExceptionExample {
public static void main(String[] args){
Animal animal = new Dog();
Cat cat = (Cat) animal;
RemoteControl rc = new Television();
Audio audio = (Audio) rc;
}
}
ClassCastException을 발생시키지 않으려면 타입 변환 전에 타입 변환이 가능한지 instanseof 연산자로 확인하는 것이 좋다. instanceof 연산의 결과가 true이면 좌항 객체를 우항 타입으로 변환이 가능하다는 뜻이다.
public class ClassCastExceptionExample {
public static void main(String[] args){
Animal animal = new Dog();
if(animal instanceof Dog){
Dog dog = (Dog) animal;
}else if(animal instanceof Cat){
Cat cat = (Cat) animal;
}
RemoteControl rc = new Television();
if(rc instanceof Television){
Television tx = (Television) rc;
}else if(rc instanceof Audio){
Audio audio = (Audio) rc;
}
}
}
커스텀(사용자 정의) 예외 만드는 방법
프로그램을 개발하다 보면 자바 표준 API에서 제공하는 예외 클래스만으로 처리할 수 없는 경우가 있다. 예를 들어 은행 업무 처리 프로그램에서 잔고보다 더 많은 출금 요청이 들어 온다면 예외를 발생시켜야 할 것이다.
이와 같이 애플리케이션 서비스와 관련된 예외를 애플리케이션 예외라고 하며, 개발자가 직접 정의해서 만들어야 하므로 사용자 정의 예외라고도 한다.
🌈 사용자 정의 예외 클래스는 컴파일러가 체크하는 일반 예외로 선언하거나, 컴파일러가 체크하지 않는 실행 예외로 선언할 수 있다. 일반 예외로 선언할 경우 Exception을 상속하고, 실행 예외로 선언할 경우 RuntimeException을 상속한다.
사용자 정의 예외 클래스 이름은 Exception으로 끝나는 것이 좋고, 메소드 선언을 포함할 수 있지만 대부분 생성자 선언만 포함한다. 생성자는 두 개를 선언하는 것이 일반적이다. 하나는 매개 변수가 없는 기본 생성자, 다른 하나는 예외 발생 원인(예외 메시지)을 전달하기 위해 String 타입의 매개 변수를 갖는 생성자를 생성한다.
위에서 설명한 잔고 부족 예외를 선언하면 다음과 같다. Exception을 상속하기 때문에 컴파일러에 의해 체크되는 예외가 된다. 그래서 소스 작성 시 try-catch 블록으로 예외 처리가 필요하다.
public class BalanceInsufficientException extends Exception {
public BalanceInsufficientException(){}
public BalanceInsufficientException(String message){
super(message);
}
}
🌈 사용자 정의 예외 또는 자바 표준 예외를 소스 코드에서 발생시키는 방법을 알아보자. 다음과 같이 예외를 발생시키려면, 기본 생성자 또는 예외 메시지를 갖는 생성자를 사용한다. 만약 catch 블록에서 예외 메시지가 필요하다면 예외 메시지를 갖는 생성자를 이용해야 한다.
public class Main {
public static void main(String[] args){
int balance = 100_000;
int money = 100_000_000;
try{
withdraw(balance,money);
}catch (BalanceInsufficientException e){
System.out.println(e);
}
}
public static void withdraw(int balance, int money) throws BalanceInsufficientException {
if(balance<money){
throw new BalanceInsufficientException("잔고부족");
}
}
}
(+추가) 예외 정보 얻기
try 블록에서 예외가 발생되면 예외 객체는 catch 블록의 매개 변수에서 참조된다. 그러므로 매개 변수를 이용하면 예외 객체의 정보를 알 수 있다. 이때 모든 예외 객체는 Exception 클래스를 상속하기 때문에 Exception이 가지고 있는 메소드들을 모든 예외 객체에서 호출할 수 있다.
자주 사용되는 메소드로 getMessage(), printStackTrace()가 있다. getMessage()는 예외가 발생하면, String 타입의 메시지를 갖는 생성자를 이용했을 때 저장되는 메시지를 리턴한다. printStackTrace()는 예외 발생 코드를 추적해서 모두 콘솔에 축력한다.
위에서 작성한 사용자 정의 예외(BalanceInsufiicientException)의 정보를 얻는 코드를 작성해보자.
public class Main {
public static void main(String[] args){
int balance = 100_000;
int money = 100_000_000;
try{
withdraw(balance,money);
}catch (BalanceInsufficientException e){
String msg = e.getMessage();
System.out.println(msg);
System.out.println();
e.printStackTrace();
}
}
public static void withdraw(int balance, int money) throws BalanceInsufficientException {
if(balance<money){
throw new BalanceInsufficientException("잔고부족");
}
}
}
'자바' 카테고리의 다른 글
[자바] Annotations, 자바에서 메타 데이터를 제공하는 방법 (0) | 2021.08.29 |
---|---|
[Java] 자바에서 문자열을 다루는 방법 - String (0) | 2021.08.28 |
[Java] Nested Class, 클래스 안의 클래스 (0) | 2021.08.27 |
[Java] JVM 메모리 사용 영역 (0) | 2021.08.20 |
[Java] 자바의 연산자 (0) | 2021.08.20 |
댓글