예외처리

오늘·2021년 3월 22일
0

Java

목록 보기
23/42

에러
: 하드웨어의 잘못된 동작 또는 고장으로 인한 오류
: 발생시 프로그램 종료
: 정상상태로 돌아갈 수 없음

예외
: 예측할 수 있는 오류
: 사용자의 잘못된 조작 혹은 개발자의 잘못된 코딩으로 인한 오류
: 예외 발생시 프로그램 종료
-> 예외 처리(Exception Handling)을 통해
프로그램이 종료되지 않고 실행 상태가 유지되도록 컨트롤 가능

예외의 종류
: 일반(컴파일 체크) 예외 -> 예외 처리 코드 없으면 컴파일 오류 발생
: 실행 예외 -> 예외 처리 코드를 생략하더라도 컴파일은 되는 예외 -> 오류표시는 없는데 돌리면 예외가 뿅


실행 예외

1. NullPointerException

-자바 프로그램에서 가장 빈번하게 발생하는 실행 예외입니다.
-객체 참조가 없는 상태 즉, null 값을 갖는 참조 변수로 객체 접근 연산자인 도트(.)를 사용했을 때 발생합니다.
-객체가 없는 상태에서 객체를 사용하려 했으니 예외가 발생한느 것이다

예시)

String data1 = "김자바";
System.out.println(data1.toString());

String data2 = null;
System.out.println(data2.toString());

이 경우 data1의 값은 제대로 출력되지만 data2의 값은 null, 즉 비워져있는 상황이다. 값을 꺼내가고자 하는데 꺼낼 값이 없으니 오류가 나는 것이다.

ArrayIndexOutOfBoundsException

-배열에서 인덱스 범위를 초과하거나 입력이 없으면 발생하는 실행 예외입니다
-예를 들어 길이가 3인 배열을 선언한경우 arr[0][1][2] 는 사용이 가능하지만 arr[3]의 경우에는 범위가 초과되어 오류가 발생되는 것

String data1 = args[0];
String data2 = args[1];

system.out.println(data1);
system.out.println(data2);

이 예제는 실행 매개값을 주지 않았기 때문에 args[0][1]과 같이 인덱스를 사용할 수 없는 것이다.

-> 오류를 줄이기 위해 배열을 사용할 때에는 배열값을 읽기 전에 배열의 길이를 먼저 조사하는 것이 좋다. 실행 매개값이 없거나 부족할 경우 조건문을 이용해 사용자에게 실행 방법을 알려주는 것이다.

NumberFormatException (문자열을 숫자로 변환하는 방법)

-프로그램을 개발하다보면 문자열로 되어 있는 데이터를 숫자로 변경하는 경우가 자주 발생한다.
-문자열을 숫자로 변환하는, 가장 많이 사용되는 코드는 다음과 같다

-parseXXX() 메소드를 이용해 문자열을 숫자로 변환할 수 있는 것이다.
이 메소드는 매개값인 문자열이 숫자로 변활될 수 있다면 숫자를 리턴하지만, 숫자로 변환될 수 없는 문자가 포함되어있다면 오류를 발생시킨다.

String data1 = "100";
String data2 = "a200";

int value1 = Integer.parseInt(data1);
int value2 = Integer.parseInt(data2);

이 경우 data1의 변수는 숫자로 잘 변환되지만 data2의 경우는 아니다. a가 16진수로 변환되어 출력되지도 않고, 그렇게 되어도 원하는 숫자값이 아닐것이다.

ClassCastException

타입 변환(Casting)은 상위-하위 클래스 관계, 구현클래스-인터페이스 간에 발생한다. 이 경우가 아니라면 클래스는 다른 클래스로 타입 변환할 수 없다. 억지로 타입 변환을 시도하면 발생하는 오류가 ClassCastException이다.


예외처리 코드

  • 프로그램에서 예외가 발생했을 경우 프로그램의 갑작스러운 종료를 막고, 정상 실행을 유지할 수 있도록 처리하는 코드를 말한다.
  • 예외 처리 코드는 try-catch-finally 블록을 이용한다.
  • try 블록에는 예외 발생이 발생할 수도 있는 코드가 위치한다.
    -> 예외가 없다면 catch를 건너뛰고 finally 블록의 코드를 실행한다
    -> 에외가 있다면 catch 블록을 실행 후 finally 블록 실행한다

1. NullPointerException을 예외처리 한 모습

public class NullPointEx {
	public static void main(String[] args) {
		// NullPointException 이 발생하는 경우
		// data에 들어있는 값이 없어 오류남
//		String data = null;
//		System.out.println(data.toString());
		
		
		// 예외처리 하려면 try-catch
		String data1 = "입력";
		String data2 = null;
		
		try {
			// data1은 값이 있으니 try 처리 되다가
			System.out.println(data1.toString());
			System.out.println("데이터가 정상 입력 되었습니다");
			System.out.println("----------------------");

			// data2의 값은 존재하지 않으니 바로 catch로 넘어가는 모습 확인
			System.out.println(data2.toString());
			System.out.println("데이터가 정상 입력 되었습니다");
			System.out.println("----------------------");
		
		// 실행하다가 catch의 요구사항 (NullPointerException) 발생시
		// e변수에 담아 처리한다
		} catch (NullPointerException e) {
			// 변수에 어떤 내용이 들어있는지 출력 = getMessage()
			System.out.println(e.getMessage());
			System.out.println("데이터 값이 존재하지 않습니다");
			System.out.println("----------------------");
		
		} finally {		// 예외처리와 상관없이 무조건 처리할 내용을 넣는 곳
			System.out.println("데이터 베이스를 닫습니다");
		}
	}
}

2. NumberFormatException를 예외 처리한 모습

public class NumberFormatEx {
	public static void main(String[] args) {
		String data1 = "100";
		String data2 = "a100";
		
		
		try {
		// 숫자만 있는 data1의 경우에는 상관없지만
		// 문자가 섞여있는 data2의 경우에는 exception 발생
		int value1 = Integer.parseInt(data1);
		int value2 = Integer.parseInt(data2);
		int result = value1 + value2;
		System.out.println(data1 + "+" + data2 + " = " + result);
		
		
		} catch (NumberFormatException e) {
			System.out.println(e.getMessage());
			System.out.println("입력된 숫자 안에 다른 문자가 포함되어있는지 확인해 주세요");
		
		
		} finally {
			System.out.println("--------------------------------");
			System.out.println("데이터 베이스를 종료합니다");
		}
	}
}

예외 종류에 따른 처리코드

다중 catch

try 블록 내부에서는 다양한 종류의 예외가 발생할 수 있다. 이 경우에 발생되는 예외 별로 처리코드를 다르게 하려면 다중 catch문을 사용한다.

public class ArrayIndexExcep {
	public static void main(String[] args) {
		try {
			String data1 = args[0];
			String data2 = args[1];
			System.out.println("args[0] : " + data1);
			System.out.println("args[1] : " + data2);
			
			String name = null;
			System.out.println(name.toString());

		// 배열값이 없을 때 예외처리
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("입력하는 방법이 틀렸씁니다");
			
		// 오류가 두개라도 try에서 가장 먼저 난 오류만
		//catch로 넘어가고 finally로 가기 때문에
		// 다음 오류들은 catch가 실행되지 않는다.
		} catch (NullPointerException e) {
			System.out.println("입력이 없습니다");
			
		} finally {
			System.out.println("데이터베이스를 닫습니다");
		}
	}
}

주석의 내용을 보면 알 수 있다싶이 catch블럭이 여러개라 하더라도 실행되는 블럭은 하나이다. try에서 동시다발적으로 예외가 발생하는 것이 아니라 가장 먼저 난 오류에 해당하는 catch 블럭만 실행된 뒤, 바로 finally 블럭으로 넘어가기 때문이다.

catch 순서

다중 캐치 블록을 작성할 때 주의할 점은 상위 예외 클래스가 하위 예외 클래스보다 아래쪽에 위치해야 한다. try 블록에서 예외가 발생한다면 catch블록은 위에서부터 차례대로 검색된다.

try{
	// 배열 오류발생
	// 숫자->문자 오류발생

} catch (전체 오류 처리) {
	예외처리 1
} catch (배열 오류 처리) {
	예외처리 2
}

이 경우에는 배열 오류가 발생되어 해당 catch로 가려 했으나, 먼저 전체 오류를 받는 블럭이 있기 때문에 원하는 대로 출력이 되지 않는다. 아래와같이 수정해야한다.

try{
	// 배열 오류발생
	// 숫자->문자 오류발생

} catch (배열 오류 처리) {
	예외처리 1
} catch (전체 오류 받는 처리) {
	예외처리 2
}

멀티 catch

하나의 블록에서 여러개의 예외를 처리할 수 있는 멀티 기능이 있다. catch 괄호 안에 동일하게 처리하고 싶은 예외를 | 로 연결하면 되는 것이다.

try {
	// 배열 오류 발생
    	// 숫자 -> 문자 오류 발생
} catch (배열 오류처리 | 숫자 오류처리) {
	예외처리 1
} catch (전체 오류 받기) {
}

예시 보기

public class Main {
	public static void main(String[] args) {
		try {
			String data1 = args[0];
			String data2 = args[1];
			int value1 = Integer.parseInt(data1);
			int value2 = Integer.parseInt(data2);
			int result = value1 + value2;
			System.out.println(data1 +"+"+ data2 +"="+ result);
			
		// 배열 오류와 널포인트 오류가 발생한 경우에 받는 catch 블록
		// 변수 e에 오류 정보를 받아놓는다
		} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
			System.out.println("실행 매개값의 수가 부족하거나 숫자로 변환할 수 없습니다");
		
		// 그 외의 오류가 발생시 받아주는 catch 블록
		// Exception 은 모든 오류를 받아주는 처리코드이다.
		} catch (Exception e) {
			System.out.println("알 수 없는 오류발생");
		
		} finally {
			System.out.println("다시 실행하세요");
		}
	}
}

자동 리소스 닫기

-코드의 실행 위치가 try문을 벗어나면 try-with-resources는 try안에서 선언된 객체의 close()메소드들을 호출한다. 그래서 finally 에서 close()를 명시적으로 호출해줄 필요 x
-try-with-resourc는 구문이 따로 있는건 아님

사용 모습 보기

  1. FileInputStream 클래스
public class FileInputStream implements AutoCloseable{
	// AutoCloseable은 close() 메소드가 정의되어 있고
	// try-with-resources는 이 close() 메소드를 자동 호출한다
	private String file;
	
	// 입력으로 file을 받아 변수로 올리기
	public FileInputStream(String file) {
		this.file = file;
	}
	
	// read() 호출시 실행될 부분
	public void read() {
		System.out.println(file + "을 열겠습니댜");
		System.out.println(file + "을 읽습니다");
	}
	
	// 예외가 던져지면 바로 close() 가 실행되는 것
	@Override
	public void close() throws Exception {
		System.out.println(file + "을 닫습니다");
	}
}
  1. TryWithSource 클래스
public class TryWithSource {
	public static void main(String[] args) {
		try (FileInputStream fis = new FileInputStream("file.txt")){
			fis.read();
			
			// 강제적으로 예외 발생시킴
			// throw new 예외명
			throw new Exception();
		} catch (Exception e) {
			System.out.println("예외 처리 코드 발생");
		}
	}
}

실행해보면 아래와 같이 출력된다.

file.txt을 열겠습니댜
file.txt을 읽습니다
file.txt을 닫습니다
예외 처리 코드 발생

먼저 매개변수 값으로 file.txt 가 들어가 read() 메소드가 정상적으로 실행될 것이나, throw를 사용해 강제적으로 예외를 던져버린다. 그럼 예외를 받도록 오버로딩된 FileInputStream 클래스의 close()부분이 실행. 그 후 메인으로 다시 넘어와 catch부분이 실행되는 것이다.

Try-with-resourcs로 close()가 호출되는 객체

-위 FileInputStream 클래스를 보면 implements AutoCloseable 해준것을 확인 할 수 있다. 맞다. close()메소드는 AutoCloseable에 있는 추상메소드 close()를 오버로딩 해준것이다.
-implements AutoCloseable를 해주지 않으면 close()메소드를 사용할 수 없고 아래와 같은 오류가 난다.

Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
	The resource type FileInputStream does not implement java.lang.AutoCloseable
	The type FileInputStream is not visible

	at a2.TryWithSource.main(TryWithSource.java:5)

예외 떠넘기기

메소드 내부에서 예외가 발생할 수 있는 코드를 작성할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만, 경우에 따라서는 메소드를 호출한 곳으로 떠넘길 수도 있다. 이때 사용하는 키워드가 throws

리터타입 메소드명(매개변수) throws 예외 {    }
ex)
void A(int num) throws Exception {    }
// Exception = 모든 예외 떠넘기기

throws 키워드 뒤에는 떠넘길 예외 클래스들을 호출하는데 반점(,)으로 구분해 여러개 나열해줘도 된다.

void A() throws RuntimeException, ArithmeticException {    }

예를 들어 아래와 같은 메소드의 경우 호출해 사용하게 되면

void mC() throws ArithmeticException {
	System.out.println("mC() 메소드");
	System.out.println(15/0);
}

분모를 0으로 뒀기 때문에 수학적으로 계산 오류가 난다. 이런 수학적 오류가 발생했을 시 ArithmeticException 예외 클래스에게 떠넘기겠다 작성한 것이고, 아래와 같이 호출한 곳에서 받아주는 것이다.

try {
	mC();
} catch (ArithmeticException e) {
	System.out.println("계산 입력이 틀렸습니다");
}

사용 모습 보기

public class ThrowsEx {
	public static void main(String[] args) {
		A a = new A();
		
		try {
			a.mA();
		} catch (ArithmeticException e) {
			System.out.println("계산 입력이 틀렸습니다");
		}
	}
}

class A{
	// 예외를 자기를 호출한 메소드에게 떠넘기기
	void mC() throws ArithmeticException {
		System.out.println("mC() 메소드");
		// 분모를 0으로 두어 수학적으로 예외가 발생
		System.out.println(15/0);
	}
}

고의로 수학적 오류를 발생시켜 예외 떠넘기기 한 모습. 실행 모습은 아래와 같다.

mC() 메소드
계산 입력이 틀렸습니다
public class ThrowsEx {
	public static void main(String[] args) {
		A a = new A();
		
		try {
			a.mA();
		} catch (ArrayIndexOutOfBoundsException e) {
			System.out.println("계산 입력이 틀렸습니다");
		}
	}
}

class A{
	void mA() {
		System.out.println("mA() 메소드");
		mB();
	}
	void mB() throws ArrayIndexOutOfBoundsException{
		System.out.println("mB() 메소드");
		int[] num = new int[3];
		// 범위를 넘어간 ArrayIndex 예외
		num[3] = 10;
	}
}

고의로 배열 범위 오류를 일으켰고, 호출한 곳으로 예외를 떠넘기고 받았다. 실행 결과는 아래와 같다.

mA() 메소드
mB() 메소드
계산 입력이 틀렸습니다
  1. 여러개를 떠넘기고, 여러개를 받는 것도 가능하다
public class ThrowsEx {
	public static void main(String[] args) {
		A a = new A();
		
		try {
		a.mA();
		} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
			System.out.println("계산 입력이 틀렸습니다");
		}
	}
}

class A{
	void mA() {
		mB();
		System.out.println("mA() 메소드");
	}
	void mB() throws ArrayIndexOutOfBoundsException{
		mC();
		int[] num = new int[3];
		// 범위를 넘어간 ArrayIndex 예외
		num[3] = 10;
		System.out.println("mB() 메소드");
	}
	void mC() throws ArithmeticException {
		System.out.println("mC() 메소드");
		// 분모를 0으로 두어 수학적으로 예외가 발생
		System.out.println(15/0);
	}
}

Mc() 메소드와 mB() 메소드 각각 고의로 오류를 일으켰고, 받는 곳에서는 두 문제 모두 받을 수 있도록 작성해주었다. 현 코드 실행시 mC()메소드의 오류가 먼저 나타나 mB()의 예외처리는 되지 않는 모습을 확인할 수 있다. 실행 모습은 아래와 같다.

mC() 메소드
계산 입력이 틀렸습니다

사용자 정의 예외

프로그램을 개발하다보면 자바 표준 API에서 제공하는 예외 클래스만으로는 다양한 종류의 예외를 표현하지 못 할 수 있다. 애플리케이션 서비스와 관련된 예외를 애플리케이션 예외(Application Exception)라고 하며, 이는 개발자가 직접 정의해서 만들어야 하므로 사용자 정의 예외라고도 한다.

사용자 정의 예외 클래스 선언

사용자 정의 예외 클래스는 컴파일러가 체크하는 일반 예외로 선언할 수도 있고, 컴파일러가 체크하지 않는 실행 예외로 선언할 수도 있다.
-일반예외로 선언시 Exception을 상속하면 되고,
-실행예외로 선언시 RuntimeException을 상속하면 된다.

public class oooException extends [ Exception | RuntimeException ] {
	public oooException() {    }
    	public oooException(String message) {
        	super(message);
        }
}

위와 같이 사용자 예외도 ~Exception으로 끝나는 것이 좋다. 명시적 생성자는 예외 발생 원인을 (예외 메시지를) 전달하기 위해 String 타입의 매개변수를 가진다.
-> String 타입의 매개 변수를 갖는 생성자는 상위 클래스의 생성자를 호출하여 예외 메시지를 넘겨준다. 예외 메시지 용도는 catch{ } 블록의 예외처리 코드에서 이용할 수 있다.

// 일반 예외로 선언할 것이니
// Exception을 상속 받는다
public class BalanceInsufficientEx extends Exception{
	public BalanceInsufficientEx() {	}
	
	public BalanceInsufficientEx(String message) {
		// 명시적 생성자
		// super로 넘기면 부모인 Exception에서 받아가고 처리된다
		super(message);
	}
}

예외 발생 시키기

코드에서 예외를 발생시키는 방법은 다음과 같다.
throw new oooException(); or throw new oooException("메시지")
예외 객체 생성시 기본 생성자 혹은 명시적 생성자 중 어떤 것을 사용해도 상관 없다. 만약 catch 블록에서 예외 메시지가 필요하다면 예외 메시지를 가지고 있는 생성자를 이용해야 한다. 대부분은 자신을 호출한 곳에서 예외를 처리하도록 throws 키워드로 떠넘긴다.

public void method() throws oooException {
	throw new oooException("메시지");
}

다음 예제는 잔고 필드와 출금액을 배교해 잔고가 부족하면 사용자 정의 예외를 발생키기도록한다.

public class Account {
	// 일반 필드
	private long balance;
	
	// 기본 생성자 작성
	public Account() { }
	
	public long getBalance() {
		return balance;
	}
	public void deposit(int money) {
		balance += money;
	}
	
	// 사용자 정의 Exception을 호출해 넘겨주기
	public void withdraw(int money) throws BalanceInsufficientEx {
		if(balance < money) {
			// 코드에서 고의로 예외를 밠생시키는 방법
			throw new BalanceInsufficientEx("잔고 부족 " + (money-balance) + " 모자람");
		}
		balance -= money;
	}
}

예외 정보 얻기

try 블록에서 예외가 발생되면 예외 객체는 catch 블록의 매개 변수에서 참조하게 되므로 매개변수를 이용하면 예외 객체의 정보를 알 수 있다. 모든 예외 객체는 Exception을 상속받기 때문에 Exception이 가지고 있는 메소드들을 모든 예외 객체에서 호출할 수 있는데, 그중에서도 많이 사용되는 메소드는 getMessage()printStrackTrace() 이다.

getMessage()
: 좀 더 상세한 예외의 원인을 세분화하기 위해 예외 코드를 포함하기도 하는데,
예를 들어 DB에서 발생한 오류들은 예외 코드가 예외 메시지로 전달된다.
이와 같은 예외 메시지를 리턴 값으로 얻을 수 있는 메소드.

printStrackTrace()
: 예외 발생 코드를 추적해 콘솔에 모두 출력한다.
어떤 예외가 어디에서 발생했는지 상세히 출력해주기 때문에
프로그램을 테스트하면서 오류를 찾을 때 활용된다.

사용하는 모습보기

public class AccountEx {
	public static void main(String[] args) {
		// 객체 생성
		Account ac = new Account();
		
		// 예금하기
		// 만원을 잔고에 넣어주기
		ac.deposit(10000);
		System.out.println("예금액" + ac.getBalance());
		
		// 출금하기
		try {
			ac.withdraw(3000);
			System.out.println(ac.getBalance());
			ac.withdraw(30000);
			System.out.println(ac.getBalance());
		} catch (BalanceInsufficientEx e) {
        		// 예외 메시지 얻기
			String message = e.getMessage();
			System.out.println(message);
			System.out.println();
			e.printStackTrace();
		}
	}
}

실행 결과는 이렇다

예금액10000
7000
잔고 부족 23000 모자람

a4.BalanceInsufficientEx: 잔고 부족 23000 모자람
	at a4.Account.withdraw(Account.java:21)
	at a4.AccountEx.main(AccountEx.java:17)

오류가 발생되기 전까지는 정상 출력되다가, 오류 발생되니 catch로 넘어가 예외처리되는 걸 확인할 수 있다.
getMessage() 로 설정한 message를 출력해주었고,
printSttackTrace() 로 어떤 예외가 어디에서 발생했는지 상세히 출력됨을 볼 수 있다.


예외 처리 사용해보기

요구사항 : 점수를 받아 출력하되 올바르지 않은 입력을 걸러내자

1. 사용자 정의 예외 클래스 만들기

public class ScoreException extends Exception{
	// 기본 생성자
	public ScoreException() {	}
	public ScoreException(String num) {
		super(num);
	}
}

2. 조건으로 걸러내 정의한 예외처리로 넘겨주기

public class Score {
	// 일반 필드
	int score;
	
	// 기본 생성자 작성
	public Score() { }
	
	// 사용자 정의 Exception을 호출해 넘겨주기
	public void Input(int score) throws ScoreException {
		// 받은 점수를 필드값으로 넘겨주고
		this.score = score;
		// 예외를 발생시킬 조건문 작성
		if(score < 0 || score > 100) {
			System.out.println("----------------------------");
			System.out.println("입력 점수의 범위는 0~100 이여야 합니다");
			System.out.println("입력된 점수 : " + score);
			throw new ScoreException();
			
		}
	}
}

3. 객체 생성해 사용해보기

public class ScoreEx {
	public static void main(String[] args) {
		// 객체 생성
		Score jumsu = new Score();
		
		// 점수 입력하기
		try {
			jumsu.Input(90);
			System.out.println(jumsu.score);
			jumsu.Input(100);
			System.out.println(jumsu.score);
			
			// 조건문에 걸리는 입력
			jumsu.Input(120);
			System.out.println(jumsu.score);
			
			
		} catch (ScoreException e) {
			System.out.println("점수를 잘못 입력 하셨습니다");
		}
	}
}

0개의 댓글