프로그램 오류의 종류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를

프로그램 에러 또는 오류라고 한다.

이를 종류로는 3가지 종류로 나눌 수 있다.

  • 컴파일 에러 : 컴파일 시에 발생하는 에러
  • 런타임 에러 : 실행 시에 발생하는 에러
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

자바에서는 이러한 에러중 런타임 에러에 대해서 다음과 같이 2가지로 구분하였다.

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러는 프로그램 실행 도중 발생하면 복구 할 수 없는 심각한 오류들을 말한다.

예를들어 우리가 StackOverFlow같은 것이나 메모리누수 이러한 것들을 말한다.

예외는 프로그램에 의해 수습을 할 수 있다.

예외 클래스의 계층구조

이러한 에러와 오류는 Throwable이라는 클래스에 상속되어 있다.

Throwable은 결과적으로 클래스이기 때문에 Object에 상속이 되어있다.

ExceptionThrowableObject

ErrorThrowableObject

와 같은 계층 구조로 이루어져 있다.

이중 예외의 최고조상은 Exception클래스 이며 이러한 예외 클래스는 두 그룹으로 나누어진다.

  1. RuntimeException클래스와 그 자식들
  2. RuntimeException을 제외한 Exception클래스와 그 자식들

첫번 째 그룹은 보통 프로그래머의 실수로 발생하는 예외이다. 예를들어 NullpointerException같은 오류들이 있다.

두번 째 그룹은 보통 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외이다.

예를들어 프로그램을 사용하고있는 사용자가 입력 데이터형식을 잘못해서 들어가서 DataFormatException을 발생하거나 하는 것이다.

예외 처리하는 법

이러한 에러가 아닌 예외는 프로그래머가 미리 이에 대한 처리를 해줄 수 있다.

이러한 예외가 일어나면 프로그램이 종료되는데 이러한 것을 방직하고 정상적인 실행상태를 유지할 수 있게 하는 것이다.

이러한 예외를 처리하기 위해서는 try-catch문을 다음과 같이 사용한다.

try{
    //예외가 발생할 가능성이 있는 문장을 삽입
} catch (Exception1 e1){
        //Exception1 이 발생하면 이를 처리할 문장을 적는다.
} catch (Exception2 e2){

}

이러한 catch블럭은 여러개가 붙을 수 있지만 발생한 종류와 일치하는 단 한개의 catch블럭만 실행이 된다.

또한, try-catch문은 {}를 생략할 수 없다는 것을 알아두자

try{
    try{

    }catch(Exception e1){

    }
}catch (Exception e){

}

그리고 이렇게 try-catch문 안에 다시 반복해서 try-catch문을 넣는 행위도 금지한다.

또한, Exception의 참조변수는 중복될 수 없다. 그렇기 때문에 e를 썼다면 다른 예외의 참조변수는 다른 단어를 써야 할 것 이다.

try-catch문 예제

public static void main(String[] args) {

        int number = 100;
        int result = 0;
        System.out.println(number / result);

}

이러한 경우 다음과 같은 에러가 발생하여 프로그램이 강제 종료된다.

Exception in thread "main" java.lang.ArithmeticException: / by zero

이러한 ArithmeticException을 해결하기 위해서 try-catch문을 써보자

public static void main(String[] args) {
        int number = 100;
        int result = 0;
        try{
            System.out.println(number / result);
        }catch (ArithmeticException e){
            System.out.println(e);
            System.out.println("오류가 발생하여 0으로 출력됩니다.");
        }
}

이렇게 try-catch문으로 ArithmeticException을 예외처리해주면 catch 블럭 안에 있는 코드가 실행이 된다.

만약에 오류가 없다면 catch문장을 굳이 수행하지는 않는다.

그렇다고 오류가 있어도 무조건 들어가는 것이 아니라 **catch에 선언되어 있는 오류만 예외처리 할 수 있다.**

또한, 참조변수 e는 오류 내용을 그대로 가지고 있다. 그렇기 때문에 출력하면 다음과 같은 결과를 가진다.

java.lang.ArithmeticException: / by zero

이렇게 System.out.println이 있지만 이를 출력하는 다른 방법도 있다.

이중 오류 메세지인 / by zero만 출력하고 싶으면 getMessage()메서드를 사용하면 되고

모든 것을 다 출력하고 싶다면 printStackTrace()를 출력하면 된다.

catch (ArithmeticException e){
    e.printStackTrace();// java.lang.ArithmeticException: / by zero at codewords02.main(codewords02.java:12)
    System.out.prinln(e.getMessage()); // / by zero
}

이렇게 출력이 된다.

멀티 catch블럭

예외처리마다 똑같은 행위를 하고싶다면 예를들어 단순히 출력하는것이다.

이러한 경우 예외의 수가 많아지면 catch블럭 줄이 엄청 길어지게 되기 마련이다.

이를 해결하기위해서 JDK1.7부터 |를 사용해 예외들을 묶을 수 있게 되었다.

try{
    //...
}catch(Exception1 | Exception2 e){
    /..
    e.printStackTrace();
}

이런식으로 하나의 참조변수에 예외들을 묶을 수 있다.

하지만 이것도 규칙이 있다.

만약 Exception1Exception2가 서로 부모와 자식 관계에 있다면 컴파일 에러가 발생한다

왜냐면 부모와 자식관계에 있다면 부모 클래스만 적어도 자식클래스의 예외처리를 해줄 수 있기 때문이다.

사실 허용할 수 있는 정도지만 불필요한 코드를 제거하라는 의미에서 에러가 발생한다.

또한, 주의해야할 점이 참조변수가 같아진것 이기 때문에

어떤 오류가 발생할지 모르는 시점이기 때문에 자신의 멤버를 사용할 수 없고 부모의 멤버만 사용한다.

물론 장점도 있지만 이러한 점이 있기 때문에 겹치는 멤버나 메서드에 한에서 사용하기를 권장한다.

예외를 발생시키는 법

프로그래머는 다음과 같은 과정으로 예외를 고의로 발생시킬수 있다.

  1. 연산자 new를 이용하여 Exception객체를 생성하는 것이다
    • Exception e = new Exception("오류 내용");
  2. 키워드 throw를 사용하여 예외를 발생시킨다.
    • throw e;
    • throw new Exception();

하지만 이러한 예외중에서도 RuntimeException을 고의로 발생시킬 때는 컴파일이 잘되는 것을 볼 수 있다.

왜냐하면 RuntimeException과 그 자식 클래스들을 프로그래머에 의해 실수로 발생하는 것들이기 떄문에 예외처리를 강제하지 않는다는 것이다.

이들은 unchecked예외라고 불린다. 나머지들은 checked예외라고 불리낟.

메서드에 예외 선언하는 법

메서드에 예외를 선언하기 위해서는 throws를 사용하여 처리할 수 있다.

void method() throws Exception1, ....[

}

다 귀찮다 하면 예외의 최고 조상인 Exception클래스를 메서드에 선언하면 모든종류의 예외가 발생할 가능성이 있다는 것을 표시한다.

이렇게 예외를 선언한다면 자식 클래스가 상속하여 오버라이딩할 때도 예외가 발생할 수 있다는 점에 주의해야한다.

그렇기 때문에 자바는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서들ㄹ 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에 좀더 견고한 프로그램 코드를 작성하게 도와준다.

그렇기 때문에 메서드를 사용할 때 API를 보고 throws목록을 확인하고 예외를 처리해줘야할 생각을 하는 것이 좋다.

예외의 호출 스택(Call Stack)

예외는 처리를 하지 않으면 호출한 메서드에게 넘겨주게된다.

public static void main(String[] args) throws Exception
{
    method();
} 

public static void method() throws Exception{
    throw new Exception();
}

이러면 다음과 같은 오류 메세지가 등장하게된다.

image

오류가 등장하는것을 보면 mehtod()에서 먼저 오류가 발생한뒤 처리를 해주지 않아 method를 호출한 main에게 넘겨주고 main에서도 처리를 안해주어 결국 예외가 발생한것으로 보인다.

이를 해결하기위해서는 두가지 방법이있다.

try-catch를 이용하여

  1. main메서드에서 처리해주는것
  2. mehtod()에서 처리를 해주는것

둘중 어느 것을 사용해도 무방하다

하지만 첫번째 방법은 보통 메서드 내에서 자체적으로 해결이 안되는 경우에 사용한다. 예를들어 호출값을 다시 받아서 처리해주는 경우가 있다.

그게 아니라면 보통은 예외가 발생한 해당 메서드에서 처리해주는 것이 좋다.

Finally블럭

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

try-catch문의 끝에 선택적으로 덧붙여 사용하면된다.

try{

}catch(Exception e1){

}finally{

}

중요한것은 예외가 발생하지 않아도 실행되고 혹은 예외가 발생해도 실행된다는 것이다.

try-with-recsoorces문

JDK 1.7부터 새로 추가된 기능으로 보통 파일 입출력에서 사용한다.

왜 입출력에서 자주쓰이는지는 다음 예를 살펴보자

try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    dataInputStream.close();
}

이러한 파일 입출력은 사용한 후에 꼭 close()를 해주어야 사용된 자원이 반환된다.

이러한 특성을 지니고 있기 때문에 만약 예외가 발생하면 close가 되지않아 자원이 반환되지 않기 때문에 finally에서 항상 close()를 해주고 있다.

하지만 이 코드에는 문제점이 있다. close()라는 메서드에서도 오류가 발생할 수 있다는 것이다.

프로그래머가 잘못해서 다른 곳에서 close()이미 했다던지 혹은, 파일이 없어서 close()할 파일도 없다던지 하는 오류가 발생할 수 있다. 그래서 다음과 같이 작성해야한다.

 try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    try{
        if(dataInputStream != null)
            dataInputStream.close();
    }catch (IOException e){
            e.printStackTrace();
    }
}

이런식으로 finally블럭에 try-catch문을 추가하여 close()에서 발생할 수 있는 예외를 처리하도록 변경하였다.

이러한 문장을 try-with-resources문으로 고친다면 다음과 비슷하게 사용될 수 있다.

try (FileInpuStream fileInputStream = new FileInputStream("score.bat");
        DataInputStream dataInputStream = new DataInputStream(fileInputStream)){
    while(true){
        score = dataInputStream.readInt();
        System.out.prinln(score);
        sum += score;
    } catch (EOFException e) {
        System.out.println("점수의 총합은 " sum + "입니다.");
    } catch (IOException ie){
        ie.printStackTrace();
    }
}

이렇게 변경하면 앞선 코드보다 복잡하지 않고 좋다.

현재 try의 괄호 안에 객체를 생성하는 문장을 넣었는데 이는 try를 벗어나자마자 자동적으로 close()를 호출하게 된다.

하지만 사용하기 위해서는 객체의 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자 정의 예외 만들기

기존 정의된 예외말고 자신이 필요한 예외클래스를 새롭게 정의 할 수 있다.

Exception클래스나 RuntimeException클래스로부터 보통은 상속받아 클래스를 작성한다.

// 예시 1
class MyException extends Exception{
    private final int ERR_CODE;

    MyException(String message){
        super(message, 100);
    }

    MyException(String message, int errCode){
        super(message);
        ERR_CODE = errCODE;
    }

    public int getErrCode(){
            return ERR_CODE;
    }        
}

클래스를 만약 Exception을 하면 checked예외로 처리가 된다.

만약 RuntimeException을 상속 받았다면 nonchecked예외로 처리할 수 있어서 더 환영받고 있다.

예외 되던지기

만약, 한 메서드에서 발생할 수 있는 예외가 여럿인 경우는 다음과 같이 처리할 수 있다.

  • 몇 개는 try-catch문을 이용하여 메서드 내에서 자체로 처리한다.
  • 나머지는 선언부에 지정하여 호출한 메서드에서 처리한다.

이런식으로 양쪽을 통해 처리할 수 있다. 또한, 이런 방법으로도 예외가 단 하나인 상황에서도 처리할 수 있다.

이렇게 예외가 단 하나인 경우에는 예외를 처리한 후에 다시 발생시키는 방법을 통해서 가능하다.

이를 예외 되던지기라고 한다.

예외 되던지기를 사용하는 방법은 다음과 같다.

  • 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용하여 예외를 처리해준다.
  • catch문에서 필요한 작업을 행한 후 throw문을 이용하여 예외를 다시 발생시킨다.
  • 다시 발생한 예외를 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리한다.

예외 되던지기를 사용할 때는 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야할 작업이 있을 때 사용한다.

// 예제
public static void main(Stirng[] args){

    try{
        method();
    } catch (Exception e){
        System.out.println("main 메서드에서 예외가 처리되었습니다");
    }
} 

static void method() throws Exception{
    try{
        throw new Exception();
    } catch (Exception e){
        System.out.println("method 에서 예외가 처리되었습니다.");
        throw e;
    }
}

이런식으로 main에서도 try-catch를 이용하여 처리를 해주고 method에서 예외를 처리하고 예외를 방생시켜서 사용한다.

연결된 예외(chained exception)

한 예외는 다른 예외를 발생시킬 수 있다.

다음 예시는 SpaceException을 원인 예외로하는 InstallException을 발생시키는 예시이다.

public class ChainedException {
    public static void main(String[] args) {
        try{
            install();
        } catch (InstallException e){
            e.printStackTrace();
        }
    }

    static void install() throws InstallException{
        try{
            startInstall();
            copyFile();
        } 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(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

    static void copyFile(){

    }

    static void deleteTempFiles(){

    }

    static boolean enoughSpace(){

        return false;
    }

    static boolean enoughMemory(){

        return true;
    }
}

class InstallException extends Exception{
    InstallException(String message){
        super(message);
    }
}

class SpaceException extends Exception{
    SpaceException(String message){
        super(message);
    }
}

class MemoryException extends Exception{
    MemoryException(String message){
        super(message);
    }
}

일단 initCase()는 지정한 예외를 원인 예외로 지정하는 메서드이다. 또한, getCause()는 원인 예외를 반환하는 메서드이다.

원인 예외로 왜 등록해서 다시 예외를 발생하는지는 이유가 있다.

그냥 예외만 처리하면되는데 왜이렇게 복잡하게 하는지 궁금할텐데

그냥 예외만 바로 처리해버린다면 실제로 발생한 예외가 무엇인지 모르기 때문에 함부로 할 수 없다.

또한, checked예외라면 이를 unchecked에외로 바꿀 수 있기 떄문이다.

startInstall()메서드를 확인해보면 자세히 알 수 있다.

static void startInstall(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

이처럼 throw new RuntimeException으로 한번 감싸준다면 unchecked예외로 설정할 수 있다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11

+ Recent posts