본문 바로가기
java

[java] 예외처리 Exception handling

by moonsiri 2020. 10. 11.
728x90
반응형

이 글은 Java의 정석 (남궁성/도우출판) 기반으로 작성되었습니다.
 
 

1. 프로그램 오류

 

컴파일 에러(compile-time error): 컴파일할 때 발생하는 에러

 

런타임 에러(runtime error): 실행할 때 발생하는 에러

  • 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류 (처리 가능한 오류)

 
 
 

2. 예외처리의 정의와 목적

 
에러(error)는 어쩔 수 없지만, 예외(exception)는 처리해야 합니다.
 
예외처리(exception handling)는 프로그램 실행 시 발생할 수 있는 예외의 발생에 대한 코드를 작성하는 것입니다.
예외처리의 목적은 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것입니다.
(에러와 예외는 모두 실행 시(runtime) 발생하는 오류입니다.)
 
 
 

3. 예외처리구문 : try-catch

 
예외를 처리하려면 try-catch문을 사용해야 합니다.
 

try {

} catch (Exception e) {
    // 왠만하면 catch 구문 안에는 logic을 넣지 않고, 보통 log만 찍는다.
    // if문과 달리 try 블럭이나 catch블럭 내에 포함된 문장이 하나라고 해서 괄호{}를 생략할 수는 없다.
} catch (Exception e2) {
    // catch 구문은 많이 쓸 수 있다.
    // catch계층구조가 높을수록 아래로 내려가야 한다.
    // 자식이 처리할 수 있는건 자식이 하고 처리하지 못하는 것은 아래에 둔다. (부모가 처리)
    // 순서가 바뀌면 UnReachableException이 발생한다.
} catch (Exception ignored) {}	// exception을 무시할때 ignored로 표기한다.

 
 
 

4. try-catch문에서의 흐름

 
try블럭 내에서 예외가 발생한 경우,
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인합니다.
2. 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그다음 문장을 계속해서 수행합니다. 만약 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못합니다.

class ExceptionEx5 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);  // 오류 발생
            System.out.println(4);
        } catch (ArithmeticException ae) {
            System.out.println(5);
        }
        System.out.println(6);
    }
}

 
[실행결과]

1
2
3
5
6

 
 
try블럭 내에서 예외가 발생하지 않은 경우,
1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속합니다.

class ExceptionEx4 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(4);
        } catch (Exception e) {
            System.out.println(5);
        }
        System.out.println(6);
    }
}

 
[실행결과]

1
2
3
6

 
 
 

5. 예외 발생시키기

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

Exception e = new Exception(...);

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

throw e;

 
 

class ExceptionEx6 {
    public static void main(String[] args) {
        try {
            Exception e = new Exception("고의로 발생시켰음.");
            throw e;
            // 위 두 줄을 한 줄로 줄여 쓸 수 있다.
            // throw new Exception("고의로 발생시켰음.");
        } catch (Exception e) {
            System.out.println("에러 메시지 : " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("프로그램이 정상 종료되었음.");
    }
}

 
[실행결과]

에러 메시지 : 고의로 발생시켰음.
java.lang.Exception: 고의로 발생시켰음.
	at ExceptionEx6.main(ExceptionEx6.java:4)
프로그램이 정상 종료되었음.

 
 
여기서 잠깐! throw와 throws의 차이를 아는가?
throws는 메소드나 생성자가 수행할 때 발생하는 exception을 선언할 때 사용하고, throw는 강제로 예외를 발생시키는 경우에 사용합니다.

public void doException() throws Exception {
    ...
    if (...) {
        throw new Exception();
    }
    ...
}

 
 
 

6. 예외 클래스의 계층구조

 
 

출처 -  https://finewoo.tistory.com/22

 
 

RuntimeException (Unchecked Exception)

  • 컴파일하는 데는 문제가 없지만 code적인 부분의 에러 임으로 실행하면 문제가 발생합니다.

예) RuntimeException클래스들 중의 하나인 ArithmeticException을 try-catch문으로 처리하는 경우도 있지만, 사실 try-catch문을 사용하기보다는 0으로 나누지 않도록 프로그램을 변경하는 것이 올바른 처리방법입니다.
 

Exception (Checked Exception)

  • Exception 처리 코드 여부를 compiler가 check 합니다.

예) 사용자가 존재하지 않는 파일을 처리하려 한다던지 (FileNotFoundException), 입력한 데이터의 형식이 잘못되었다던가 (DataFormatException)하는 경우에는 반드시 처리를 해주어야 합니다.
 
 

 checked ExceptionUnchecked Exception
처리 여부반드시 예외를 처리해야 함명시적인 처리를 강제하지 않음
확인 시점컴파일 단계실행단계
예외 발생 시 트랜잭션 처리roll-back 하지 않음roll-back 함
대표 예외Exception의 상속받는 하위 클래스 중
RuntimeException을 제외한 모든 예외
- IOException
- SQLException
RuntimeException 하위 예외
- NullPointerException
- IllegalArgumentException
- IndexOutOfBoundException
- ArithmeticException

 
 
 

7. 예외의 발생과 catch블럭

 
try블럭에서 예외가 발생하면, 발생한 예외를 처리할 catch블럭을 찾습니다.
첫 번째 catch블럭부터 순서대로 찾아 내려가며, 일치하는 catch블럭이 없으면 예외는 처리되지 않습니다.
예외의 최고 조상인 Exception을 처리하는 catch블록은 모든 종류의 예외를 처리할 수 있습니다. (반드시 마지막 catch블럭이어야 합니다.)
 

class ExceptionEx11 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);  // ArithmeticException 발생
            System.out.println(4);
        } catch (ArithmeticException ae) {
            if (ae instanceof ArithmeticException) {
                System.out.println("true");
                System.out.println("ArithmeticException");
            }
        } catch (Exception e) {
            System.out.println("Exception");   // ArithmeticException을 제외한 모든 예외가 처리된다.
        }
        System.out.println(6);
    }
}

 
[실행결과]

1
2
3
true
ArithmeticException
6

 
 
발생한 예외 객체를 catch블럭의 참조 변수로 접근할 수 있습니다.

  • printStackTrace() - 예외 발생 당시의 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력합니다.
  • getMessage() - 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있습니다.

 

class ExceptionEx11 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);  // 예외 발생
            System.out.println(4);
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
            System.out.println("예외메시지 : " + ae.getMessage());
        }
        System.out.println(6);
    }
}

 
[실행결과]

1
2
3
java.lang.ArithmeticException: / by zero
	at ExceptionEx11.main(ExceptionEx6.java:17)
예외메시지 : / by zero
6

 
 
개인적으로 추천하지 않는 방식입니다.
개발할 때에는 에러 내용을 보기 위해 사용할 수 있지만, 실제 운영 시에는 catch블럭에 에러가 발생했다는 log만 찍히는 것이 좋습니다.
 
 
 

8. finally 블럭

 
예외 발생 여부와 관계없이 실행되어야 하는 코드를 넣는습니다.
선택적으로 사용할 수 있으며, 예외 발생 시, try-catch-finally의 순서로 실행되고, 예외 미발생시, try-finally의 순서로 실행됩니다.
try 또는 catch블럭에서 return문을 만나도 finally 블럭은 수행됩니다.
 

class FinallyTest {
    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() {
        // 임시파일들을 삭제하는 코드를 적는다.
    }
}

 
위 코드에서 try-catch문을 아래와 같이 try-catch-finally문으로 변경할 수 있습니다.

try {
    startInstall();     // 프로그램 설치에 필요한 준비를 한다.
    copyFiles();        // 파일들을 복사한다.
} catch (Exception e) {
    e.printStackTrace();
} finally {
    deleteTempFiles();  // 프로그램 설치에 사용된 임시파일들을 삭제한다.
}

 
 
 

9. 메서드에 예외 선언하기

 
메서드에 예외를 선언하는 것은 예외를 처리하는 또 다른 방법입니다.
사실 예외를 처리하는 것이 아니라 호출한 메서드로 전달해주는 것이라고 볼 수 있습니다.
 
 

예제 1)

class ExceptionEx18 {
    public static void main(String[] args) throws Exception {
        method1(); // 같은 클래스내의 static 멤버이므로 객체생성없이 직접 호출가능.
    }

    static void method1() throws Exception {
        method2();
    }

    static void method2() throws Exception {
        throw new Exception();
    }
}

 
[실행결과]

Exception in thread "main" java.lang.Exception
    at ExceptionEx18.method2(ExceptionEx18.java:11)
    at ExceptionEx18.method1(ExceptionEx18.java:7)
    at ExceptionEx18.main(ExceptionEx18.java:3)

 
 

 
예외가 발생했을 때, 3개의 메서드 모두(main, method1, method2) 호출 스택에 있었으며, 예외가 발생한 곳은 제일 윗줄에 있는 method2()라는 것과 main메서드가 method1()를, 그리고 method1()은 method2()를 호출했다는 것을 알 수 있습니다.
 
 

예제 2)

class ExceptionEx19 {
    public static void main(String[] args) throws Exception {
        method1(); // 같은 클래스내의 static 멤버이므로 객체생성없이 직접 호출가능.
    }
    
    static void method1() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1 메서드에서 예외가 처리되었습니다.");
            e.printStackTrace();
        }
    }
}

 
[실행결과]

method1 메서드에서 예외가 처리되었습니다.
java.lang.Exception
    at ExceptionEx19.method1(ExceptionEx98.java:8)
    at ExceptionEx19.main(ExceptionEx19.java:3)

 
 

예제 3)

예외가 발생한 메서드 내에서 자체적으로 처리

import java.io.*;

class ExceptionEx21 {
    public static void main(String[] args) throws Exception {
        // command line에서 입력받은 값을 이름으로 갖는 파일을 생성한다.
        File f = creatFile(args[0]);
        System.out.println(f.getName() + "파일이 성공적으로 생성되었습니다.");
    }
    
    static File creatFile(String fileName) {
        try {
            if (fileName == null || fileName.equals("")) {
                throw new Exception("파일이름이 유효하지 않습니다.");
                // 예외가 발생한 createFile메서드 자체 내에서 예외를 처리한다.
            }
        } catch (Exception e) {
            // fileName이 부적절한 경우, 파일 이름을 'untitle.txt'로 한다.
            fileName = "untitle.txt";
        } finally {
            File f = new File(fileName);  // File클래스의 객체를 만든다.
            createNewFile(f);             // 생성된 객체를 이용해서 파일을 생성한다.
            return f;
        }
    }
    
    static void createNewFile(File f) {
        try {
            f.createNewFile();  // 파일을 생성한다.
        } catch(Exception e) { }
    }
}

 
만약 java 실행 시 "오류: 기본 클래스... 을(를) 찾거나 로드할 수 없습니다."라는 오류 문구가 뜬다면 환경변수 설정에 들어가
CLASSPATH를 %JAVA_HOME%\lib가 아닌 %JAVA_HOME%\lib;.;로 변경합니다.
 
 

예제 4)

메서드 호출 시 넘겨받아야 할 값을 다시 받아야 하는 경우(메서드 내에서 자체적으로 해결이 안 되는 경우)

import java.io.File;

class ExceptionEx22 {
    public static void main(String[] args) {
        try {
            File f = creatFile(args[0]);
            System.out.println(f.getName() + "파일이 성공적으로 생성되었습니다.");
        } catch (Exception e) {
        	System.out.println(e.getMessage() + " 다시 입력해 주시기 바랍니다.");
            // createFile메서드를 호출한 메서드(main메서드)에서 처리
        }
    }
    
    static File creatFile(String fileName) throws Exception {
        if (fileName == null || fileName.equals("")) {
            throw new Exception("파일이름이 유효하지 않습니다."); // 예외 발생
        }
        File f = new File(fileName);  // File클래스의 객체를 만든다.
        f.createNewFile();  // 파일을 생성한다.
        return f;
    }
}

 

 
 
 

10. 예외 던지기 (re-throwing)

 
예외 던지기는 예외를 처리한 후에 다시 예외를 생성해서 호출한 메서드로 전달하는 것을 말합니다.
예외가 발생한 메서드와 호출한 메서드, 양쪽에서 예외를 처리해야 하는 경우에 사용합니다.
 

class ExceptionEx23 {
	public static void main(String[] args) {
		try {
			method1();
		} catch (Exception e) {
			System.out.println("main메서드에서 예외가 처리되었습니다.");
		}
	}
	
	static void method1() throws Exception {
		try {
			throw new Exception();
		} catch (Exception e) {
			System.out.println("method1메서드에서 예외가 처리되었습니다.");
			throw e;  // 다시 예외를 발생시킨다.
		}
	}
}

 
[실행결과]

method1메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.

 
 
 

11. 사용자 정의 예외 만들기

 
기존의 예외 클래스를 상속받아서 새로운 예외 클래스를 정의할 수 있습니다.

class MyException extends Exception {
    MyException(String msg) {
        super(msg);  // 조상인 Exception클래스의 생성자를 호출한다.
    }
}

 
 
에러코드를 저장할 수 있게 ERR_CODE와 getErrCode()를 멤버로 추가

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()와 함께 사용될 것이다.
    }
}
class MyExceptionTest {
	public static void main(String[] args) {
		try {
			throw new MyException("에러 메시지 : 사용자정의 예외가 발생되었습니다.", 200);
		} catch (MyException e) {
			System.err.println(e.getErrCode());
			System.err.println(e.getMessage());
		}
	}
}

 
[실행결과]

200
에러 메시지 : 사용자정의 예외가 발생되었습니다.

 
 
 

12. 멀티 캐치 multicatch

 

try {
    // ...
} catch (Exception1 e1) {
    e.printStackTrace();  // 중복코드
} catch (Exception2 e2) {
    e.printStackTrace();  // 중복코드
} catch (Exception e) {
    System.err.println(e.getMessage());
}

 
 
예외가 발생할 때, 동일한 예외처리를 하는 경우가 있습니다.
자바 7에서는 다음과 같이 하나의 catch 블록에서 동시에 여러 예외를 묶어서 처리할 수 있도록 했습니다.
 

try {
    // ...
} catch (Exception1 | Exception2 e) {
    e.printStackTrace();
} catch (Exception e) {
    System.err.println(e.getMessage());
}

 
 
 

13. try-with-resources

 

FileInputStream fis = null;
FileOutputStream fos = null;
try {
    fis = new FileInputStream("");
    fos = new FileOutputStream("");
} catch (IOException e) {

} finally {
    try {
        fis.close();
    } catch (IOException ie) {
		
    }
    try {
        fos.close();
    } catch (IOException ie) {
		
    }
}

 
 
뭔가 자원을 생성하면 해제 코드를 구현을 해줘야 했습니다.
하지만 자바 7에서 try-with-resources라는 특징이 추가되었습니다. try에 자원 객체를 전달하면 finally 블록으로 종료 처리를 하지 않아도 try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능입니다.
 
리소스가 AutoCloseable 인터페이스를 구현하고 있다면, 위 코드를 아래와 같이 작성할 수 있습니다.
 

try (FileInputStream fis = new FileInputStream("");
     FileOutputStream fos = new FileOutputStream("")) {
	 
} catch (IOException e) {

}
// in과 out 모두 자동으로 종료됨

 

728x90
반응형

댓글