프로그램의 오류에는 크게 3가지 종류가 있다
public class playGround {
public static void play() {
int[] intArr = {1, 2, 3, 4};
// system.out.println(intArr[0]); 컴파일 에러 system은 컴파일 시 체크 가능
// System.out.println(intArr[8]); intArr[8]이라는 구문에는 오류가 없지만 실행 중 오류가 발생
System.out.println("First index = " + intArr[1]); //의도와 다른 출력
}
}
컴파일 에러의 경우는 컴파일러가 기본적으로 오타나 잘못된 구문, 자료형의 체크 등 기본적으로 체크를 한다. IDE에서도 충분한 체크를 통해 오류를 줄일 수 있다.
하지만, 정상적으로 컴파일이 되더라도 에러가 발생할 수 있다. 이러한 잠재적인 에러는 컴파일러가 거르지 못하고, 프로그램이 비정상적으로 종료된다.
이런 에러를 방지하기 위해 런타임시 발생할 수 있는 경우에 대비하여 처리하여야 한다.
Java에서는 런타임에러를 에러
와 예외
로 구분하였다. 이 둘의 차이는 프로그램 코드를 통해 수습할 수 있느냐 없느냐의 차이이다.
Java에서는 런타임오류들을 클래스로 정의하였다. 오류들은 모두 클래스이고, 최고 조상은 Object()
이다. 그 밑으로는,
Throwable
Error
StackOverFlow
,OutOfMemoryError
, ...
Exception
ClassNotFoundException
, ...
RuntimeException
ClassCastExecption
,NullPointerException
, ...
Error
와 Exception
의 차이는 코드를 통한 수습의 가능성이다.
Exception
도 RuntimeException
이냐 아니냐로 구분지을 수 있는데, 그 차이는 예외의 발생요인이 프로그래머인가 외부인가이다. 프로그래머의 실수로 발생하는 예외는 RuntimeException
로 구분 짓는다.
또, 예외처리의 의무에서도 차이점이 있다.
Exception
과 그 자손에 해당하는 클래스들은 예외처리를 해주지 않으면, 컴파일 조차 안된다.
하지만, RuntimeException
과 그 자손에 해당하는 클래스들은 예외처리를 하지 않아도 컴파일이 된다. 그렇다고 해서 런타임에러가 발생하지 않는 다는 것은 아니다.
발생할 수 있는 모든 예외에 대해 처리가 강요되는 것은 안정성이 더 보장될 수 있지만, 불필요한 예외처리를 강요받아 효율이 낮아질 수 있기 때문에, 자주 사용되는 경우와 연관되어있는 예외들의 처리들은 의무가 아닌 선택사항으로 남아져 있는 것이다.
int[] a = new int[10]; //NullPointerException의 가능성이 있지만 처리의 의무는 아님
이러한 예외를 어떻게 수정을하고 처리하는지 알아보자.
int a = 10 / 0; //--1
/*
Exception in thread "main" java.lang.ArithmeticException: / by zero
at playground.playGround.play(playGround.java:6)
at playground.main.main(main.java:6)
*/
런타임 에러가 발생하면, 어떤 에러이고 어디서 발생하는지 알려준다.
에러가 발생하면, 그에 해당하는 에러(예외)클래스 타입의 인스턴스가 생성된다. 그 인스턴스에는 에러에 대한 정보와 메서드를 가지고 있기 때문에 예외가 발생한다고 해서 무조건 종료되는 것은 아니다.
try-catch를 통해 에러를 처리할 수 있다.
public class playGround {
public static void play() {
try{
System.out.println(1+1);
System.out.println(1/0); //--1
System.out.println(1+2); //--3
}catch (ArithmeticException e){ //--2
System.out.println("ArithmeticException");
}
}
}
// result
// 2
// ArithmeticException
--1
을 보면, ArithmeticException
타입의 인스턴스가 생성된다. 이때 인스턴스가 생성된 문장이 try블럭안에 있다면, 이를 처리할 수 있는 catch블럭을 찾아간다.
--2
에서 ArithmeticException
를 처리하고 있기 때문에 해당 구현부를 수행하고, try-catch문을 빠져나온다. 그래서 --3
의 문장은 실행되지 않는다.
public class playGround {
public static void play() {
try{
System.out.println(1+1);
System.out.println(1/0);
System.out.println(1+2);
}
catch (Exception e){ //--1
System.out.println("Exception");
}catch (ArithmeticException e){ //--2
System.out.println("ArithmeticException");
}
}
}
catch문도 주의해야할 점이 있다. --1
의 Exception
타입은 --2
의 ArithmeticException
의 부모이다. 그래서, try문에서 발생한 에러는 --1
에서도 잡아낼 수 있다.(다형성)
try-catch문의 흐름을 이해했다면, 더 넓은 범위의 예외타입이 아래쪽에 위치해야 좀 더 세부적인 예외처리가 가능하다는 것을 쉽게 알 수 있다.
public class playGround {
public static void play() {
try{
System.out.println(1/0);
}catch (Exception e){ //--1
System.out.println("Exception");
}finally{
System.out.println("finished");
}
}
}
예외의 발생여부와 상관없이 수행하고자 하는 문장은 finally
블럭을 통해 실행 할 수 있다.
public class playGround {
public static void play() {
try{
System.out.println(1+1);
System.out.println(1/0);
System.out.println(1+2);
}
catch (ArithmeticException e){
e.getMessage();
e.printStackTrace();
}
}
}
예외클래스들은 여러 메서드를 가지고 있기 때문에, 이를 활용해서 생성된 예외 인스턴스를 다양하게 다룰 수 있다.
public class playGround {
public static void play() {
try{
throw new ArithmeticException("ArithmeticException!!");
} catch (ArithmeticException e){
System.out.println(e.getMessage()); //ArithmeticException!!
}
}
}
throw를 통해 예외를 고의적으로 발생 시킬 수 있다.
throw new Exception()
에서 매개변수는 getMessage()
메소드와 연관있다는 것을 알 수 있다.
try{
System.out.println(1/0);
}
catch (ArithmeticException e){
System.out.println("Error");
}
catch (NullPointerException e){
System.out.println("Error");
}
catch문의 중복을 제거할 수 있다.
try{
System.out.println(1/0);
}
catch (ArithmeticException | NullPointerException e){
System.out.println("Error");
//e.methodOnlyFromNullPointerEx(); ERROR --1
}
두 코드는 동일한 코드이다. 이때 주의해야 할 점은,
--1
try-catch문 말고도 예외를 처리하는 방법이 있다.
메소드에서 예외 타입을 반환하여 던지는 경우가 있다.
public class playGround {
public static void play() {
int a = 0;
try{
checkNum(a);
System.out.println(100 / a);
}catch (ArithmeticException e){
System.out.println(e.getMessage());
}
}
static void checkNum(int a) throws ArithmeticException{
if (a == 0){
throw new ArithmeticException("Can't be divided by 0");
}
System.out.println("Continue");
}
}
// Can't be divided by 0
checkNum()
메소드가 int a
의 값에 따라 예외처리를 하는 것을 볼 수 있다.
이때,
void checkNum(int a) throws ArithmeticException {}
와 같은 구문으로 반환하려는 예외 타입을 명시할 수 있다. 여러개를 명시 할 수 있다.
주의할 점은, 이 메소드를 오버라이딩하는 경우이다. 오버라이딩의 경우 조상보다 많은 예외선언은 불가하기 때문에,
class Child extends Parent{
void checkNum(int a, int b) throws Exception{}; //Override 불가
}
와 같은 결과가 나타날 수 있다. Exception
은 모든 예외타입의 부모이기 때문에, 선언된 갯수는 하나이지만 실제 반환할 수 있는 예외가 많다는 점을 주의하자.
Java에서 제공하는 예외타입 이외의 예외들을 선언할 수 있다.
class OverNumberException extends Exception{ //Exception을 상속받았기 때문에 반드시 예외처리 필요
OverNumberException(String errMessage) {
super(errMessage);
}
}
public class playGround {
public static void play() {
int a = 1000;
try{
checkNum(a);
System.out.println(100 / a);
}catch (OverNumberException e){
System.out.println(e.getMessage());
}
}
static void checkNum(int a) throws OverNumberException {
if (a > 100){
throw new OverNumberException("Over Value"); //--1
}
System.out.println("Continue");
}
}
OverNumberException
라는 예외를 하나 만들었다. 예외의 최고 조상인 Exception
을 상속받고,
여러 메소드들을 통해 예외타입을 다룰 수 있다. 이 예제에서는 super()
생성자를 통해 --1
을 구현했다.
예외들간에도 관계들이 있을 것이다.
OverNumberExcpetion()
, LowerNumberExcpetion()
이라는 예외는 WrongNumberExcepction()
이라고 묶을 수 있을 것이다.
어떠한 예외의 원인인 예외들의 관계를 통해 보다 정확화게 예외를 파악할 수 있다.
package playground;
class OverNumberException extends Exception{
OverNumberException(String errMessage) {
super(errMessage);
}
}
class UnderNumberException extends Exception{
UnderNumberException(String errMessage) {
super(errMessage);
}
}
class WrongNumberException extends Exception{
WrongNumberException(String errMessage) {
super(errMessage);
}
}
public class playGround {
public static void play(){
try{
playForException();
}catch (WrongNumberException e){
e.printStackTrace();
}
}
static void playForException() throws WrongNumberException {
int a = 1000;
try{
checkNum(a);
System.out.println(100 / a);
}catch (OverNumberException | ArithmeticException | UnderNumberException e){
WrongNumberException we = new WrongNumberException("Wrong Number Value");
we.initCause(e); // --1
throw we;
}
}
static void checkNum(int a) throws OverNumberException, ArithmeticException, UnderNumberException {
if (a > 100){
throw new OverNumberException("Over Value");
}
if (a == 0){
throw new ArithmeticException("Zero Value");
}
if (a < 0){
throw new UnderNumberException("Under Value");
}
System.out.println("Continue");
}
}
// 실행 결과.
// a > 100이므로
// OverNumberException
// -> WrongNumberException의 원인이 됨. // .initCause(e)
// e.printStackTrace();
/*
playground.WrongNumberException: Wrong Number Value
at playground.playGround.playForException(playGround.java:31)
at playground.playGround.play(playGround.java:20)
at playground.main.main(main.java:6)
Caused by: playground.OverNumberException: Over Value
at playground.playGround.checkNum(playGround.java:38)
at playground.playGround.playForException(playGround.java:28)
... 2 more
*/
예외들을 연결시켜, WrongNumberValue
가 OverValue
에 의해 일어났다는 것을 알 수 있다.