Java(심화) - 예외(Exception) part1
안녕하세요
Shiny Ocean 입니다 : )
이번 포스팅에서 다루어볼 내용은 예외 입니다.
개요
개발을 진행하다보면 여러가지의 예외와 마주치게 됩니다. 특히 자바에서는 NULL과 관련된 예외를 마주하는 경우가 참 많습니다. 그런데 이러한 예외를 에러와 동일하게 생각하면 안됩니다. 비슷한 어감에 비슷한 뜻인것 같지만 예외와 에러는 다릅니다. 이번포스팅은 예외의 기본적인 개념과 예외클래스를 다루어 보겠습니다.
예외 VS 에러
먼저 예외와 에러 둘다 오류입니다. 개발자가 의도한 정상적인 범주의 동작에서 벗어난 것이기 때문입니다.
에러의 경우 하드웨어의 오동작으로 인한 오류일수도 있습니다. 에러가 발생한다면 프로그램은 종료됩니다. 정상적인 상태로 돌아갈수 없이 해당 에러의 원인을 찾아 근본적으로 고치지 못한다면 프로그램은 작동할수 없습니다.
하지만 예외의 경우 에러와 조금 다릅니다. 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인한 오류이죠. 이또한 오류이기 떄문에 프로그램은 종료되지만, 예외의 종류를 분류하여 해당 예외가 발생했을떄에 대한 처리를 추가해 준다면 다시 프로그램은 정상적인 실행상태로 돌아갈수 있습니다.
예외의 종류
1. 일반(컴파일 체크) 예외, Exception
예외 처리 코드가 없다면 컴파일이 되지 않는 예외
2. 실행 예외, RuntimeException
예외 처리 코드를 생략 하더라도 컴파일이 되는 예외
경험에 따라 예외 처리 코드를 작성할 필요
예외 클래스
자바는 예외를 클래스화 하여 다룹니다.
어떠한 예외가 발생을 한다면 그에 맞는 클래스를 찾아 객체로 만들고, 예외가 발생한 이유를 해당 객체안에 저장을 하여 예외처리 코드에서 이용할수 있도록 합니다. 해당 클래스의 상속관계는 아래와 같습니다.
출처 - 이것이 자바다 (한빛미디어)
일반 예외의 경우 가장 상위 클래스인 Exception 을 상속받습니다.
하지만 실행예외의 경우 Exception를 상속받은 RuntimeException을 상속받습니다.
즉 일반예외와 실행예외를 구분지을 때는 RuntimeException을 상속받냐 안받냐로 판단할수 있습니다.
실행 예외 (Runtime Exception)
1. NullPointerException
대표적인 실행예외중 첫번째는 널포인터예외이다. 이는 이전에 NULL값에 대하여 다룬 포스팅에서도 다룬바가 있습니다. 참조 변수 등을 이용할때 객체 참조가 없는 상태, 즉 .null값을 갖는 참조변수로 객체 접근 연산자인 도트(.)를 사용할때 발생합니다.
2. ArrayIndexOutOfBoundsException
배열의 범위를 초과한 인덱스에 접근하거나 사용할 경우에 발생하는 예외입니다. 주로 반복문의 조건을 잘못 생각해서 실행했을때 빈번히 발생합니다.
3. NumberFormatException
문자열을 숫자로 변환하는 경우가 많다, String을 split하지 않고 Integer.parsInt같은 변환 함수를 사용했을 때 발생한다.
예를 들어 Integer.parsInt(" a123d ") 를 실행하는 경우이다.
4. ClassCastException
네번째로 다룰 클래스캐스트예외 또한 많이 발생합니다, 클래스의 타입 변환이 되지 않을 경우 발생하는 예외입니다.
추상클래스나 인터페이스의 구현체를 부모클래스가 아닌 다른 타입으로 변환하려 할때 발생합니다.
(같은 부모를 상속받은 형제클래스의 객체로 강제타입변환을 시도하는 경우 등)
예외 처리 코드(try catch finally)
가장 이상적인 개발은 메인스레드가 종료될때까지 아무런 예외가 발생하지 않는것일것입니다. 하지만 개발 상황과 목적에 따라 유용성있게 개발을 해야하기 때문에 예외가 발생하지 않을수는 없습니다.
이와 관련된 궤변을 한번 해보겠습니다.
예를들어 원인은 알수 없지만 어떠한 코드가 매일 10시47분에는 정상작동이 안된다고 가정해보겠습니다.
그런데 이는 회사에서 2년을 투자해서 개발한 코드이고, 예외가 딱 하나 (10시47분에는 정상작동 안됨) 발생한다해보겠습니다. 프로그램 배포는 얼마 남지 않은 상황에 개발팀 전직원이 야근하며 2년동안 처리해온 모든 로직을 살펴보며 왜 예외가 발생했는지 알아보고 로직을 수정하는것이 나을까요, 아니면 일단 예외 처리를 하나하고 배포이후 대응하는것이 나을까요?
시간적인 비용과 상황을 고려한다면 위의 상황에서는 예외처리를 선택할것 같습니다.
예외처리코드란?
예외가 발생할시 프로그램의 종료를 막고, 정상상태를 유지할 수 있도록 처리하는 코드
일반예외의 경우 반드시 작성해야 컴파일이 되며, 실행예외의 경우 컴파일시 요구하지 않고 경험에 의해서 작성해야 합니다. 실행예외는 작성시 잘 발견되지 않다가, 디버깅과 테스팅 단계에서 발견되는 경우가 많습니다.
예외처리 코드 작성방법)
1. try 블럭안에 예외가 발생할것 같은 코드를 넣는다.
2. catch(예외클래스 e) 블럭을 통해 예외를 처리한다.
해당 블럭은 try에서 발생가능한 예외 클래스 객체인 e를 매개변수로 선언하여 예외의 정보를 담아 처리합니다.
즉 try 블럭에서 발생가능한 예외의 클래스가 NullPointerException 이라면 catch(NullPointerException e) 라고 블럭을 선언한다.
3. finally 블럭에 예외 발생유무에 상관없이 항상 실행되는 로직을 작성한다.(이는 옵션이다.)
다중 Catch
만약 try블럭에서 발생할수 있는 예외의 경우가 두가지 이상이라면 하나의 Catch블럭 하나로는 예외를 처리하기 어려울것이다. 이때 사용하는것이 다중 catch이고 try블럭 하단에 여러개의 catch 블럭을 작성하여 처리해주면 된다.
간단한 예제를 다루어보겠습니다.
class Obj{
}
public class Exam2 {
public static void main(String[] args) {
try {
Obj obj = null;
int[] arr = new int[5];
arr[6] = 1;
System.out.println(obj.toString());
}
catch(NullPointerException e) {
System.out.println("널 포인터 예외 발생함");
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스 범위 초과 예외 발생");
}
}
}
위처럼 try 구문에 두가지 예외를 발생시켰고 catch 구문에 예외를 처리할수 있는 코드를 작성해 두었습니다. 위의 예지는 arr[6] = 1; 즉 배열의 범위초과 예외가 먼저 발생할것이기 때문에 예외에 해당되는 처리 구문이 실행되어 "배열 인덱스 범위 초과 예외 발생"문구가 콘솔에 출력될 것입니다.
중요한점은 예외가 발생하며 try구문을 바로 빠져나오고 catch가 실행되기 때문에 arr 에러 아래에 발생 가능한 예외인 널포인터 예외는 발생되지 않습니다.
위의 코드를 굳이 모든 예외 처리 결과를 확인하고 싶다면 아래와 같이 작성하면 됩니다.
class Obj{
}
public class Exam2 {
public static void main(String[] args) {
for (int i =0;i<2;i++) {
try {
Obj obj = null;
int[] arr = new int[5];
if(i==0) {
arr[6] = 1;
}else {
System.out.println(obj.toString());
}
}
catch(NullPointerException e) {
System.out.println("널 포인터 예외 발생함");
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스 범위 초과 예외 발생");
}
}
}
}
반복문을 사용해 첫번째 반복에는 ArrayIndexOutOfBoundsException을 두번쨰 반복에는 NullPointerException을 발생시켰고 결과는 아래와 같습니다.