Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

예외 처리(exception handling) 본문

Programming Language/JAVA

예외 처리(exception handling)

scarecrow1992 2021. 3. 22. 00:15

1. 프로그램 오류

  • 에러는 프로그램의 오작동, 비정상적인 종료를 야기한다.
    • 컴파일 에러 : 컴파일 시 발생하는 에러, 자바 컴파일러가 감지한다.
    • 런타임 에러 : 프로그램의 실행도중 발생하는 에러, JVM이 감지한다.
  • JAVA에서는 런타임 에러를 에러(error)예외(exception) 두 가지로 구분하였다.
    • 에러
      • 메모리 부족(OtOfMemoryError)나 스택오버플로우(StackOverflowError) 처럼 발생하면 복구할 수 없는 심각한 오류
      • 비정상적인 종료를 막을 수 없다.
    • 예외
      • 발생하더라도 수습가능한 비교적 덜 심각한 오류
      • 이를 대비할 수 있는 코드를 미리 작성하여 비정상적인 오류를 막을 수 있다.

※ 컴파일 에러도 예외처리가 가능하다.

 

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

  • 정의 : 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
  • 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

 

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

  • 구조
1
2
3
4
5
6
7
8
9
10
try {
    // 예외가 발생할 가능성이 있는 문장들을 넣는다.
catch (Exception1 e1) {
    // Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
catch (Exception2 e2) {
    // Exception2가 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
...
catch (ExceptionN eN) {
    // ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}
cs
  • 하나의 try블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있다.
  • 여러개의 catch 블럭 중 예외의 종류와 일치하는 단 한개의 블럭만 수행된다.
  • 예외의 종류와 일치하는 catch 블럭이 없으면 예외처리는 되지 않는다.

※ if문과 달리, try와 catch 블럭은 문장이 하나라고 괄호{}를 생략할 수 없다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Hello {
    public static void main(String[] args) {
        try {
            try {    } catch (Exception e) {    }
        } catch (Exception e) {
            try {    } catch (Exception e) {    }    // Compile Error
        }
        
        try {
            
        } catch (Exception e) {
            
        }        
    }
}
cs
  • 같은
    • 5번째와 6번째 줄의 참조 변수 영역이 서로 겹치기 때문에 에러가 발생한다.

 

  • 실제 사용 예
1
2
3
4
5
6
7
8
9
10
11
public class Hello {
    public static void main(String[] args) {
        int number = 100;
        int result = 0;
        
        for(int i=0; i<10; i++) {
            result = number / (int) (Math.random() * 10);
            System.out.println(result);
        }
    }
}
cs

위 예제는 number에 랜덤한 값을 나누는 수행을 반복한다. 
이 때 random 함수가 우연히 0을 반환하게 되면 예외가 발생하여 프로그램이 비정상적으로 종료될 것이다.

예외가 발생하는 원인을 알았으니, 예외처리 구문을 통해 프로그램의 비정상적인 종료를 막아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Hello {
    public static void main(String[] args) {
        int number = 100;
        int result = 0;
        
        for(int i=0; i<2; i++) {
            try {
                result = number / (int) (Math.random() * 10);
                System.out.println(result);
            } catch (ArithmeticException e) {
                System.out.println("0");
            }            
        }        
        System.out.println(5/0);                
    }
}
cs

예외 처리를 통해 0으로 나누려 시도하는 ArithmetricException이 발생하면 0을 화면에 출력하게끔 했다.

 

4. try-catch 문에서의 흐름

  • 예외가 발생한 경우와 발생하지 않았을 때의 흐름은 서로 다르다.
  • try 블럭내에서 예외 발생시
    1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인한다.
    2. 일치하는 catch 블럭을 찾으면, 블럭 내의 문장들을 수행하고, 전체 try-catch 문을 빠져나가서 다음 문장을 계속 수행한다.
    3. 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 못하고, 프로그램은 비정상 종료된다.
  • try 블럭 내에서 예외가 발생하지 않은 경우
    1. catch 블럭을 거치지 않고 전체 try-catch 문을 빠져나가서 수행을 계속한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Hello {
    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);
        }    // try-catch의 끝
        System.out.println(6);
    }    // main 메서드의 끝
}
cs

  • 그 어떠한 예외도 발생하지 않앗으므로 catch 블럭 내의 출력문은 호출되지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Hello {
    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);
        }    // try-catch의 끝
        System.out.println(6);
    }    // main 메서드의 끝
}
cs

  • 0/0 을 나누는 연산에서 ArithmeticException이 발생, 그 이후의 9번째 줄의 문장은 무시되고 곧바로 catch 블럭내의 문장이 출력된다.
  • 예외가 발생하면 try블럭 내에서 그 이후의 문장은 출력되지 않는것을 볼 수 있다. 그러므로 try의 범위는 잘 설정 해주어야 한다.

 

5. 예외 발생시키기

  • 키워드 throw를 사용하여 프로그래머가 고의로 예외를 발생시킬 수 있다.

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

Exception e = new Exception("고의로 발생시켰음");

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

throw e;

 

아래처럼 한줄로 예외를 발생시킬 수도 있다.

throw new Exception();

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Hello {
    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("프로그램이 정상 종료되었음.");
    }
}
cs

 

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

  • JAVA에서는 실행시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다.
  • 조상은 Object 클래스이다. 즉, Exception과 Error 클래스 역시 Object 클래스의 자손이다.

  • 모든 예외의 최고 조상은 Exception 클래스이며, 에러의 최고 조상은 Error 클래스 이다.
  • Exception과 Error 클래스들은 Throwable을 공통조상으로 가진다.
  • 예외 클래스들은 2개의 그룹으로 나뉠 수 있다.
    • RuntimeException클래스와 그 자손클래스들
      • RuntimeException 클래스들 이라 지칭
      • 프로그래머의 실수에 의해 주로 발생
        • 배열의 범위를 벗어날 경우(IndexOutOfBoundsException)
        • null인 참조변수의 멤버를 호출(NullPointerException)
        • 클래스간의 형변환 실수(ClassCastException)
        • 0으로 나누려 함(ArithmeticException)
    • Exception클래스와 그 자손클래스들 (RuntimeException과 자손들 제외)
      • Exception 클래스들 이라 지칭
      • 외부의 영향, 주로 프로그램의 사용자들의 동작에 의해 주로 발생
        • 존재하지 않는 파일의 이름을 입력(FileNotFoundException)
        • 클래스의 이름을 잘못 입력(ClassNotFoundException)
        • 입력한 데이터 형식이 잘못된 경우(DataFormatException

  • RuntimeException클래스들과 Exception클래스들의 차이점 = >컴파일시의 예외처리 체크 여부
    • RuntimeException 클래스들 그룹에 속하는 예외가 발생할 가능성이 있는 코드에는 예외 처리를 해주지 않아도 컴파일 시에 문제가 되지 않는다.
    • Exception클래스들 그룹에 속하는 예외가 발생할 가능성이 있는 예외는 처리하지 않으면 컴파일 에러가 발생한다.

※ RuntimeException 클래스들에 속하는 예외가 발생할 가능성이 있는 코드에도 예외처리를 해야 한다면, 모든 참조변수와 배열이 사용되는 곳에 예외처리를 해주어야 한다.

 

1
2
3
4
5
public class Hello {
    public static void main(String[] args) {
        throw new Exception();        // Exception을 강제로 발생 시킨다.
    }
}
cs
  • 위 코드는 컴파일 에러가 발생한다.

 

1
2
3
4
5
6
7
8
9
public class Hello {
    public static void main(String[] args) {
        try {
            throw new Exception();        // Exception을 강제로 발생 시킨다.
        } catch(Exception e) {
            System.out.println("Exception이 발생했습니다.");
        }
    }    // main 메서드의 끝
}
cs

  • 예외 처리를 통해 컴파일과 실행을 정상적으로 수행하였다.

 

 

1
2
3
4
5
public class Hello {
    public static void main(String[] args) {
        throw new RuntimeException();    // RuntimeException을 강제로 발생시킨다.
    }
}
cs

  • catch가 없어도 컴파일은 되지만, 런타임시에 에러가 발생함을 볼 수 있다.
RuntimeException 클래스들은 예외처리를 해주지 않아도 컴파일러가 문제삼지 않는다.

 

 

7. 예외 발생과 catch 블럭

  • catch 블럭은 괄호()와 블럭{}으로 나뉘어져있다.
    • 괄호() : 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언
    • 블럭{} : 예외를 catch 했을때 호출해야할 명령문들
  • 예외가 발생하면
    1. 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.
    2. 예외가 발생한 문장이 try블럭에 포함되어 있다면, 이 예외를 처리할 수 있는 catch 블럭이 있는지 찾게 된다.
      • 검사결과가 true인(예외를 처리할 수 있는) catch 블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나오고 예외는 처리된다.
      • 검사결과가 true인 catch 블럭이 하나도 없으면 예외는 처리되지 않는다.
  • 모든 예외 클래스는 Exception 클래스의 자손이므로, catch 블럭의 괄호()에 Exception 클래스 타입의 참조 변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch 블럭에 의해서 처리된다.
    • try-catch문의 마지막에 Exception클래스 타입의 참조변수를 선언한 catch 블럭을 사용하면, 어떤 종류의 예외에 대해서도 이 catch블럭에 의해 처리되도록 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Hello {
    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 (Exception e) {
            System.out.println(5);
        }
        System.out.println(6);        
    }
}
cs

  • 0/0 에 대해 ArithmeticException이 발생하지만 이의 조상 클래스인 Exception의 참조변수가 있어도 예외를 받을 수 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Hello {
    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) {
            if (ae instanceof ArithmeticException)
                System.out.println("true");
            System.out.println("ArithmeticException");
        } catch (Exception e) {
            System.out.println("Exception");
        }
        System.out.println(6);        
    }
}
cs

 

  • 예외가 발생했을 때 생성되는 예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.
  • catch 블럭의 괄호()에 선언된 참조변수를 통해 이 인스턴스에 접근할 수 있다.
  • 이 참조변수는 선언된 catch 블럭 내에서만 사용가능한다.
  • 주로쓰이는 메서드는 아래와 같다.
    • printStackTrace() : 예외 발생시 출력되어야할 예외메시지를 출력한다.
    • printStackTrace(PrintStream s) , printStackTrace(PrintWriter s) : 발생한 예외에 대한 정보를 파일에 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
 
public class Hello {
    public static void main(String[] args) {
        PrintStream ps = null;
        
        try {
            ps = new PrintStream("error.log");    // 파일 error.log에 출력할 준비를 한다.
            
            System.out.println(1);
            System.out.println(2);
            System.out.println(3);
            System.out.println(0/0);    // 예외 발생
            System.out.println(4);        // 실행 되지 않는다.
        } catch (Exception ae) {
            ae.printStackTrace(ps);
            ps.println("예외메시지 : " + ae.getMessage());    // 화면 대신 error.log 파일에 출력 한다.
        }
        System.out.println(6);
    }
}
cs

프로젝트 경로 내에 error 파일이 생성되고, 출력문이 저장되었음을 볼 수 있다.

 

 

8. finally 블럭

  • try-catch문과 함께, 예외의 발생 여부에 상관없이 실행되어야할 코드를 포함시키는데 쓰인다.
1
2
3
4
5
6
7
8
try {
    // 예외가 발생할 가능성이 있는 코드 작성
catch (Exception1 e1) {
    // 예외처리를 위한 문장을 적는다.
finally {
    // 예외의 발생여부에 관계없이 항상 수행되어아하는 문장들을 넣는다.
    // finally 블럭은 try-catch문의 맨 마지막에 위치해야한다.
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package project_1;
 
public class Hello {
    public static void main(String[] args) {
        Hello.method1();
        System.out.println("method1()의 수행을 마치고 main메서드로 복귀했습니다.");
    }
    
    static void method1() {
        try {
            System.out.println("method1() 이 호출되었습니다.");
            return;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("method1()의 finally 블럭이 실행되었습니다.");
        }
    }
}
cs

  • 위 코드의 결과에서 알 수 있듯이, try블럭에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다.
  • 이와 마찬가지로 catch블럭의 문장 수행 중에 return문을 만나도 finally 블럭의 문장들은 수행된다.

 

 

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

  • 예외 처리를 위해 지금까지는 try-catch 문을 사용했다.
  • 이외에 예외를 메서드에 선언하는 방법도 있다.
    • 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주면 된다.
    • 예외가 여러개면 쉼표로 구분한다.
1
2
3
void method() throws Exception1, Exception2, ... ExceptionN {
    // 메서드의 내용
}
cs
  • JAVA에서는 이와 같은 방법을 통해 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때, 이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있다.
  • 그 자체로써 예외를 처리하는 기능을 가진게 아니라, 자신을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

 

JAVA API문서에서 java.lang.Object클래스의 wait 메서드를 살펴보자

docs.oracle.com/javase/7/docs/api/index.html

위 문서를 통해 wait 메서드에서 발생하는 예외들에 대한 정보를 볼 수 있으며, 언제 예외가 발생하는지 설명이 덧붙여저 있으므로, 이 메서드를 호출하고자 하는 메서드에서는 미리 예외에 대한 대응을 할 수 있다.

InterrupedException 링크를 통해 들어가 해당 예외에 대한 정보를 확인해본다.

위 정보를 통해 InterruptedException은 Exception 클래스의 자손임을 알 수 있다. 따라서 반드시 처리해주어야 하는 예외임을 알 수 있다. 그러므로 wait 함수의 선언부에서는 throws 키워드와 함게 선언되어진것을 볼 수 있다.

그러면 나머지 2개의 예외는 어떨까?

RuntimeException클래스의 자손이므로 예외처리를 할 필요가 없다. 그래서 wait 메서드의 선언부에 IllegalMonotirStateException과 IllegalArgumentException은 적혀있지 않다.

 

메서드에 예외를 선언할 때 일반적으로 RuntimeException클래스들은 적지 않는다.

함수의 선언부에는 보통 반드시 처리해주어야 하는 예외들만 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package project_1;
 
public class Hello {
    public static void main(String[] args) throws Exception {
        method1();
    }
    
    static void method1() throws Exception {
        method2();
    }
    
    static void method2() throws Exception {
        throw new Exception();
    }
}
cs

  • method2에서 예외를 throw했지만 main과 method1, method2 중 어떤 함수에서도 이를 catch 하지 않아 에러가 발생했다.
    • 3개의 함수 중 아무 한군대에서라도 catch를 했다면 비정상종료는 발생하지않는다.
  • 프로그램 실행 도중 java.lang.Exception이 발생하여 비정상적으로 종료됐다는 것과 예외가 발생했을 때 호출스택(Call Stack)의 내용을 알 수 있다.
  • 위 출력을 통해 다음과 같은 사실을 알 수 있다.
$\cdot$ 예외 발생시, 3개의 메서드(main, method1, method2)가 호출 스택에 있었다.
$\cdot$ 예외가 발생한 곳은 제일 윗줄의 method2이다.
$\cdot$ main메서드가 method1()를, 그리고 method1()은 method2()를 호출했다는 것을 알 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package project_1;
 
public class Hello {
    public static void main(String[] args) throws Exception {
        method1();
    }
    
    static void method1() throws Exception {
        try {
            method2();
        } catch(Exception e) {
            System.out.println("method1 : catch the exception");
            e.printStackTrace();
        }
    }
    
    static void method2() throws Exception {
        throw new Exception();
    }
}
cs

예외처리를 method1에서 catch 하여 비정상적인 종료를 막았다.

printStackTrace를 통해 함수 콜스택을 확인할 수 있다.

 

 

예외 처리 팁

예외를처리하는 방법은 크게 2가지로 나뉜다.

  1. 예외가 발생한 메서드 내에서 예외를 처리한다.
  2. 예외가 발생한 메서드에서 처리하지 않고 호출한 메서드로 넘긴다.

혹은 두 메서드가 예외처리를 분담할수도 있다.

첫번째 사례의 예를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;
 
class ExceptionEX1 {
    public static void main(String[] args) throws Exception {
        File f = createFile(args[0]);
        System.out.println(f.getName() + " 파일이 성공적으로 생성되었습니다.");
    }
    
    static File createFile(String fileName) {
        try {
            if(fileName == null || fileName.equals(""))
                    throw new Exception("파일이름이 유효하지 않습니다.");
        } catch (Exception e) {
            // fileName이 부적절한 경우, 파일 이름을 '제목없음.txt'로 한다.
            fileName = "제목없음.txt";
        } finally {
            File f = new File(fileName);
            createNewFile(f);
            return f;
        }
    }
    
    static void createNewFile(File f) {
        try {
            f.createNewFile();
        } catch(Exception e) {}
    }   
}
cs

  1. 생성할 파일의 이름을 main의 매개변수로 전달받음
  2. createFile 함수에서 파일 이름을 조사
    • 이름이 비어있다 : 예외로 던진후 파일이름을 "제목없음.txt"로 설정
  3. 파일이름을 바탕으로 파일 생성

예외가 발생한 메서드에서 직접 예외를 처리하도록 되어있다.

createFile메서드를 호출한 main 메서드에서는 예외가 발생한 사실을 알지 못한다.

이 경우 main함수의 작성자가 옳지못한 파일명을 전달하였음에도 자신의 의도와 맞지 않는 파일을 생성받게된다.

 

 

 

 

두번째 사례를 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
 
class ExceptionEX2 {
    public static void main(String[] args) throws Exception {
        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;
    }
}
cs

이전 예제와는 달리 createFile을 호출한 main에서 직접 예외를 처리하도록 하였다.

그 결과 main의 작성자가 직접 예외에 대해 원하는 처리를 할 수 있게 되었다.

 

예외에 대한 처리를 함수의 호출자에게 맡기고 싶다면 해당 함수를 호출한 함수에서 예외를 처리하도록 유도한다.

 

 

'Programming Language > JAVA' 카테고리의 다른 글

사용자정의 예외 만들기  (0) 2021.04.05
예외 되던지기(exception re-throwing  (0) 2021.04.05
인터페이스(interface)  (0) 2021.03.11
추상클래스(abstract class)  (0) 2021.03.11
다형성(polymorphism)  (0) 2021.03.10
Comments