[ 예외처리 설명 ]
1. printStackTrace()
- 예외 발생 당시의 호출 스텍(Call Stact)에 있었던 메소드의 정보와 예외 메시지를 화면에 출력한다.
2. getMessage()
- 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
3. 멀티 catch 블럭
- JDK1.7부터 여러 catch 블럭을 ' | ' 기호를 이용해서, 하나의 catch 블럭으로 합칠 수 있게 되었으며, 이를 '멀티 catch블럭'이라고 한다. 아래의 코드에서 알 수 있듯이 '멀티 catch블럭'을 이용하면 중복된 코드를 줄일 수 있다. 그리고 ' | ' 기호로 연결할 수 있는 예외 클래스의 개수에는 제한이 없다. (멀티 catch 블럭에 사용되는 ' | ' 는 논리 연산자가 아니라 기호이다.)
(1) 멀티 catch 블럭 합치기 전
try{
...
} catch(ExceptionA e) {
e.printStackTrace();
} catch(ExceptionB e2) {
e2.printStackTrace();
}
(2) 멀티 catch 블럭 합친 후
try{
...
} catch(ExceptionA | ExceptionB e){
e.printStackTrace();
}
(3) 만일 멀티 catch 블럭의 ' | ' 기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다.
try{
...
} catch(ParentException | ChildException e) { // 에러
e.printStackTrace();
}
왜냐하면 두 예외 클래스가 조상과 자손의 관계에 있다면, 그냥 다음과 같이 조상 클래스만 써주는 것과 똑같기 때문이다. 불필요한 코드는 제거하라는 의미에서 에러가 발생하는 것이다.
try{
...
} catch(ParentException e){
e.printStackTrace();
}
(4) 그리고 멀티 catch는 하나의 catch 블럭으로 여러 예외를 처리하는 것이기 때문에 발생한 예외를 멀티 catch 블럭으로 처리하게 되었을 때, 멀티 catch 블럭 내에서는 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조변수 e로 멀티 catch 블럭에 ' | ' 기호로 연결된 예외 클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.
try{
...
} catch(ExceptionA | ExceptionB e) {
e.methodA(); // 에러. ExceptionA에 선언된 methodA()는 호출불가
if(e instanceof ExceptionA) {
ExceptionA e1 = (ExceptionA) e;
e1.methodA(); // OK. ExceptionA에 선언된 메서드 호출가능
} else{ // if(e instanceof ExceptionB)
...
}
e.printStackTrace();
}
필요하다면, 위와 같이 instanceof로 어떤 예외가 발생한 것인지 확인하고 개별적으로 처리할 수는 있다. 그러나 이렇게까지 해가면서 catch 블럭을 합칠 일은 거의 없을 것이다.
(5) 마지막으로 멀티 catch 블럭에 선언된 참조변수 e는 상수이므로 값을 변경할 수 없다는 제약이 있는데, 이것은 여러 catch 블럭이 하나의 참조변수를 공유하기 때문에 생기는 제약으로 실제로 참조변수의 값을 바꿀 일은 없을 것이다. 여러 catch 블럭을 멀티 catch 블럭으로 합치는 경우는 대부분 코드를 간단히 하는 정도의 수준일 것이므로 이러한 제약에 대해 너무 고민하지 않기를 바란다.
4. 메소드 예외 선언
- 메소드에 예외를 선언할 때 일반적으로 RuntimeException 클래스들은 적지 않는다. 이들을 메소드 선언부의 throw에 선언한다고 해서 문제가 되지는 않지만, 보통 반드시 처리 해주어야 하는 예외들만 선언한다.
5. try-with-resource문 (자동 자원 반환)
- JEK1.7부터 try-wirh-resource문이라는 try-catch문의 변현이 새로 추가되었다. 이 구문은 주로 입출력(I/O)와 관련된 클래스를 사용할 때 유용하다. 주로 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아줘야 하는 것들이 있다. 그래야 사용했던 자원(resource)이 반환되기 때문이다.
try{
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
...
} catch(IOException ie) {
ie.printStackTrace();
} finally{
dis.close(); // 작업 중에 예외가 발생하더라도, dis가 닫히도록 finally 블럭에 넣음
}
위의 코드는 DataInputStream을 사용해서 파일로부터 데이터를 읽는 코드인데, 데이터를 읽는 도중에 예외가 발생하더라도 DataInputStream이 닫히도록 finally 블럭 안에 close()를 넣었다. 여기까지는 별 문제가 없어 보이는데, 진짜 문제는 close()가 예외를 발생시킬 수 있다는 데에 있다. 그래서 위의 코드는 아래와 같이 해야 올바른 코드가 된다.
try{
fis = new FileInputStream("score.dat");
dis = new DataInputStream(fis);
...
} catch(IOException ie) {
ie.printStackTrace();
} finally {
try{
if(dis != null)
dis.close();
} catch(IOException ie){
ie.printStackTrace();
}
}
fianlly 블럭 안에 try-catch문을 추가해서 close()에서 발생할 수 있는 예외를 처리하도록 변경했는데, 코드가 복잡해져서 별로 보기에 좋지 않다. 더 나쁜 것은 try블럭과 finally블럭에서 모두 예외가 발생하면 try블럭의 예외는 무시된다는 것이다.
public class ExceptionTest {
public static void main(String[] args) throws Exception{
try {
throw new Exception("aaa");
} catch (Exception e) {
} finally {
throw new Exception("bbb");
}
}
}
Exception in thread "main" java.lang.Exception: bbb
at ExceptionTest.main(ExceptionTest.java:9)
이러한 점을 개선하기 위해서 try-with-resource문이 추가된 것이다. 위의 코드를 try-with-resource문으로 바꾸면 다음과 같다.
// 괄호() 안에 두 문장 이상 넣을 경우 ';'로 구분한다.
try(FileInputStream fis = new FileInputStream("score.dat");
DataInputStream dis = new DataInputStream(fis)) {
while(true){
score = dis.readInt();
System.out.println(score);
sum += score;
}
} catch(EOFException e) {
System.out.println("점수의 총합은 " + sum + "입니다.");
} catch(IOException ie) {
ie.printStckTrace();
}
try-with-resource문의 괄호() 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음에 catch블럭 또는 fianlly 블럭이 수행된다. (try블럭의 괄호() 안에 변수를 선언하는 것도 가능하며, 선언된 변수는 try블럭 내에서만 사용할 수 있다.)
이처럼 try-with-resource문에 의해 자동으로 객체의 close()가 호출될 수 있으려면, 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.
public interface AutoCloseable{
void close() throws Exception;
}
위의 인터페이스는 각 클래스에서 적절히 자원 반환작업을 하도록 구현되어 있다. 그런데, 위의 코드를 잘 보면 close()도 Exception을 발생시킬 수 있다. 만일 자동 호출된 close()에서 예외가 발생하면 어떻게 될까?
class TryWithResourceEx {
public static void main(String args[]) {
try (CloseableResource cr = new CloseableResource()) {
cr.exceptionWork(false); // 예외가 발생하지 않는다.
} catch(WorkException e) {
e.printStackTrace();
} catch(CloseException e) {
e.printStackTrace();
}
System.out.println();
try (CloseableResource cr = new CloseableResource()) {
cr.exceptionWork(true); // 예외가 발생한다.
} catch(WorkException e) {
e.printStackTrace();
} catch(CloseException e) {
e.printStackTrace();
}
} // main의 끝
}
class CloseableResource implements AutoCloseable {
public void exceptionWork(boolean exception) throws WorkException {
System.out.println("exceptionWork("+exception+")가 호출됨");
if(exception)
throw new WorkException("WorkException발생!!!");
}
public void close() throws CloseException {
System.out.println("close()가 호출됨");
throw new CloseException("CloseException발생!!!");
}
}
class WorkException extends Exception {
WorkException(String msg) { super(msg); }
}
class CloseException extends Exception {
CloseException(String msg) { super(msg); }
}
☞ 실행결과
// 실행 결과
exceptionWork(false)가 호출됨
close()가 호출됨
CloseException: CloseException발생!!!
at CloseableResource.close(TryWithResourceEx.java:33)
at TryWithResourceEx.main(TryWithResourceEx.java:6)
exceptionWork(true)가 호출됨
close()가 호출됨
WorkException: WorkException발생!!!
at CloseableResource.exceptionWork(TryWithResourceEx.java:28)
at TryWithResourceEx.main(TryWithResourceEx.java:14)
Suppressed: CloseException: CloseException발생!!!
at CloseableResource.close(TryWithResourceEx.java:33)
at TryWithResourceEx.main(TryWithResourceEx.java:15)
main 메소드에 두 개의 try-catch문이 있는데, 첫 번째 것은 close()에서만 예외를 발생시키고 두 번째 것은 exceptionWork()와 claose()에서 모두 예외를 발생시킨다.
첫 번째는 일반적인 예외가 발생했을 때와 같은 형태로 출력되지만, 두 번째는 출력 형태가 다르다. 먼저 exceptionWork()에서 발생한 예외에 대한 내용이 출력되고, close()에서 발생한 예외는 '억제된(supporessed)'이라는 의미의 머리말과 함께 출력되었다.
두 예외가 동시에 발생할 수는 없기 때문에 실제 발생한 예외를 workException으로 하고, CloseExcepion은 억제된 예외로 다룬다. 억제된 예외에 대한 정보는 실제로 발생한 예외인 WorkException에 저장된다.
Throwable에는 억제된 예외와 관련된 다음과 같은 메소드가 정의되어 있다.
- void addSuppressed(Throwable exception) : 억제된 예외를 추가
- Throwable[] getSuppressed() : 억제된 예외를 반환
만일 기존의 try-catch문을 사용했다면, 먼저 발생한 WorkException은 무시되고, 마지막으로 발생한 CloseException에 대한 내용만 출력되었을 것이다.
※ 참고 ※
기존의 예외 클래스는 주로 Exception을 상속받아서 'checked 예외' 로 작성하는 경우가 많앗지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어가고 있다. 'checked 예외' 는 반드시 예외처리를 해주어야 하기 때문에 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해지기 때문이다.
예외처리를 강제하도록 한 이유는 프로그래밍 경험이 적은 사람들도 보다 견고한 프로그램을 작성할 수 있게 유도하기 위한 것이었는데, 요즘은 java가 탄생하던 20년 전과 달리 프로그래밍 환경이 많이 달라졌다. 그 때 java를 설계하던 사람들은 자바가 주로 소형 가전기기나 데스크탑에서 실행될 것이라고 생각했지만 현재 java는 모바일이나 웹 프로그래밍에서 주로 쓰이고 있다.
이처럼 프로그래밍 환경이 달라진만큼 필수적으로 처리해야만 할 것 같았던 예외들이 선택적으로 처리해도 되는 상황으로 바뀌는 경우가 종종 발생하고 있다. 그래서 필요에 따라 예외처리의 여부를 선택할 수 있는 'unchecked 예외'가 강제적인 'checked 예외'보다 더 환영받고 있다.
6. 사용자 정의 예외 클래스
class NewExceptionTest {
public static void main(String args[]) {
try {
startInstall(); // 프로그램 설치에 필요한 준비를 한다.
copyFiles(); // 파일들을 복사한다.
} catch (SpaceException e) {
System.out.println("에러 메시지 : " + e.getMessage());
e.printStackTrace();
System.out.println("공간을 확보한 후에 다시 설치하시기 바랍니다.");
} catch (MemoryException me) {
System.out.println("에러 메시지 : " + me.getMessage());
me.printStackTrace();
System.gc(); // Garbage Collection을 수행하여 메모리를 늘려준다.
System.out.println("다시 설치를 시도하세요.");
} finally {
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일들을 삭제한다.
} // try의 끝
} // main의 끝
static void startInstall() throws SpaceException, MemoryException {
if(!enoughSpace()) // 충분한 설치 공간이 없으면...
throw new SpaceException("설치할 공간이 부족합니다.");
if (!enoughMemory()) // 충분한 메모리가 없으면...
throw new MemoryException("메모리가 부족합니다.");
} // startInstall메서드의 끝
static void copyFiles() { /* 파일들을 복사하는 코드를 적는다. */ }
static void deleteTempFiles() { /* 임시파일들을 삭제하는 코드를 적는다.*/}
static boolean enoughSpace() {
// 설치하는데 필요한 공간이 있는지 확인하는 코드를 적는다.
return false;
}
static boolean enoughMemory() {
// 설치하는데 필요한 메모리공간이 있는지 확인하는 코드를 적는다.
return true;
}
} // ExceptionTest클래스의 끝
class SpaceException extends Exception {
SpaceException(String msg) {
super(msg);
}
}
class MemoryException extends Exception {
MemoryException(String msg) {
super(msg);
}
}
// 실행 결과
에러 메시지 : 설치할 공간이 부족합니다.
SpaceException: 설치할 공간이 부족합니다.
at NewExceptionTest.startInstall(NewExceptionTest.java:22)
at NewExceptionTest.main(NewExceptionTest.java:4)
공간을 확보한 후에 다시 설치하시기 바랍니다.
7. 예외 되던지기 (exception re-throwing)
한 메소드에서 발생할 수 있는 예외가 여럿인 경우 몇 개는 try-catch문을 통해서 메소드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메소드에서 처리하도록 함으로써 양쪽에서 나눠서 처리되도록 할 수 있다.
그리고 심지어는 단 하나의 예외에 대해서도 예외가 발생한 메소드와 호출한 메소드, 양쪽에서 처리하도록 할 수 있다.
이것은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 '예외 되던지기(exception re-throwing)' 이라고 한다.
1. 예외의 발생 가능성이 있는 메소드에서 try-catch문을 사용해서 예외를 처리해준다.
2. catch문에서 필요한 작업을 실행한 후에 throw문을 사용해서 예외를 다시 발생시킨다.
3. 다시 발생한 예외는 이 메소드를 호출한 메소드에게 전달되고
4. 호출한 메소드의 try-catch문에서 예외를 또 다시 처리한다.
이 방법은 하나의 예외에 대해서 예외가 발생한 메소드와 이를 호출한 메소드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다.
이 때, 주의할 점은 예외가 발생할 메소드에서는 try-catch문을 사용해서 예외처리를 해줌과 동시에 메소드의 선언부에 발생할 예외를 throws에 지정해줘야 한다는 것이다.
class ExceptionEx17 {
public static void main(String[] args)
{
try {
method1();
} catch (Exception e) {
System.out.println("main메서드에서 예외가 처리되었습니다.");
}
} // main메서드의 끝
static void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println("method1메서드에서 예외가 처리되었습니다.");
throw e; // 다시 예외를 발생시킨다.
}
} // method1메서드의 끝
}
// 실행결과
method1메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.
8. 연결된 예외(chained exception)
한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다. 아래의 코드는 SpaceException을 원인 예외로 하는 InstallException을 발생시키는 방법을 보여준다.
try{
startInstall(); // SpaceException 발생
copyFiles();
} catch(SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
throw ie; // InstallException을 발생시킨다.
} catch(MemoryException me) {
...
먼저 InstallException을 생성한 후에 initCause()로 SpaceException을 InstallException의 원인 예외로 등록한다. 그리고 'throw'로 이 예외를 던진다.
initCause()는 Exception 클래스의 조상인 Throwable 클래스에 정의되어 있기 때문에 모든 예외에서 사용이 가능하다.
- Throwable initCause(Throwable cause) : 지정한 예외를 원인 예외로 등록
- Throwable getCause() : 원인 예외를 반환
발생한 예외를 그냥 처리하면 될텐데 원인 예외로 등록해서 다시 예외를 발생시키는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함이다.
그렇다고 아래와 같이 InstallException을 SpaceException과 MemoryException의 조상으로 해서 catch블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 생긴다. 그리고 SpaceExceoption과 MemoryException의 상속관계를 변경해야 한다는 것도 부담이다.
try{
startInstall(); // SpaceException 발생
copyFiles();
} catch(InstallException e) { // InstallException 은
e.printStackTrace(); // SpaceException과 MemoryException의 조상
}
그래서 생각한 것이 예외가 원인 예외를 포함할 수 있게 하는 것이다. 이렇게 하면 두 예외는 상속관계가 아니어도 상관이 없어진다.
public class Throwable implements Serializable {
...
private Throwable cause = this; // 객체 자신(this)을 원인 예외로 등록
...
}
또 다른 이유는 checked 예외를 inchecked 예외로 바꿀 수 있도록 하기 위해서다.
checked 예외로 예외처리를 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것이었는데, 지금은 java가 처음 개발되던 1990년대와 컴퓨터 환경이 많이 달라졌다. 그래서 checked 예외가 발생해도 예외를 처리할 수 없는 상황이 하나둘 발생하기 시작했다.
이럴 때 할 수 있는 일이라곤 그저 의미없는 try-catch문을 추가하는 것뿐인데, checked 예외를 unchecked 예외로 바꾸면 예외처리가 선택적이게 되므로 억지로 예외처리를 하지 않아도 된다.
> 변경 전
static void startInstall() throws SpaceException, MemoryException {
if(!enoughSpace() ) // 충분한 설치 공간이 없으면 ...
throw new SpaceException("설치할 공간이 부족합니다.");
if(!enoughMemory())
throw new MemoryException("메모리가 부족합니다.");
}
> 변경 후
static void startInstall() throws SpaceException{
if(!enoughSpace()) // 충분한 설치 공간이 없으면 ...
throw new SpaceException("설치할 공간이 부족합니다.");
if(!enougMemory())
throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
} // startInstall 메서드의 끝
MemoryException은 Exception의 자손이므로 반드시 예외를 처리해야 하는데, 이 예외를 RuntimeException으로 감싸버렸기 때문에 unchecked 예외가 되었다. 그래서 더 이상 startInstall()의 선언부에 MemoryException을 선언하지 않아도 된다. 참고로 위의 코드에서는 initCause() 대신 RuntimeException의 생성자를 사용했다.
- RuntimeException(Throwable cause) : 원인 예외를 등록하는 생성자
class ChainedExceptionEx {
public static void main(String args[]) {
try {
install();
} catch(InstallException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
} // main의 끝
static void install() throws InstallException {
try {
startInstall(); // 프로그램 설치에 필요한 준비를 한다.
copyFiles(); // 파일들을 복사한다.
} catch (SpaceException e) {
InstallException ie = new InstallException("설치중 예외발생");
ie.initCause(e);
throw ie;
} catch (MemoryException me) {
InstallException ie = new InstallException("설치중 예외발생");
ie.initCause(me);
throw ie;
} finally {
deleteTempFiles(); // 프로그램 설치에 사용된 임시파일들을 삭제한다.
} // try의 끝
}
static void startInstall() throws SpaceException, MemoryException {
if(!enoughSpace()) { // 충분한 설치 공간이 없으면...
throw new SpaceException("설치할 공간이 부족합니다.");
}
if (!enoughMemory()) { // 충분한 메모리가 없으면...
throw new MemoryException("메모리가 부족합니다.");
// throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}
} // startInstall메서드의 끝
static void copyFiles() { /* 파일들을 복사하는 코드를 적는다. */ }
static void deleteTempFiles() { /* 임시파일들을 삭제하는 코드를 적는다.*/}
static boolean enoughSpace() {
// 설치하는데 필요한 공간이 있는지 확인하는 코드를 적는다.
return false;
}
static boolean enoughMemory() {
// 설치하는데 필요한 메모리공간이 있는지 확인하는 코드를 적는다.
return true;
}
} // ExceptionTest클래스의 끝
class InstallException extends Exception {
InstallException(String msg) {
super(msg);
}
}
class SpaceException extends Exception {
SpaceException(String msg) {
super(msg);
}
}
class MemoryException extends Exception {
MemoryException(String msg) {
super(msg);
}
}
// 실행결과
InstallException: 설치중 예외발생
at ChainedExceptionEx.install(ChainedExceptionEx.java:17)
at ChainedExceptionEx.main(ChainedExceptionEx.java:4)
Caused by: SpaceException: 설치할 공간이 부족합니다.
at ChainedExceptionEx.startInstall(ChainedExceptionEx.java:31)
at ChainedExceptionEx.install(ChainedExceptionEx.java:14)
... 1 more
'Java > Day22' 카테고리의 다른 글
[Java] MenuException (0) | 2021.11.28 |
---|---|
[Java] 단계별 프로젝트 - 전화번호 관리 프로그램 06단계 (0) | 2021.11.28 |
[Java] 예외처리 문제 (0) | 2021.11.28 |