코딩관계론

Error vs Exception 본문

개발/Java

Error vs Exception

개발자_티모 2024. 6. 18. 17:05
반응형

자바에는 Error와 Exception이 존재하는데 이 두 클래스는 모두 Throwable 클래스를 상속 받습니다.

그럼 java.lang.Error는 무엇일까?

java.lang.Error는 시스템 수준에서 발생한 심각한 문제로 어플리케이션이 복구가 불가능한 상태가 됩니다. Error 클래스의 예로는 VirtualMachineError, OutOfMemoryError, StackOverflowError, AssertionError 등이 있습니다.

그럼 java.lang.Exception는 무엇일까?

java.lang.Exception는 어플리케이션 실행 중 복구 가능한 문제를 나타냅니다. 보통 프로그래머의 논리적 오류가 예외를 뜻합니다. Exception예제는 java.lang.RuntimeException, java.lang.Exception등이 있습니다.

 

따라서 둘의 에러와 예외의 주된 차이점은 복구 가능성입니다.

Checked와 Unchecked의 차이는?

Checked와 Unchecked의 차이는 Check Exception은 컴파일 시점에 예외처리 검사를 수행하고, 예외처리가 되어 있지 않다면 컴파일이 불가능합니다. 하지만 Unchecked는 런타임 시점에 예외처리를 검사합니다. 즉  컴파일러가 예외처리 검사를 수행하냐가 차이점입니다

Checked Exception

먼저 CheckException부터 살펴보면 CheckException은 컴파일 시점에서 반드시 처리되어야 하는 예외라고 설명했습니다. 이 CheckException의 대표적인 예외는 IO Exception, Sql Exception, ClassNotFound Exception이 있습니다.

 

아래의 코드는 IO Excpetion이 발생하는 예제인데 try-catch없이 컴파일하게 되면 컴파일이 되지 않습니다. 따라서 주석을 풀어서 예외처리를 해줘야 컴파일이 되고, 어플리케이션을 실행할 수 있습니다.

public class ExceptionTest {
    public static void main(String[] args) {
        Path filePath = Paths.get("./src/test.txt");

        // try {
            // 파일 전체를 한 번에 읽기
        List<String> lines = Files.readAllLines(filePath);

        for (String line : lines) {
            System.out.println(line);
        }
    //     } catch (IOException e) {
    //         System.err.println("파일을 읽는 도중 오류가 발생했습니다: " + e.getMessage());
    //     }
    // }
    }
}

 

Runtime Exception

다음으로 Runtime Exception에 대해서 살펴보겠습니다. Runtime Exception은 보통 프로그래머의 논리 구조의 오류로 인해서 발생하는 예제입니다. 아래의 코드처럼 배열이 3개가 선언됐지만 4번째 인덱스에 접근하게 되면 Nullpoint Exception이 발생하게 됩니다.

public class ExceptionTest {
    public static void main(String[] args) {
        Integer[] arr = new Integer[3];
        System.out.println(arr[4]);
    }
}

 

 

Try-Catch-finally

앞서 예외와 에러의 차이점을 복구 가능성이라고 봤는데, 이 모든 예외를 복구하기 위해서 자바에서는 try-catch-finally이라는 문법을 제공합니다. 특히 finally에서는 오류와 상관없이 실행되어야 하는 코드가 들어가게 됩니다. 예를들면 파일 핸들러의 연결을 종료하는 등의 자원 회수를 진행하는 영역입니다.

try{
  // 예외처리가 필요한 로직
}catch(//예외이름){
  //예외가 발생하면 실행되는 코드 
}finally{
 //예외와 상관없이 반드시 실행해야 하는 코드
}

 

만약 위의 에러를 방지하기 위해선 아래처럼 코드를 작성하면 됩니다.

    public  static void main(String[] args){
        try{
            Integer[] arr = new Integer[3];
            System.out.println(arr[4]);
        }catch (RuntimeException e){
            System.out.println(e);
        }
    }

 

finally을 통해서 자원회수를 진행한다고 하면 아래의 코드처럼 작성해야 합니다. 하지만 대충 보기에도 자원의 회수 과정이 쉽지는 않아보입니다.

public static void main(String[] args) {
    BufferedReader reader = null;

    try {
        reader = new BufferedReader(new FileReader("/home/bae/ws/threadSafe/src/test.txt"));
        String line = null;
        while ((line = reader.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        System.err.println("파일을 읽는 도중 오류가 발생했습니다: " + e.getMessage());
    } finally {
        try {
            if (reader != null) {
                reader.close();
                System.out.println("BufferedReader가 정상적으로 닫혔습니다.");
            }
        } catch (IOException e) {
            System.err.println("BufferedReader를 닫는 도중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

 

이러한 과정을 심플하게 하고자 자바7에서는 try-catch-resourc 문법을 사용함으로써 JVM에서 close 함수를 호출할 수 있게 해줍니다.

아래의 코드는 명시적으로 close하는 함수가 없음에도 JVM에서 close를 호출합니다.

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

        try(BufferedReader reader = new BufferedReader(new FileReader("./src/test.txt"))){
            String line = null;
            while((line = reader.readLine()) != null){
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("파일을 읽는 도중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

 

하지만 중요한 점은 모든 클래스가 이러한 기능을 지원하는 것은 아닙니다. Java의 try-with-resources 문법을 사용하려면 리소스 클래스가 AutoCloseable 인터페이스를 구현해야 합니다.

 

앞서 우리가 예외처리를 진행할 때는 함수 중간에서 try-catch를 수행했기 때문에 코드의 가독성이 떨어졌습니다. 하지만 throws선언문을 이용하면 이러한 문제를 해결할 수 있습니다.

 

Throws

throws 선언을 한 함수는 명시적으로 이 함수를 호출자에게 예외처리를 위임하게 됩니다. 그 뿐만 아니라 프로그래머가 봤을 때 어떤 에러들이 이 함수에서 발생할 수 있을지 알려줍니다. 

public class ExceptionTest {
    public static void main(String[] args) {
        raiseError();
    }

    public static void raiseError() throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader("./src/test.txt"));
        String line = null;
        while((line = reader.readLine()) != null){
            System.out.println(line);
        }
    }
}

 

throw의 경우 명시적으로 예외를 일으키기 위해서 사용됩니다. 다음과 같이 잔고가 0이면 출금을 하면 안되는 상황에서 강제로 throw함수를 통해서 출금을 막을 수 있습니다.

class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public double getBalance() {
        return balance;
    }

    public void withdraw(double amount) throws InsufficientFundsException {
        if (balance <= 0) {
            throw new InsufficientFundsException("잔고가 부족합니다. 출금을 할 수 없습니다.");
        } else if (amount > balance) {
            throw new InsufficientFundsException("잔고가 부족합니다. 출금하려는 금액이 잔고를 초과합니다.");
        }
        balance -= amount;
        System.out.println("출금 완료. 현재 잔고: " + balance);
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(0);

        try {
            account.withdraw(100);
        } catch (InsufficientFundsException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }
    }
}

 

우리가 위의 예외클래스 사진에서 InsufficientFundsException라는 예외를 찾아볼 수 있었습니까? 당연히 찾아볼 수 없었을 것입니다.
왜냐하면 이건 제가 예외를 재정의 했기 때문이죠.


위에서 확인했듯이 모든 예외는 Exception클래스를 상속받기 때문에 Exception클래스를 상속 받아서 자신만의 예외를 작성할 수 있습니다.

 

그렇다면 여기서 RuntimeExcetion를 상속 받을 수도 있고, 부모인 Exception 클래스를 상속 받을 수도 있는데 어떤 기준으로 선택하는 것이 좋을까요? 당연히 둘의 차이점을 생각해보시면 됩니다. 즉 "예외를 처리할 때 RuntimeException을 상속받을지 Exception을 상속받을지는 주로 예외가 checked 예외인지 unchecked 예외인지에 따라 결정됩니다."

 

 

깊게 생각해보기

1. 예외는 어디에서 처리되는게 이상적일까?

Clean Code와 같은 책을 참고하면, 예외 처리는 발생한 곳에서 가까이 처리되는 것이 좋다고 합니다. 그 이유는 다음과 같습니다.

  1. 정보의 충분성: 예외가 발생한 위치에서 해당 클래스나 메소드의 맥락을 가장 잘 이해하고 있는 사람이 예외를 처리하는 것이 이상적입니다. 이는 적절한 예외 처리를 위해 필요한 정보(상황, 상태, 예상 동작 등)를 가장 잘 알고 있기 때문입니다.
  2. 응집도 유지: 예외를 가까운 곳에서 처리하면 해당 코드의 응집도를 유지할 수 있습니다. 예외 처리를 멀리서 하게 되면, 코드가 복잡해지고 응집도가 낮아질 수 있습니다.
  3. 디버깅 용이성: 예외가 발생한 곳에서 바로 처리하면 디버깅이 용이해집니다. 예외가 발생한 위치와 처리 위치가 가깝기 때문에 문제를 추적하기가 더 쉽습니다.

2. 그럼 왜  thorws를 통해서 예외처리를 위임할까?

가까운 곳에서 예외를 처리하는 원칙을 따르게 되면, 여러 곳에서 같은 예외 처리 코드를 작성해야 할 수도 있습니다. 예를 들어, a, b, c 객체가 특정 작업을 수행하고, 그 작업이 실패했을 때 사용자에게 메시지를 전달해야 하는 경우를 생각해보겠습니다. 그럼 코드가 아래쪽 코드와 같이 같이 각 클래스마다 동일한 예외 처리 코드가 반복될 것입니다. 이는 코드 중복을 초래하고 유지보수성을 저하시킵니다.

class A {
    void doSomething() {
        try {
            // 작업 수행
        } catch (Exception e) {
            // 예외 처리
            System.out.println("에러 메시지");
        }
    }
}

class B {
    void doSomething() {
        try {
            // 작업 수행
        } catch (Exception e) {
            // 예외 처리
            System.out.println("에러 메시지");
        }
    }
}

class C {
    void doSomething() {
        try {
            // 작업 수행
        } catch (Exception e) {
            // 예외 처리
            System.out.println("에러 메시지");
        }
    }
}

 

반면에 throws를 통해서 예외처리를 묶어준다면  예외 처리가 공통된 경우, 상위 계층에서 한 번에 처리할 수 있어 코드 중복을 제거하고 유지보수성을 높일 수 있습니다

class A {
    void doSomething() throws Exception {
        // 작업 수행
    }
}

class B {
    void doSomething() throws Exception {
        // 작업 수행
    }
}

class C {
    void doSomething() throws Exception {
        // 작업 수행
    }
}

class Main {
    void execute() {
        try {
            A a = new A();
            a.doSomething();
            B b = new B();
            b.doSomething();
            C c = new C();
            c.doSomething();
        } catch (Exception e) {
            // 공통된 예외 처리
            System.out.println("에러 메시지");
        }
    }
}

 

3. 예외를 재정의 하는 이유는?

그 이유는 예외를 재정의 하게 된다면 자신만의 타입(도메인)이 생기게 된다.

class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

class AccountNotFoundException extends Exception {
    public AccountNotFoundException(String message) {
        super(message);
    }
}

 

반응형