26. 예외처리2

Isaiah IM·2023년 9월 11일
0

java basic

목록 보기
28/38
post-thumbnail

1. 예외 발생시키기

1.1 예외를 고의적으로 발생시키는 이유

프로그램을 만들다 보면 고의적으로 예외를 발생시켜야 할 경우가 존재한다. 예를 들어보자.
우리가 사용자의 정보가 들어있는 파일인 userInfo.txt라는 파일을 읽으려고 한다 가정하자. 그런데 만약 어떠한 이유로 인해 userInfo.txt 파일이 없을 경우에는 고의적으로 예외를 발생시켜 예외처리를 하는 방식으로 파일이 없을 경우를 대비할 수 있다.
이와 같은 방법은 실제 java에서 파일 입출력에서 사용하는 방식이며, 데이터베이스 연결, sql 쿼리응답(데이터베이스를 제어하는 명령어) 등에서 많이 사용한다.
예를들어 사용자 정보가 들어있는 데이터베이스에 접근을 하려고 했는데 통신에 문제가 생겨 응답이 없으면 예외처리를 해서 다시 데이터베이스 접근을 하거나, 데이터베이스에 명령을 내렸는데 응답이 없으면 다시 명령을 내리는등 실제로 예외처리는 이렇게 사용이 된다.

이렇듯 특정 상황에서 응답이 안되거나 형식이 잘못된 경우 고의로 예외를 발생시키고, 이를 예외처리를 하는 방식으로 문제를 해결하는 방식을 많이 사용한다.

1.2 trow

throw는 고의적으로 예외를 발생시키는 것이다.

throw를 사용하면 고의적으로 예외를 발생시킬 수 있다. 고의적으로 예외를 발생시키기 위해서는 아래와 같이 예외 객체를 만들고 발생시켜야 한다.

public class ExceptionStudy {
    public static void main(String[] args){

        try {
            Exception e=new Exception("고의로 예외 발생시킴.");
            throw e;

        } catch (Exception e) {
            System.out.println("예외 발생! 예외 내용: "+e.getMessage());
        }

    }

}

이때 .getMessage()메소드는 예외의 내용을 알 수 있는 메소드이다.

또한, 위 코드는 다음과 같이 단축할 수 있다.

public class ExceptionStudy {
    public static void main(String[] args){

        try {
            throw new Exception("고의로 예외 발생시킴.");

        } catch (Exception e) {
            System.out.println("예외 발생! 예외 내용: "+e.getMessage());
        }

    }

}

1.3 throws

throws는 메소드에서 예외를 처리할때 사용한다.

예외가 발생할 수 있는 코드를 작성할 때 try - catch 블록으로 처리하는 것이 기본이지만, 경우에 따라서는 다른 곳에서 예외를 처리하도록 호출한 곳으로 예외를 떠넘길 수도 있다.
예를 들어보자.

public class ExceptionStudy {
    public static void main(String[] args) {

        try{
            method1();
            method2();
            method3();
        } catch(Exception e){
            System.out.println("예외 발생. 예외 내용: "+e.getMessage());
        }

    }

    public static void method1() {
        try {
            throw new Exception("예외쨩1");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void method2() {
        try {
            throw new Exception("예외쨩2");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void method3() {
        try {
            throw new Exception("예외쨩3");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

위와 같은 코드를 실행하면 다음과 같은 결과가 나온다.

예외쨩1
예외쨩2
예외쨩3

분명 예외가 발생했는데 메소드 내에서 예외처리를 하고, main메소드 내에서는 예외처리를 하지 않는 것을 알 수 있다. 즉, 각 클래스 메소드에서 예외가 처리됬기 때문에 main메소드에서는 예외가 발생하지 않아 main 메소드에서는 catch가 동작하지 않은 것이다.

만약 우리가 데이터베이스를 접근하는 프로그램을 만들때 데이터베이스 접근이 안될경우 다음과 같은 원인을 예상할 수 있다.
1. 단순한 통신오류
2. 데이터베이스 이름, 주소 등 경로 오류

단순한 통신오류일 경우에는 재접속 하면 되지만 데이터베이스의 이름이나 주소를 잘못 입력한 경우 메소드 내에서 예외를 처리하는 것이 아닌 사용자가 직접 데이터베이스 이름, 주소 등의 경로를 수정할 수 있도록 예외를 메인메소드로 보내야 한다. 이를 예외를 떠넘긴다 라고 부른다.(혹은 짬처리 라고 한다.)

이때, 예외를 짬처리 떠넘기기 위해서는 아래와 같이 throws키워드를 사용한다.

public class ExceptionStudy {
    public static void main(String[] args) {

        try{
            method1();
            method2();
            method3();
        } catch(Exception e){
            System.out.println("예외 발생. 예외 내용: "+e.getMessage());
        }

    }

    public static void method1() throws Exception{// 메소드 안에서 try-catch를 안함
        throw new Exception("예외쨩1");
    }

    public static void method2() throws Exception{// 메소드 안에서 try-catch를 안함
        throw new Exception("예외쨩2");
    }

    public static void method3() throws Exception{// 메소드 안에서 try-catch를 안함
        throw new Exception("예외쨩3");
    }
}

output

예외 발생. 예외 내용: 예외쨩1

이때, 메소드에서 어떤 예외가 발생할 수 있는지 미리 관심법으로 예측하는 것이 중요하다.


2. 연결된 예외

2.1 연결된 예외란?

연결된 예외는 한 예외가 다른 예외를 발생시킬 수 있는 기능이다.

이전에 클래스를 상속해 대형성을 이용해 부모클래스로 다룬것 처럼 예외 역시도 부모 예외로 감싸서 예외를 다형성처럼 다룰 수 있다.

예를 들어보자. 우리가 특정한 프로그램을 설치하는 프로그램을 설계한다 가정하자. 이때 만약 설치 공간이 부족합니다.라는 예외를 띄우면 설치하기 전에 공간을 확인하다 설치 공간이 부족함을 인지한것인지, 설치 도중에 더 이상 공간이 없어 설치 공간이 부족하다고 한 것인지 알 수 없을 것이다. 동일한 설치 공간이 부족한 예외임에도 이 둘의 예외처리는 완전히 다를 것이다.
연결된 예외는 이러한 문제를 추적해서 설치 도중에 공간이 부족한것인지, 설치 이전에 공간이 부족함을 인지한건지 알 수 있도록 한다.

2.2 initCause()와 getCause()

예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)라고 한다.

이때, 원인예외로 등록할때는 initCause(cause)를 이용해 원인예외로 등록할 수 있다.
또한, 원인예외를 반환할때는 getCause()를 이용해 원인 예외를 반환할 수 있다.

직접 코드를 작성하면서 알아보자.

public class ExceptionStudy {
    public static void main(String[] args) {
        try {
            install();
        } catch (InstallException e) {
            System.out.println("원인 예외 : " + e.getCause()); // 원인 예외 출력
            e.printStackTrace();
        }
    }

    public static void install() throws InstallException {
        try {
            throw new SpaceException("설치할 공간이 부족합니다."); // SpaceException 발생

        } catch (SpaceException e) {
            InstallException ie = new InstallException("설치중 예외발생"); // 예외 생성
            ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
            throw ie; // InstallException을 발생시켜 상위 메소드로 throws 된다.
        }
    }
}

InstallException

public class InstallException extends Exception {

    InstallException(){

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

}

SpaceException

public class SpaceException extends Exception{

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

결과:

2.3 checked 예외를 unchecked 예외로 변환

예외에는 크게 checked 예외와 unchecked 예외가 있다.
checked 예외는 컴파일 과정에서 예외를 확인하는 것으로, 반드시 try-catch구문으로 예외처리를 해야 하며, 예외처리를 하지 않은 경우 컴파일이 안된다. 대표적으로 파일입출력시 발생하는 IOException으로, 파일 입출력시 반드시 예외처리를 해야 한다.
unchecked 예외는 동작중에 발생할 수 있는 예외로, 예외처리를 강제하지 않아서 예외처리를 하지 않아도 된다. 대표적으로 RuntimeException의 예외인 NullPointerException과 같은 경우가 대표적이다.

초기에는 자바 초보 개발자들도 견고한 코드를 작성할 수 있도록 하기 위해 checked exception이 많았다. 그러나, 현재 컴퓨터 환경이 처음 자바가 만들어진 1990년대와 많이 달라지면서 checked exception으로 분류된 것들이 unchecked exception으로 분류해도 가능한 경우가 있으며, 로직상 runtime exception으로 분류해야 하는 경우가 생기기 때뮨에 연결된 예외를 통해 checked exception을 unchecked exception으로 바꿀 수 있다.

public class ExceptionStudy {
    public static void main(String[] args) {
        install();
    }

    public static void install() {
        throw new RuntimeException(new IOException("설치할 공간이 부족합니다."));// Checked 예외인 IOException을 Unchecked 예외인 RuntimeException으로 감싸 Unchecked 예외로 변환함
    }
}

결과:


3. try-with-resource

3.1 try-with-resource란?

try-with-resource는 파일, DB등 외부 파일 제어시 유용하게 사용할 수 있도록 try-catch 구문을 변형한 것을 말한다.

try-with-resource 에서 resource란 파일, DB등 외부에 있는 데이터들을 의미한다. 즉, resource(리소스)란 데이터를 변수, 배열과 같이 코드 내에서 저장해서 사용하지 않고, 파일, 데이터베이스 등의 코드 외부에서 가져와서 사용하는 데이터를 의미한다.

이러한 리소스는 리소스를 열고, 사용한 다음 리소스를 닫아야 한다. 이때, 외부와 통신을 하면서 리소스를 제어하기 때문에 리소스를 제어하면서 통신오류등 각종 예외가 발생할 수 있다.

직접 코드를 보면서 알아보자.

import java.io.File;
import java.io.FileWriter;

public class ExceptionStudy {
    public static void main(String[] args) {
        File file = new File("D:/test.txt");// 파일 경로
        if(!file.exists()){ // 파일이 존재하지 않으면
            file.createNewFile(); // 신규생성
        }

        FileWriter fw = new FileWriter(file);// 파일 쓰기기능

        fw.write("hello!");// 파일에 hello 씀

        fw.close();// 파일 닫음
    }
}

위 코드를 실행하면 예외처리를 안해서 컴파일 오류가 발생한다.
이제 파일을 제어하는 부분에 예외처리를 하면 다음과 같다.

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class ExceptionStudy {
    public static void main(String[] args) {
        File file = new File("D:/test.txt");// 파일 경로
        FileWriter fw=null;

        try{
            if(!file.exists()){ // 파일이 존재하지 않으면
                file.createNewFile(); // 신규생성
            }

            fw = new FileWriter(file);// 파일 쓰기기능

            fw.write("hello!");// 파일에 hello 씀


        }catch (IOException e){
            System.out.println("i/o 예외 발생!");
            throw new RuntimeException(e);
        }finally {// 예외와 상관없이
            fw.close();// 파일 닫음
        }
    }
}

위 코드 역시 동작하지 않는다. 파일을 닫는 과정에서 통신오류 등의 예외가 발생할 수 있기 때문에 .close()역시 아래와 같이 예외처리를 해야 한다.

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class ExceptionStudy {
    public static void main(String[] args) {
        File file = new File("D:/test.txt");// 파일 경로
        FileWriter fw=null;

        try{
            if(!file.exists()){ // 파일이 존재하지 않으면
                file.createNewFile(); // 신규생성
            }

            fw = new FileWriter(file);// 파일 쓰기기능

            fw.write("hello!");// 파일에 hello 씀

            System.out.println("파일 작성 성공!");
        }catch (IOException e){
            System.out.println("i/o 예외 발생!");
            throw new RuntimeException(e);
        }finally {
            try{
                fw.close();// 파일 닫음
            }catch (IOException e){
                System.out.println("파일 못닫음");
                throw new RuntimeException(e);
            }

        }
    }
}

이렇게 하면 정상적으로 파일이 생성되서 작성이 될 것이다.

위 코드를 보면 그저 파일을 읽고 쓰는것 뿐인데 각종 예외처리로 인해 코드의 가독성이 떨어지는 것을 알 수 있다.

이러한 문제를 해결하는 것이 try-with-resource구문이다. try-with-resource는 기존의 try 구문에 변형을 준 것으로, 파일을 닫는 부분을 쉽게 해결할 수 있다.

public class ExceptionStudy {
    public static void main(String[] args) {
        
        File file = new File("D:/test.txt");// 파일 경로

        try( FileWriter fw = new FileWriter(file) ){// 파일을 열고, 다 사용하면 알아서 닫는다.

            if(!file.exists()){ // 파일이 존재하지 않으면
                file.createNewFile(); // 신규생성
            }

            fw.write("hello!");// 파일에 hello 씀

            System.out.println("파일 작성 성공!");
        }catch (IOException e){
            System.out.println("i/o 예외 발생!");
            throw new RuntimeException(e);
        }
    }
}

위와 같이 try구문에 괄호를 사용하면 파일을 자동으로 닫는 기능을 수행하며, 파일을 닫는 도중 예외가 발생하면 catch 구문에서 예외를 처리한다.

위와 같이 try-with-resource를 사용하기 위해서는 AutoCloseable이라는 인터페이스를 구현받아야 한다.

profile
나는 생각한다. 고로 나는 코딩한다.

0개의 댓글