『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처
자바에는 예외(Exception)라는 것이 있다.
하는 경우가 여기에 속한다. 이런 예외적인 일이 발생하게 되면 “예외”라는 것을 던진다.
예를 들어, 5개의 공간을 가지는 int 타입의 배열의 6번째 값을 출력하도록 하고 실행하면, 아래 오류가 출력된다.
Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at c.exception.ExceptionSample.arrayOutOfBounds(ExceptionSample.java:9)
at c.exception.ExceptionSample.main(ExceptionSample.java:5)
... 11 more
이 예에서는 호출 관계가 단순하여 스택 트레이스가 몇 개 찍히지 않았지만, 실제 운영되는 자바 기반의 시스템들은 그렇게 단순하지 않기 때문에 몇 십 줄에서 백 줄까지 출력되기도 한다.
이렇게 예외가 발생하지 않도록 개발하는 것이 우선이다.
이 때 사용하는 것이 try-catch 문이다.
try 뒤에 중괄호로 예외가 발생하는 문장들을 묶어 주고 catch 괄호 안에 예외가 발생하는 부분만 묶어준다.
즉, 다음과 같이 예외가 발생하는 부분만 try-catch로 묶어 줘도 전혀 문제되지 않는다.
public void arrayOutOfBoundsTryCatch() {
try {
int[] intArray=new int[5];
System.out.println(intArray[5]);
} catch (Exception e) {
System.err.println("Exception occured.");
}
}
이렇게 작성하고 컴파일 및 실행해보면 정상적으로 동작하는 것처럼 보인다. 하지만, 실제로는 예외가 발생한 것이다.
try-catch의 try 블록 안에서 예외가 발생되면 그 이하의 문장은 실행되지 않고 바로 catch 블록으로 넘어간다.
System.err
는 일반적인 콘솔 화면에서는 별로 구분이 안 되지만, 개발 도구인 IDE에서는 출력 결과가 다른 색으로 표시된다. 오류가 발생하는 부분에는 이와 같이System.err
를 사용하는 것을 생활화 하는 것이 좋다.
아래와 같이 사용한다.
try {
// 예외가 발생할 수 있는 코드들
} catch(Exception e) {
// 예외가 발생되면 실행되는 코드들
}
try 블록은 말 그대로 중괄호로 쌓여 있는 블록이다. 그래서, try 블록 내에서 선언한 변수를 catch에서 사용할 수 없다.
따라서 일반적으로 catch 문장에서 사용할 변수에 대해서는 다음과 같이 try 앞에 미리 선언해 놓는다.
public void arrayOutOfBoundsTryCatch() {
int[] intArray=new int[5];
try {
System.out.println(intArray[5]);
} catch (Exception e) {
System.out.println(intArray.length);
}
}
try-catch 구문에는 추가로 finally 블록이 붙을 수 있다.
자바에서 예외를 처리할 때 finally는 어떠한 경우에도-예외 발생 여부와 상관 없이- 반드시 실행된다.
finally 블록은 코드의 중복을 피하기 위해서 반드시 필요하다. 특히 파일 입출력 시 꼭 필요하며, 26장에 다시 나온다.
try-catch문을 쓸 때 catch 블록이 시작되기 전(중괄호)에 있는 소괄호에는 예외의 종류를 명시한다.
try {
...
} catch(예외클래스 e) {
...
}
앞선 예제에선 Exception e
를 썼는데, 다른 예외 클래스를 선언해도 상관없다.
모든 예외의 부모 클래스는 java.lang.Exception 클래스다. 즉, Exception e
라고 쓰면 모든 예외를 받는다는 말이다.
catch 블록은 여러 개를 쓸 수 있다. 이 때, catch 블록의 순서는 매우 중요하다.
예를 들어, 아래처럼 catch(Exception e)
를 첫 번째로 썼을 경우, 컴파일 단계에서 already been caught 에러 메시지가 발생한다.
public void multipleCatch() {
try {
...
} catch (Exception e) {
System.out.println(intArray.length);
} catch(ArrayIndexOutOfBoundsException e) { //compile error
System.out.println("ArrayIndexOutOfBoundsException occurred");
}
}
이 코드처럼 부모 예외 클래스(Exception)가 이미 catch를 하고, 자식 클래스가 그 아래에서 catch를 하도록 되어 있을 경우에는 자식 클래스가 예외를 처리할 기회가 없다.
따라서 사용될 일이 없는 catch를 선언했다고 에러를 던진 것이다.
다음과 같이 사용해야 한다.
public void multipleCatch() {
try {
// NullPointerException 발생!
int[] intArray = null;
System.out.println(intArray[5]);
...
} catch(NullPointerException e) {
System.out.println("NullPointerException occurred");
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException occurred");
} catch (Exception e) {
System.out.println(intArray.length);
}
}
💡 NullPointerException은 아직 초기화를 하지 않았거나 null이 된 객체에 있는 메소드를 호출하거나 처리하려고 할 때 발생하는 예외다.
이렇게 쓰면 NullPointerException
예외가 발생한다. intArray의 5번째 값을 찾기 전에 객체가 null인 것이 먼저 확인되었기 때문이다. null인 객체를 갖고 작업하면 안 되기 때문에 해당 객체가 null인지 확인하는 작업은 반드시 먼저 선행되어야만 한다.
그리고, catch 문을 사용할 때에는 Exception 클래스로 catch 하는 것을 가장 아래에 추가할 것을 권장한다. 예상치도 못한 예외가 발생하면 제대로 처리가 되지 않기 때문이다.
따라서, 가장 마지막 catch 블록은 Exception 클래스를 선언하여 예외들이 빠져 나가지 않게 묶어주는 것이 좋다.
자바에는 세 종류의 예외가 존재한다.
에러는 자바 프로그램 밖에서 발생한 예외를 말한다. 자바 프로그램이 제대로 동작하지 못하는 경우가 여기에 속한다.
Exception 클래스는 에러가 아니다. 뭔가 자바 프로그램에 오류가 발생했을 때, 오류의 이름이 Error로 끝나면 에러이고, Exception으로 끝나면 예외다.
💡 예외의 종류와 관련된 내용은 아래 사이트를 참고
http://docs.oracle.com/javase/tutorial/essential/exceptions/catchOrDeclare.html
Error와 Exception으로 끝나는 오류의 가장 큰 차이는 프로그램 안에서 발생했는지, 밖에게 발생했는지 여부이다. 하지만, 더 큰 차이는 프로그램이 멈추어 버리느냐 계속 실행할 수 있느냐의 차이다.
더 정확하게 말하면 Error는 프로세스에 영향을 주고 Exception은 쓰레드에만 영향을 준다.
런타임 예외는 예외가 발생할 것을 미리 감지하지 못했을 때 발생한다. 이 런타임 예외에 해당하는 모든 예외들은 RuntimeException을 확장한 예외들이다.
이 예외들은 컴파일할 때는 예외가 발생하지 않지만, 실행 시에 발생할 가능성이 있다. 그래서 이러한 예외들을 "런타임 예외"라고 한다.
컴파일 시에 체크를 하지 않기 때문에 unchecked exception이라고도 부른다.
error와 runtime exception을 제외한 모든 예외가 checked exception이다.
위 내용들을 그림으로 그리면 아래와 같다. (책의 그림을 참고하여 그림)
여기서 Exception을 바로 확장한 클래스들이 Checked 예외이며, RuntimeException 밑에 확장되어 있는 클래스들이 런타임 예외들이다.
Exception과 Error의 공통 부모 클래스는 Object 클래스와 Throwable 클래스다. 다시 말해서 Throwable 클래스를 상속받아 처리하도록 되어 있다.
그래서, Exception과 Error를 처리할 때 Throwable로 처리해도 무관하다.
상속 관계가 이렇게 되어 있는 이유는 Exception이나 Error의 성격은 다르지만, 모두 동일한 이름의 메소드를 사용하여 처리할 수 있도록 하기 위함이다.
Throwable은 매개 변수가 없는 생성자를 기본적으로 제공하고, 예외 메시지를 String으로 넘겨줄 수 있다. 그리고 별도로 예외의 원인을 Throwable 객체로 넘겨 줄 수도 있다.
Throwable 클래스에 선언되어 있고, Exception 클래스에서 Overriding한 메소드는 10개가 넘는다. 그 중 가장 많이 사용되는 메소드는 다음과 같다.
메소드 명 | 설명 |
---|---|
getMessage() | 예외 메시지를 String 형태로 제공 받는다. 예외가 출력되었을 때 어떤 예외가 발생되었는지 확인할 때 매우 유용하며, 그 메시지를 활용하여 별도의 예외 메시지를 사용자에게 보여주려고 할 때 좋다. |
toString() | 예외 메시지를 getMessage() 메소드보다는 약간 더 자세하게, 예외 클래스 이름도 같이 String 형태로 제공한다. |
printStackTrace() | 가장 첫 줄에는 예외 메시지를 출력하고, 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출 관계(스택 트레이스)를 출력해준다. System.out.print없이, e.printStackTrace()로 쓰면 된다. |
printStackTrace() 메소드를 사용하면 자세한 메소드를 볼 수 있지만, 운영할 시스템에 사용하면 많은 양의 로그가 쌓이므로 개발할 때에만 사용하기 바란다.
지금까지는 예외를 처리하는 방법을 배웠다. 이제부터는 예외를 발생시키는 방법을 알아보자.
아래와 같이 throw
를 사용하면 예외를 발생시킬 수 있다.
throw new Exception("write a excepion")'
try-catch로 감쌌다면 위 문장 다음 문장들은 수행되지 않고, catch 블록으로 이동한다. 만약 해당하는 예외가 없다면 발생한 예외는 메소드 밖으로 던져버린다.
다시 말해서, 예외가 발생된 메소드를 호출한 메소드로 던진다는 의미다. 이럴 때 사용하는 것이 throws
구문이다.
다음과 같이 메소드를 선언할 때 같이 사용한다.
public void 메소드() throws Exception {}
// 아래처럼 여러 개를 선언할 수도 있다.
public void 메소드() throws NullPointerExeption, Exception {}
public void 메소드() throws ..., Exception {}
이러면 예외가 발생했을 때 try-catch로 묶어주지 않아도 그 메소드를 호출한 메소드로 예외 처리를 위임하는 것이기 때문에 전혀 문제가 되지 않는다.
호출한 메소드에서는 반드시 try-catch 블록으로 호출하는 문장을 감싸주어야만 한다. 그렇지 않을 경우 컴파일 에러가 발생한다.
호출한 메소드에서도 다시 throws를 할 수 있지만 이렇게 다시 throws하는 것은 그리 좋은 습관이 아니다.
예외를 throw하는 이유는 해당 메소드에서 예외를 처리하지 못하는 상황이거나, 미처 처리하지 못한 예외가 있을 경우에 대비하기 위함이다.
자바에서 예외 처리를 할 때 thow와 throws는 매우 중요하다
앞에서 Throwable을 직접 상속 받는 클래스는 Exception과 Error가 있다고 했다. Error와 관련된 클래스는 개발자가 손댈 필요도 없고, 손대어서도 안된다.
하지만, Error가 아닌 Exception을 처리하는 예외 클래스는 개발자가 임의로 추가해서 만들 수 있다.
단, 한 가지의 조건이 있다. 반드시 Throwable이나 그 자식 클래스의 상속을 받아야만 한다.
Throwable 클래스의 상속을 받아도 되지만, Exception을 처리하는 클래스라면 java.lang.Exception 클래스의 상속을 받는 것이 좋다.
public class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
예외 클래스가 되기 위한 조건은 예외 관련 클래스를 확장(extends)하면 된다.
이렇게 만든 예외 클래스는 아래와 같이
public class CustomException {
public static void main(String args[]) {
CustomException sample = new CustomException();
try {
sample.throwMyException(13);
} catch(MyException mye) {
mye.printStackTrace();
}
}
public void throwMyException(int number) throws MyException{
try {
if(number>12) {
throw new MyException("Number is over than 12");
}
} catch (MyException e) {
e.printStackTrace();
}
}
}
참고로 여기서 MyException
을 던진다고 명시해 놓았지만, 이 메소드를 호출하는 메소드(여기서는 main
)에서는 반드시 MyException
로 catch할 필요 없다.
MyException
의 부모 클래스인 Exception
클래스로 catch해도 무방하다.
자바에 예외를 처리할 때는 표준을 잡고 진행하는 것이 좋다.
앞에서 Exception 클래스를 확장하여 나만의 예외 클래스를 만들었다. 그런데, 이 예외가 항상 발생하지 않고, 실행 시에 발생할 확률이 매우 높은 경우에는 런타임 예외로 만드는 것이 나을 수도 있다.
즉, 클래스 선언 시 extends Exception 대신에 extends RuntimeException으로 선언하는 것이다.
이렇게 되면, 해당 예외를 던지는(throw하는) 메소드를 사용하더라도 try-catch로 묶지 않아도 컴파일 시에 예외가 발생하지 않는다.
하지만, 이 경우에는 예외가 발생할 경우 해당 클래스는 호출하는 다른 클래스에서 예외를 처리하도록 구조적인 안전 장치가 되어 있어야만 한다.
여기서는 안전 장치라고 하는 것은 try-catch로 묶지 않은 메소드를 호출하는 메소드에서 예외를 처리하는 try-catch가 되어 있는 것을 이야기 한다.
public void methodCaller() {
try {
methodCalle();
} catch(Exception e) {
// 예외 처리
}
}
public void methodCalle() {
// RuntimeException 예외 발생 가능성 있는 부분
}
위 코드에서 try-catch를 제외해도 문제는 발생하지 않는다. 하지만 예외가 발생할 확률은 높으므로, 위의 예에서처럼 try-catch로 묶어주는 것이 좋다.
catch 문장에서 처리를 해줄 때, 다음과 같이 catch 문 내에서 아무런 작업도 하지 않도록 처리하는 것은 반드시 피해야만 한다.
try {
// 예외 발생 가능한 코드
} catch(SomeException e) {
// Nothing here
}
이렇게 되면, 나중에 예외가 발생했을 경우 catch 문 내에서 아무런 작업을 하지 않기 때문에 문제가 어디서 발생했는지 전혀 찾을 수가 없다.
따라서, 개발 표준을 잡을 때 catch문 내에서 어떻게 처리할지를 명시적으로 선언해 두어야만한다.
정리해 보자면
💡 Google에서 “자바 예외 전략”이나 “Java Exception Strategy”로 검색하면 더 많은 자료들을 찾을 수 있다.
Me: try-catch-finally
Me: try
Me: finally
Me: checked exception, error, unchecked exception
Me: error
Me: throw
Me: throws
Me: Throwable의 직계 자손들(Exception, RuntimeException)
💡 책에 있는 내용이 아닙니다.
책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.
unchecked로 만들면 try-catch와 throws를 적지 않아도 된다. 하지만, checked로 만들면 무조건 둘 중 하나는 만들어줘야 한다.
try 안에서 리턴할 경우, 리턴한 값을 복사하는 "값의 복사"가 일어난다. 이렇게 되면 catch나 finally에서 return을 써주더라도, 복사한 try문의 리턴값을 반환한다고 한다.
따라서, catch와 finally문에서 값을 변경한다면 제대로 적용되지 않을 수 있다.
글을 작성하면서 기억을 더듬어 본 건데, 나중에 발견했던 코드를 다시 찾아봐야겠다.
개발하다가 try문 안에서 케이스에 따라 많은 return문을 작성하는 코드를 볼 수 있었다.
상황에 따라 달랐지만, 주로 아래와 같이 if문으로 검사 후에 return하고, 다시 밑에서 if문으로 검사 후에 리턴하는 식이었다.
try {
if (response == null) return 0;
if (response == 1) return 1;
if (response == 2) return 2;
...
} catch {
return 0;
}
이게 if문이 하나 정도 있는거면 몰라도, 이렇게 케이스 별로 확인 후 return 하는 문을 작성하면 가독성이 좋지 않았다.
int a = 0;
try {
if (response == null) a = 0;
if (response == 1) a = 1;
if (response == 2) a = 2;
} catch {
a = 0;
}
return a;
그래서 이렇게 try-catch 밖에다 빼는 것을 선호하는 편이다.
여담으로, finally에서는 return을 선언한 것을 본 적은 없다. 애초에 이렇게 사용할 일이 있을까? 아직 그런 상황은 못 본 것 같다.
만약 유효성 검사를 위 코드처럼 사용한다고 할 때, try-catch 안에서 유효성 검사를 하는 것은 좋은 로직은 아니다.
어쨌거나 코딩하다보면 여러 케이스가 나올 수도 있으니, 상황에 따라 사용하면 될 것 같다.
다만 예상치 못한 반환값이 나올 수 있으니, 위의 try 문에서의 반환 정도만 유의해서 기억하고 있으면 좋지 않을까?
이건 내가 좀 더 다양한 사람의 코드를 보고 공부를 해야겠다.
메소드 내 예외처리(try-catch-finally)에서의 리턴 처리
Error (Java 2 Platform SE 5.0)
에러 표준화 과정 2 - Custom Exception, Exception Handler