몬그로이

자바의 정석 8. 예외처리 본문

Organizing Docs/Java Docs

자바의 정석 8. 예외처리

Mon Groy 2024. 7. 5. 20:00

→Exception 클래스들( checked 예외)

사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외

 

RuntimeException 클래스들( unchecked  예외)

프로그래머의 실수로 발생하는 예외

 

printStackTrace()

예외 발생 당시의 호출스택(Call Stack)에 있었던

메서드의 정보와 예외 메시지를 화면에 출력한다

 

getMessage()

발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다

 

catch(ExceptionA | ExceptionB e) {} : 멀티catch블럭

멀티 catch는 하나의 catch블럭으로 여러 예외를 처리하는 것이기 때문에,

발생한 예외를 멀티 catch블럭으로 처리하게 되었을 때,

멀티 catch블럭 내에서는 실제로 어떤 예외가 발생한 것인지 알 수 없다.

그래서 참조변수 e로 멀티 catch블럭에 '|'기호로 연결된 예외 클래스들의 공통 분모인

조상 예외클래스에 선언된 멤버만 사용할 수 있다

 


예외 발생시키기

 

1. 연산자 new를 이용해서 발생시키려는 예외클래스의 객체를 만든다

Exception e = new Exception("고의적 예외");

 

2. 키워드 throw를 이용해서 예외를 발생시킨다

throw e;

 

* 위의 두 줄을 한 줄로 줄여쓸 수 있다

throw new Exception("고의적 예외 발생");

 


메서드에 예외 선언하기

void method() throw Exception1, Exception2, ... ExceptionN { }

*throw 와 throws 구별하기

 

메서드를 사용하는 쪽에서

어떤 예외가 발생할 가능성이 있는지에 대해 미리 알고

이에 대한 처리를 할 수 있도록 (강요) 하는 것으로

보다 견고한 프로그램 코드를 작성할 수 있도록 돕는다

 

예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라,

자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외를 떠맡기는 것이다

예외를 전달 받은 메서드가 도다시 자신을 호출한 메서드에게 전달할 수 있으며,

이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가

제일 마지막에 있는 main메서드에서도 예외처리가 되지 않으면,

main 메서드마저 종료되어 프로그램 전체가 종료된다

 

메서드에서 발생한 예외를 메인메서드에서 처리하는 예제

public static void main(String[] args) {

      try {

              File f = createFile(args[0]);

              System.out.println(f.getName() + "파일이 성공적으로 생성되었습니다.");

       } catch (Exception e) {

              System.out.println(e.getMessage());

      }

}

 

static File createFile (String fileName) throws Exception {

       if (fileName == null || fileName.equals(""))

             throw new Exception("파일 이름이 유효하지 않습니다.");

       File f = new File(fileName);

       f. createNewFile();

       return f;

}

 

try - catch - finally

try블럭에서 return문이 실행되는 경우에도 finally 블럭의이 실행된다

try블럭에서 예외가 발생하여 catch블럭이 실행된 경우에도 finally 블럭은 실행된다

 

try - with - resources

자동 자원 반환 기능을 가지고 있다

입출력에 주로 사용되는 클래스 중에는 사용한 후 반드시 닫아줘야만 하는 것들이 있다.

그래야 사용했던 자원들이 반환되기 때문이다

  try {

          FileInputStream  fis = new FileInputStream("score.dat");

         DataInputStream  dis = new DataInputStream(fis);

          // 여러 가지

   } catch (IOException ie) {

          ie.printStackTrace();

   } finally {

          dis.close(); //작업중 예외가 발생하더라도 dis 닫히도록 finally 블럭에 넣음

   }

 

하지만 여기서 문제는 close() 가 예외를 발생시킬 수 있다는 점에 있다

따라서 아래와 같이 해야 올바른 것이 된다

  try {

          FileInputStream  fis = new FileInputStream("score.dat");

          DataInputStream  dis = new DataInputStream(fis);

          // 여러 가지

   } catch (IOException ie) {

          ie.printStackTrace();

   } finally {

          try {

                 if (dis != null) {

                 }

                 dis.close(); //작업중 예외가 발생하더라도 dis 닫히도록 finally 블럭에 넣음

          } catch (IOException ie) {

                 ie.printStackTrace();

         }

   }

 

하지만 try 블럭과 finally 블럭에서 모두 예외가 발생하면 try 블럭의 예외는 무시된다

더보기

제일 위의 try 블럭에서 예외 발생 → catch 블럭에서 예외 처리 → finally 블럭 실행

→ finally 블럭의 try블럭실행 중 예외 발생 → finally 블럭의 catch 블럭에서 예외 처리

→ 제일 위에 있는 try 블록에서 발생한 예외는 더 이상 중요하지 않게 됨 (무시된다고 표현)

*첫줄의  try 블럭에서 발생한 예외가 finally 블럭 내부의 try-catch 블럭에서 발생한 예외에 의해 덮어써진다

try-with-resources 문의 괄호 () 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close() 를 호출하지 않아도

try 블럭을 벗어나는 순간 자동적으로 close() 가 호출된다

그 다음 catch 블럭 또는 finally 블럭이 수행된다

   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.printStackTrace();

   }

*try 블럭의 괄호 () 안에 변수를 선언하는 것도 가능하며, 선언된 변수는 try 블럭 내에서만 사용할 수 있다

 

이처럼 try-with-resources 문에 의해 자동으로 객체의 close() 가 호출될 수 있으려면,

클래스가 AutoCloseable 이라는 인터페이스를 구현한 것이어야만 한다

public interface AutoCloseable {

      void close() throws Exception;

}

이 인터페이스는 각 클래스에서 적절히 자원 반환작업을 하도록 구현되어 있다

그런데, close()도 Exception을 발생시킬 수 있다

만일 자동 호출된 close() 에서 예외가 발생한다면?

public static void main(String[] args) {

      try (CloseableResource cr = new CloseableResource()) {

              cr.exceptionWork(false);//"exceptionWork(" + false + ") 가 호출됨"

              // try 블럭의 () 실행으로 close() 작동 -> throw new CloseException("CloseException 발생!!");

       } catch (WorkException e) {

              e.printStackTrace();

       } catch (CloseException e) { //위의 흐름에 의해 catch 블럭 활성화

              e.printStackTrace();

       }

 

       try (CloseableResource cr = new CloseableResource()) {

              cr.exceptionWork(true); //"exceptionWork(" + true + ") 가 호출됨" -> "WorkException 발생!!"

              // try 블럭의 () 실행으로 close() 작동 -> throw new CloseException("CloseException 발생!!");

              // , try 블럭 안애서 가지 exception 발생

       } catch (WorkException e) {//위의 흐름에 의해 catch 블럭 활성화, CloseException Suppressed 작동

              e.printStackTrace();

       } catch (CloseException e) {

              e.printStackTrace();

       }

}

 

class WorkException extends Exception {

       WorkException(String msg) { super(msg); }

}

 

class CloseException extends Exception {

       CloseException(String msg) { super(msg); }

}

 ===============================================

public 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 발생!!");

       }

}

 

출력 결과

exceptionWork(false) 가 호출됨close()가 호출됨exceptionWork(true) 가 호출됨close()가 호출됨

java_jungseok.chapter08.exceptionEx.CloseException: CloseException 발생!!        at java_jungseok.chapter08.exceptionEx.CloseableResource.close(CloseableResource.java:14)        at java_jungseok.chapter08.exceptionEx.TryWithResourceEx.main(TryWithResourceEx.java:9)
java_jungseok.chapter08.exceptionEx.WorkException: WorkException 발생!!        at java_jungseok.chapter08.exceptionEx.CloseableResource.exceptionWork(CloseableResource.java:9)        at java_jungseok.chapter08.exceptionEx.TryWithResourceEx.main(TryWithResourceEx.java:18)        Suppressed: java_jungseok.chapter08.exceptionEx.CloseException: CloseException 발생!!                at java_jungseok.chapter08.exceptionEx.CloseableResource.close(CloseableResource.java:14)                at java_jungseok.chapter08.exceptionEx.TryWithResourceEx.main(TryWithResourceEx.java:17)

 

첫 번째는 일반적인 예외가 발생했을 때와 같은 형태로 출력되었지만,

두 번째는 출력형태가 다르다

 

먼저 exceptionWork() 에서 발생한 예외에 대한 내용이 출력되고,

close() 에서 발생한 예외는 '억제된(suppressed)' 이라는 의미의 머리말과 함께 출력되었다

 

두 예외가 동시에 발생할 수 없기 때문에,

실제 발생한 예외를 WorkException으로 하고,

CloseException은 억제된 예외로 다룬다

 

억제된 예외에 대한 정보는 실제로 발생한 예외인 WorkException 에 저장된다

 

Throwable에는 억제된 예외와 관련된 다음과 같은 메서드가 정의되어 있다

public final synchronized void addSuppressed(Throwable exception) {
  //중략
    suppressedExceptions.add(exception); //억제된 예외를 추가
}

public final synchronized Throwable[] getSuppressed() {
//중략
return suppressedExceptions.toArray(EMPTY_THROWABLE_ARRAY); //억제된 예외(배열) 반환
}

 

만약 기존의 try-catch 문을 사용했다면,

먼저 발생한 WorkException 은 무시되고,

마지막으로 발생한 CloseException에 대한 내용만 출력되었을 것이다


사용자 정의 예외를 만들 수 있다

*가능하면 새로운 예외 클래스를 만들기 보다 기존의 예외 클래스를 활용하자


 

예외 되던지기(exception re-throwing)

 

한 메서드 안에서 발생할 수 있는 예외가 여럿인 경우,

몇 개는 try-catch 문을 통해서 메서드 내에서 자체적으로 처리하고

나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 할 수 있다

 

그리고 단 하나의 예외에 대해서도

예외가 발생한 메서드와 호출한 메서드

양쪽에서 처리하도록 할 수 있다

 

반환값이 없는 메서드에서의 예외 처리는 호출한 main 메서드에서 한다

static void method1() throws Exception {

        try {

                  throw new Exception();

         } catch (Exception e) {

                  System.out.println("method1 메서드에서 예외가 처리되었습니다.");

                  throw e;

         }

}

 

반환값이 있는 method의 경우 try-catch 내에도 return 문이 필요하다

static int method2() {

          try {

                    System.out.println("method2()이 호출되었습니다.");

                    return 0;

          } catch (Exception e) {

                    e.printStackTrace();

                    return 1;

          } finally {

                    System.out.println("method2() 의 finally 블럭이 실행되었습니다.");

          }

}

 

catch 블럭에서 반환값 대신 exception 을 호출한 곳으로 던질 수 있다

static int method3() {

           try {

                      System.out.println("method2()이 호출되었습니다.");

                      return 0;

           } catch (Exception e) {

                      e.printStackTrace();

                      throw new Exception(); //반환값 대신 발생한 exception 되던지기

           } finally {

                      System.out.println("method2() 의 finally 블럭이 실행되었습니다.");

           }

}

 


연결된 예외(chained exception)

 

원인예외(cause exception)

예외 A가 예외 B 를 발생시킨 경우 A 를 B의 원인 예외라고 한다

try {

       startInstall();

                copyFiles();

        } catch (SpaceException e) {

                InstallException ie = new InstallException("설치중 예외 발생"); //예외 생성

                ie.initCause(e); // SpaceExceptionInstallException의 원인예외로 지정 (e를 ie의 원인예외로 지정)

                throw ie; //InstallException을 발생시킴(던짐)

        } catch (MemoryException memoryException) {

                ...

}

*initCause() 는 Exception 클래스의 조상인 Throwable 클래스에 정의되어 있기에 모든 예외에서 가능하다

 

이렇게 하는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이며

이 방법은 조상으로 모든 예외를 묶어서 처리했을 때의 문제점들을 해결하는데 도움이 된다

더보기

조상으로 모든 예외를 묶어서 처리했을 때의 문제점

- 실제로 발생한 예외가 어떤 것인지 알 수 없다

- 한 조상으로 묶어줄 예외들의 상속관계를 변경해 두어야 한다

또 다른 이유는 checked 예외를 unchecked 예외로 바꿀 수 있도록 하기 위해서이다

이렇게 하면 예외처리가 선택적으로 되므로 강제로 예외처리를 할 필요가 없다

 

예제.

MemoryException 은 Exception 의 자손이므로 반드시 예외처리를 해야한다

static void startInstall() throws SpaceException1, MemoryException1 {

if (!enoughSpace()) {

throw new SpaceException1("메모리가 부족합니다.");

}

if (!enoughMemory()) {

throw new MemoryException1("메모리가 부족합니다.");

}

}

 

하지만 RuntimeException 으로 감싸면 unchecked 얘외가 된다

이 때는 더 이상 startInstall 선언부에 MemoryException 을 선언하지 않아도 된다

static void startInstall() throws SpaceException1 {// MemoryException 선언 빠짐

if (!enoughSpace()) {

throw new SpaceException1("메모리가 부족합니다.");

}

if (!enoughMemory()) {

throw new RuntimeException(new MemoryException("메모리가 부족합니다."));

}

}

*initCause 대신 RuntimeException 생성자 사용

**RuntimeException(Throwable cause) : 원인 얘외를 등록하는 생성자