언어/자바의 정석

[08-2] finally, try-with-resources, 사용자정의예외, 예외 던지기

chan10 2021. 2. 17. 10:40

finally블럭

o  finally블록은 예외의 발생 여부에 상관없이 반드시 실행 되어야할 코드를 포함시킬 목적으로 사용된다.

o  try-catch문에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally 순으로 구성된다.

 

public class Test {    
    public static void main(String[] args) {
        try {
            // 예외 발생 가능성이 있는 문장을 넣는다.
        } catch{
            // 예외 처리를 위한 문장을 적는다.
        } finally {
            // 예외에 발생여부에 관계없이 항상 수행 되어야하는 문장들을 넣는다.
            // finally블럭은 try-catch문의 맨 마지막에 위치한다.
        }
    }
}

예외 발생 시 실행 순서 : try -> catch -> finally

예외 발생하지 않을 시 실행 순서 : try -> finally

 

public static void main(String[] args) {
    try {
    startInstall();    // 프로그램 설치 준비
        copyFiles();    // 파일 복사
        deleteTempFiles();// 파일 제거
    } catch(Exception e) {
        e.printStackTrace();
        deleteTempFiles();// 파일 제거
    }
}
static void startInstall() {}
static void copyFiles() {}
static void deleteTempFiles() {}
public static void main(String[] args) {
    try {
    startInstall(); // 프로그램 설치 준비
        copyFiles();    // 파일 복사
    } catch(Exception e) {
        e.printStackTrace();
    } finally {
        deleteTempFiles();//파일 제거
    }
}
static void startInstall() {}
static void copyFiles() {}
static void deleteTempFiles() {}

- 왼쪽 예제는 예외에 관계없이 deleteTempFiles()를 사용해야 한다고 할 경우 try{ }, catch{ }에 모두 작성해야 한다.

- 그래서 finally 블록을 사용하여 오른쪽 예제와 같이 사용할 수 있다.

 

public static void main(String[] args) {
    method1();
    System.out.println("mehtod1()의 수행을 마치고 main메서드로 돌아왔습니다.");    
}
static void method1() {
    try {
        System.out.println("method1()이 호출되었습니다.");
        return;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {    //try의 return문이 있어도 finally 실행 후 return
        System.out.println("method1(0의 finally블럭이 실행되었습니다.");
    }
}

- try블록에서 return문이 실행되는 경우에도 finally블록이 먼저 실행된 후에 현재 실행중인 메서드를 종료한다.

-  마찬가지로 catch블록이 수행 중에 return을 만나도 finally블록의 문장들은 수행된다.

 

 

자동 자원 반환 – try-with-resources

o try-with-resources선언된 객체들에 대해서 try가 종료될 때 자동으로 자원을 해제해주는 기능이다.

o  try-with-resources는 JDK1.7부터 새로 추가되었으며 ‘입출력(I/O)’과 관련된 클래스를 사용할 때 유용하다.

public static void main(String[] args) throws Exception {
   
    try {
        throw new RuntimeException("in try");
    } finally {
        throw new RuntimeException("in finally");
    }
}

- 만약 try문에서 예외가 발생하였고 finally에서도 예외가 발생하였다면 try문에서 발생한 예외는 무시된다.

- 이는 메서드가 예외 처리하지 못하고 죽으면 호출한 메서드가 예외 처리를 해야 하는데 예외가 두개면 처리할 수 없기 때문이다. try-catch메커니즘의 한계라고 보면 된다.

- 이러한 점을 개선하기 위해 try-catch-resources문이 추가된 것이다.

 

public static void main(String[] args) {
    FileInputStream fis = null;
    DataInputStream dis = null;
    
    try {
        fis = new FileInputStream("score.dat");
        dis = new DataInputStream(fis);
    } catch (IOException ie) {
        ie.printStackTrace();
    } finally {
        try {
            if(dis!=null)
        //작업 중 예외가 발생하더라도, dis가 닫히도록 finally블럭에 넣는다
        // 그러나 close( )가 예외를 발생시킬 수도 있기에 다시 try문으로 처리한다. 
                dis.close();
        } catch(IOException ie) {
            ie.printStackTrace();
        }
    }
}//main 끝
public static void main(String[] args) {    
    int score,sum=0;
    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();
    }
}//main 끝

- try-with-resources문의 괄호( )안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close( )를 호출하지 않아도 try블록을 벗어나는 순간 자동으로 close( )가 호출되기에 finally에서 명시적으로 close( )를 호출해줄 필요가 없다. 그 다음에 catch블럭 또는 finally블록이 수행된다.

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

 

o  이처럼 try-with-resources문에 의해 자동으로 객체의 close( )가 호출될 수 있으려면, 클래스가 AutoCloseable이라는 터페이스를 구현한 것이어야만 한다. AutoCloseable은 JDK1.7부터 지원한다.

 

사용자정의 예외 만들기

o  기존에 정의된 예외 클래스 외에 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수도 있다. 보통 Exception클래스 또는 RuntimeException클래스로부터 상속받아 클래스를 만들지만, 필요에 따라 예외 클래스를 선택할 수 있다.

class MyException extends Exception {
    MyException(String msg) {    //문자열을 매개변수로 받는 생성자
        super(msg);    //조상인 Exception클래스의 생성자를 호출한다.
    }
}

- Exception클래스는 생성 시 String값을 받아서 메시지로 저장할 수 있다. String을 매개변수로 추가해주어 사용자 정의 예외 클래스도 메시지를 저장할 수 있도록 했다.

 

public class Test {    
    public static void main(String[] args) {    
        try {
            throw new MyException("예외",200); //MyException 예외 발생
        } catch(MyException e) {
            System.out.println("MyException 예외 발생");
            System.out.println(e.getMessage()+" "+e.getErrCode());
            e.printStackTrace();
        }
        
    }//main 끝
}//클래스 끝
 
class MyException extends Exception {
    private final int ERR_CODE;
        
    MyException(String msg, int errCode) {    //생성자
        super(msg);
        ERR_CODE = errCode;
    }
    
    MyException(String msg) {    //매개변수 있는 생성자
        this(msg, 100);    //ERR_CODE를 100(기본값)으로 초기화한다.
    }
    
    public int getErrCode() {    //에러 코드를 얻을 수 있는 메서드도 추가
        return ERR_CODE;    // 주로 getMessage() 함께 사용될 것이다.
    }
}

- 이전의 코드에서 메시지뿐만 아니라 에러 코드 값도 저장할 수 있도록 멤버 변수와 메서드도 멤버로 추가했다.

- catch블록에서 getMessage()와 getErrCode()를 사용해서 에러코드와 메시지를 모두 얻을 수 있다.

 

public static void main(String[] args) {    
    try {
        startInstall();
        secondInstall();
        copyFile();
        System.out.println("try");
    } catch (SpaceException e) {
        System.out.println("에러 메시지 : "+e.getMessage());
        e.printStackTrace();
        System.out.println("설치 공간을 확보 후 다시하시길 바랍니다.");
    } catch (MemoryException me) {
        System.out.println("에러 메시지 : "+me.getMessage());
        me.printStackTrace();
        System.out.println("메모리 공간을 확보 후 다시하시길 바랍니다.");
    }
}//main 끝
 
static void startInstall() throws SpaceException, MemoryException {
    if(!enoughSpace()) throw new SpaceException("설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new MemoryException("설치 메모리가 부족합니다.");
}
static void secondInstall() throws SpaceException, MemoryException {
    if(!enoughSpace()) throw new SpaceException("second 설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new MemoryException("second 설치 메모리가 부족합니다.");
}
 
static void copyFile() {/*파일 복사 코드*/}
static void deleteTempFiles() {/*임시파일 삭제 코드*/ }
static boolean enoughSpace() {
    //설치하는데 필요한 공간이 있는지 확인하는 코드 작성
    return false;
}
static boolean enoughMemory() {
    // 설치 시 필요한 메모리 공간이 있는지 확인하는 코드 작성
    return false;
}
}//Test클래스 끝
}
//설치 공간이 충분하지 않을 경우 발생하는 예외 클래스
class SpaceException extends Exception {
    SpaceException(String msg) {
        super(msg);
    }
}
//메모리 공간이 충분하지 않을 경우 발생하는 예외 클래스
class MemoryException extends Exception {
    MemoryException(String msg) {
        super(msg);
    }
}

- 위 예제는 프로그램 설치 시 예외 발생의 경우를 가정한 예시다. 새롭게 정의한 SpaceException, MemoryException 예외 클래스를 만들어 사용했으며 startInstall( ), secondInstall( ) 메서드에서 파일 설치 시 공간 또는 메모르가 부족한 경우 해당 예외를 발생시키도록 했다.

- startInstall( )메서드 실행 후 예외가 없으면 secondInstall( )을 실행하도록 했으나 startInstall( )에서 예외가 발생하도록 작성되었다(enoughSpace, enoughMemory의 return false).

- 예외 발생 시 적절한 catch(-finally)문을 찾아 실행 후 해당 try-catch문을 빠져나가기에(참고) startInstall( )메서드만 실행 후 try-catch문을 종료하여 다음 secondInstall( )은 실행을 하지 못한다.

- tartInstall( )에서 두 개의 if문은 모두 예외를 발생시키도록 되었으나 예외가 발생하였으나 처리가 되지 않을 경우 해당 메서드는 종료하여 호출한 메서드로 돌아가기에 두 번째 예외는 발생하지 않았다.

 

static void startInstall() { //예외를 직접 처리한다.
try {
    if(!enoughSpace()) throw new SpaceException("설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new MemoryException("설치 메모리가 부족합니다.");
catch (SpaceException e){
    System.out.println(e.getMessage());
catch (MemoryException e){
    System.out.println(e.getMessage());
}    
}
static void secondInstall() throws SpaceException, MemoryException {
    if(!enoughSpace()) throw new SpaceException("second 설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new MemoryException("second 설치 메모리가 부족합니다.");
}

- 만약 이전의 예외에서 startInstall( )에서 발생한 예외를 직접 처리하도록 코드를 수정해보았다. startInstall( )의 첫번째 if문인 if(!enoughSpace())에서 예외가 발생하여 적절한 catch문을 찾아 ‘설치 공간이 부족합니다’를 출력하였다. 그리고 해당 try-catch문을 빠져나오기에 두번째 if문은 실행되지 않았다.

- startInstall( )에서 예외를 직접 처리하였기에 startInstall( )를 호출한 main메서드는 예외가 발생한 사실을 모른다.

- 따라서 main메서드의 try-catch문 빠져나가지 않고 다음 문장인 secondInstall( )문장을 실행하였다.

 

예외 던지기(exception re-throwing)

o  한 메서드에서 발생할 수 있는 예외가 여러 개인 경우 try-catch문을 통해 해당 메서드에서 직접 처리하거나 선언부에 지정(throws)하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 처리되도록 할 수 있다.

o  심지어 단 하나의 예외에 대해서도 양쪽에서 처리되도록 할 수 있는데 이를 ‘예외 되던지기(exception re-throwing)’이라고 한다.

o  먼저 예외가 발생할 가능성이 있는 문장을 try-catch문에서 처리해준다. catch문에서 예외를 처리한 후에 throw문을 통해 예외를 다시 발생시킨다. 다시 발생한 예외는 호출한 메서드에서 처리할 수 있도록 메서드 선언부에 throws를 작성하여야 한다.

o  호출한 메서드에서도 전달받은 예외를 try-catch문으로 처리함으로써 양쪽에서 예외를 처리하게 할 수 있으며 하나의 예외에 대해 예외가 발생한 메서드, 호출한 메서드 양쪽에서 처리해줘야 할 작업이 있을 때 사용된다.

 

public class Test {    
    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; //다시 예외를 발생시킨다.
        }
    }
}//Test클래스 끝

- method1()과 main메서드 양쪽에 catch블록이 수행되었음을 알 수 있다. methdo1()의 catch블록에서 예외를 처리하고 throw를 통해 예외를 다시 발생시켰다.

- 그리고 이 예외를 호출한 main메서드로 넘겨줌으로써 main메서드의 try-catch문에서도 처리되었다.

 

public class Test {    
    public static void main(String[] args) {    
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main메서드에서 예외 처리");
        }
    }//main 끝
    
    static int method1() throws Exception {        
        try {
            System.out.println("method1()이 호출되었습니다.");
            System.out.println(0/0); //예외 발생 가능성 구문 작성
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            return 1;
        } finally {
            System.out.println("method1()의 finally블럭이 실행되었습니다.");
        }
    }
}//Test클래스 끝

- 반환 값이 있는 return의 경우, catch블록에도 return문이 있어야한다. 예외가 발생했을 경우에도 값을 반환해야 하기 때문이다.

 

public class Test {    
    public static void main(String[] args) {    
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main메서드에서 예외 처리");
        }
    }//main 끝
    
    static int method1() throws Exception {        
        try {
            System.out.println("method1()이 호출되었습니다.");
            System.out.println(0/0); //예외 발생 가능성 구문 작성
            return 0;
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception();  //return문 대신 예외를 호출한 메서드로 전달
        } finally {
            System.out.println("method1()의 finally블럭이 실행되었습니다.");
        }
    }
}//Test클래스 끝

- catch블록에서 예외 되던지기를 해서 호출한 메서드로 예외를 전달하면, return문이 없어도 된다.

※ finally블럭 내에도 return문을 사용할 수 있으며, try블록이나 catch블록의 return문 다음에 수행된다. 최종적으로 finally블록 내의 return문의 값이 반환된다.

 

연결된 예외(chained exception)

원인 예외(cause exception)란 한 예외가 다른 예외를 발생시키는 것을 말하는데 예를 들어 예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외(cause exception)라고 한다.

catch문에서 발생시킬 다른 예외를 생성catch조건의 예외를 원인 예외로 지정(initCause)한다. throw를 통해 새로 생성한 예외를 발생시킨다. 새로운 예외를 발생시켜 호출한 메서드로 전달하기에 메서드 선언부에 ‘throws [예외]’가 등록이 되어있어야 한다.

public static void main(String[] args) throws InstallException{    
    try {
        startInstall();    //SpaceException 발생
        copyFile();
    } catch (SpaceException e) {
        InstallException ie = new InstallException("설치 중 예외발생");//예외 생성
        ie.initCause(e);//InstallException의 원인 예외를 SpaceException로 지정 
        throw ie; //InstallException을 발생시킨다.
    } catch (MemoryException me) {
        InstallException ie = new InstallException("설치 중 예외발생");
        ie.initCause(me);
        throw ie;
    }
}//main 끝
Throwable initCause(Throwable cause) : 지정한 예외를 원인 예외로 등록
Throwable getCause() : 원인 예외를 반환

- initCause을 통해 예외의 원인을 지정된 값(참조변수 e)으로 초기화 함으로써 SpaceException인스턴스의 정보가 저장되었다.

 

o  발생한 예외를 처리하지 않고 원인 예외로 등록해서 처리하는 이유여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

o  만약 InstallException을 SpaceException과 MemoryException의 조상으로 해서 catch블록을 작성하면, 실제로 발생한 예외가 어떤 것 인지 알 수 없는 문제가 있으며 InstallExceptio과 SpaceException의 상속관계를 변경해야 하는 부담도 있다.

o  그러나 예외가 원인 예외를 포함할 수 있게 하면 두 예외는 상속관계가 아니어도 상관없다.

try {
    startInstall();    //SpaceException 발생
    copyFile();
}
    //InstallException클래스는 SpaceException의 조상 
    catch (InstallException e) { }    // 조상클래스인 InstallException가 선언되었기에
//    catch (SpaceException e) { }    // 자손 클래스 선언 시 에러 발생

 

o  원인 예외를 사용하는 또 다른 이유는 checked예외를 unchecked예외로 바꿀 수 있도록 하기 위해서다. 자바가 처음 개발되던 때와 컴퓨터 환경이 많이 달라져서 checked예외가 발생해도 처리할 수 없는 상황들이 하나씩 발생하기 시작했다. 이럴 때 의미 없는 try-catch문을 추가하는 것뿐이다. 그러나 check예외를 uncheck예외로 바꾸면 예외 처리가 선택적이 되기에 억지로 예외처리를 하지 않아도 된다.

checked 예외
static void startInstall() throws SpaceException, MemoryException {
    if(!enoughSpace()) throw new SpaceException("설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new MemoryException("설치 메모리가 부족합니다.");
}
unchecked 예외
static void startInstall() throws SpaceException{
    if(!enoughSpace()) throw new SpaceException("설치 공간이 부족합니다.");
    if(!enoughMemory()) throw new RuntimeException(new MemoryException("메모리가 부족합니다.")); //원인 예외 등록 생성자
}
 
cs

- MemoryException은 Exception의 자손이므로 반드시 예외 처리를 해야 하는데 RuntimeException으로 감싸버렸기에 unchecked예외가 되었다.

- 그래서 startInstall( ) 매서드 선언부의 throws에 선언하지 않아도 된다. initCause( )대신 RuntimeException의 생성자를 사용했다. [RuntimeException (Throwable cause)  //원인 예외를 등록하는 생성자]

 

package codingTest;
 
public class Test {    
    public static void main(String[] args) throws InstallException{    
        try {
            install();    //InstallException 전달 받음
            copyFiles();
        } catch (InstallException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }//main 끝
    
    static void install() throws InstallException {
        try {
            startInstall(); //프로그램 설치 준비
            copyFiles(); //파일 복사
        } catch (SpaceException se) {
            InstallException ie = new InstallException("설치 중 예외발생");
            ie.initCause(se);
            throw ie;
        } catch (MemoryException me) {
            InstallException ie = new InstallException("설치 중 예외발생");
            ie.initCause(me);
            throw ie;
        } finally {
            deleteTempFiles(); //프로그램 설치에 사용된 임시파일 삭제
        }
    }
    
    static void startInstall() throws SpaceException, MemoryException {
        if(!enoughSpace()) throw new SpaceException("설치 공간이 부족합니다.");
        if(!enoughMemory()) throw new MemoryException("설치 메모리가 부족합니다.");
//        if(!enoughMemory()) throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
    }
    
    static void copyFiles() {/*파일 복사 코드*/}
    static void deleteTempFiles() {/*임시파일 삭제 코드*/ }
    static boolean enoughSpace() {
        //설치하는데 필요한 공간이 있는지 확인하는 코드 작성
        return false;
    }
    static boolean enoughMemory() {
        // 설치 시 필요한 메모리 공간이 있는지 확인하는 코드 작성
        return false;
    }
    
    }//Test클래스 끝
 
    //설치 공간이 충분하지 않을 경우 발생하는 예외 클래스
    class SpaceException extends Exception {
        SpaceException(String msg) {
            super(msg);
        }
    }
    //메모리 공간이 충분하지 않을 경우 발생하는 예외 클래스
    class MemoryException extends Exception {
        MemoryException(String msg) {
            super(msg);
        }
    }
    
    class InstallException extends Exception {        
        InstallException(String msg) {
            super(msg);
        }
    }