Back-End (Web)/JAVA

[JAVA] 오류 및 예외에 대한 이해

JABHACK 2024. 11. 12. 20:26

오류 및 예외에 대한 이해

 

오류(Error)🔥 예외(Exception)🚨
  • 오류(Error)는 일반적으로 회복이 불가능한 문제입니다.
    • 이는 시스템 레벨에서, 또는 주로 환경적인 이유로 발생합니다.
    • 코드의 문제로 발생하는 경우도 있지만, 일단 발생하는 경우 일반적으로 회복이 불가능합니다.
    • 에러가 발생한 경우 우리는 어떠한 에러로 프로그램이 종료되었는지를 확인하고 대응합니다.
  • 예외(Exception)는 일반적으로 회복이 가능한 문제입니다.
    • 회복이 가능하다는 전제는 우리가 “그 예외가 발생할 수 있다는 것을 인지하고, 대응했을 것입니다”.
    • 현실적으로 코드 레벨에서 할 수 있는 문제 상황에 대한 대응은 “예외 처리”에 속합니다.
  • 보통 만나게 되는 문제는 예외이고 이는 "예외처리"로 예방이 가능하다.
  • 보통 예외는 코드의 실행, 예외 처리 관점 2가지의 종류로 나뉘는데
코드 실행 관점 예외처리 관점
  • 컴파일 에러(예외) 📂
    • .java 파일을 .class 파일로 컴파일할 때 발생하는 에러
    • 대부분 여러분이 자바 프로그래밍 언어의 규칙을 지키지 않았기 때문에 발생합니다.
    • 예를 들어 있지 않은 클래스를 호출한다거나, 접근이 불가능한 프로퍼티나 메소드에 접근한다거나 하는 경우에 발생합니다.
    • 컴파일 에러가 발생하는 경우 해결 방법은 문법에 맞게 다시 작성하는 것입니다.
  • 런타임 에러(예외) ❤️‍🔥
    • 우리가 주로 다루게 될 에러(예외)입니다.
    • 문법적인 오류는 아니라서, 컴파일은 잘 되었지만 “프로그램”이 실행 도중 맞닥뜨리게 되는 예외입니다.
확인된 예외✅ (Checked Exception)
  • 컴파일 시점에 확인하는 예외입니다.
  • 반드시 예외 처리를 해줘야 하는 예외입니다.

    주로 외부 자원(파일, 네트워크, 데이터베이스 등)과의 작업에서 발생할 수 있는 예외입니다.



미확인된 예외🚫 (Unchecked Exception)
  • 런타임 시점에 확인되는 예외입니다.
  • 예외 처리가 반드시 필요하지 않은 예외입니다.

    프로그래머의 실수로 발생하는 예외(정수를 0으로 나눈다던가...)

 

+ 컴파일 에러는 예외 처리를 하지 않아서 발생하는 오류()를 말한다. 만약 예외 처리를 생각도 못했는데 컴파일 중 에러가 나왔다면 이건 그냥 에러다.

 


1. Checked Exception (체크드 예외)

컴파일 타임에 반드시 예외 처리를 요구하는 예외입니다.
컴파일러가 예외 처리(try-catch 또는 throws 선언)가 되어 있는지 확인하며, 처리하지 않으면 컴파일 에러가 발생합니다.

특징

  • 발생 가능성이 높은 예외로 간주되며, 반드시 처리해야 합니다.
  • 주로 외부 환경과 상호작용하는 코드에서 발생합니다.
    예: 파일 입출력, 네트워크 연결, 데이터베이스 접근 등.
  • Exception 클래스에서 파생되지만, RuntimeException을 제외한 모든 예외가 해당됩니다.

대표적인 Checked Exception

  • IOException (파일 입출력 오류)
  • SQLException (SQL 관련 오류)
  • ClassNotFoundException (클래스를 찾을 수 없음)
import java.io.*;

public class CheckedExample {
    public static void main(String[] args) {
        try {
            FileReader reader = new FileReader("nonexistent.txt");  // 파일 읽기 시도
        } catch (FileNotFoundException e) {
            System.out.println("파일을 찾을 수 없습니다: " + e.getMessage());
        }
    }
}

 

  • 여기서 FileReader는 파일이 존재하지 않을 경우 FileNotFoundException을 발생시킵니다.
  • 이를 try-catch 블록으로 처리하지 않으면 컴파일 에러가 발생합니다.

 

 

 

보통 대부분의 예외는 아래의 처리 방식을 기준으로 한다

1 예외를 인지, 정의 = 예외 클래스 생성
2 예외 발생시 알림 = throw( 예약어 )
3 사용자가 예외 핸들링(처리)

 

2. Unchecked Exception (언체크드 예외)

컴파일 타임에 예외 처리를 강제하지 않는 예외입니다.
예외 처리를 하지 않아도 프로그램은 정상적으로 컴파일됩니다. 다만, 런타임 시 발생하면 프로그램이 중단될 수 있습니다.

특징

  • 주로 프로그래머의 실수로 인해 발생합니다.
  • 개발자가 예외 처리를 강제받지 않아도 되지만, 적절히 처리해야 프로그램이 중단되지 않습니다.
  • RuntimeException 클래스와 그 하위 클래스들이 포함됩니다.

대표적인 Unchecked Exception

  • NullPointerException (널 참조 접근)
  • ArrayIndexOutOfBoundsException (배열 인덱스 초과)
  • ArithmeticException (0으로 나누기)
  • ClassCastException (잘못된 형변환)

예제

public class UncheckedExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};

        // 예외 처리 없이 실행
        System.out.println(numbers[5]);  // ArrayIndexOutOfBoundsException 발생
    }
}
  • 이 코드는 예외 처리를 하지 않아도 컴파일은 되지만, 실행 시 ArrayIndexOutOfBoundsException이 발생하며 프로그램이 중단됩니다.

 

3. Checked vs Unchecked 정리

구분 Checked Exception Unchecked Exception
검사 시점 컴파일 타임 런타임
예외 처리 강제 여부 예, 처리하지 않으면 컴파일 에러 발생 아니요, 처리하지 않아도 컴파일 가능
주요 원인 외부 환경(입출력, 네트워크 등) 프로그래머 실수 또는 논리 오류
상위 클래스 Exception (단, RuntimeException 제외) RuntimeException
대표 예 IOException, SQLException NullPointerException, ArithmeticException

 

4. 실전에서의 사용

  • Checked Exception: 외부 환경과 상호작용하는 코드에서 주로 발생하므로 적절히 예외 처리를 반드시 해야 합니다.
    • 예: 파일을 읽거나 쓰는 작업에서 파일이 없을 가능성을 항상 고려.
  • Unchecked Exception: 런타임 중 예상치 못한 오류를 처리하려면 예외 처리를 추가로 넣을 수 있지만, 기본적으로 개발자가 코드를 주의 깊게 작성하는 것이 중요합니다.
    • 예: 배열 인덱스를 안전하게 접근하도록 범위를 체크.

 

5. 코드 설계 관점

  • Checked Exception은 호출자에게 예외를 알리는 역할을 하므로 예측 가능한 문제를 나타낼 때 사용.
  • Unchecked Exception코드의 버그나 프로그래머 실수를 나타내며, 예외가 발생했을 때 빠르게 수정을 유도합니다.

 

예외 발생과 try-catch, finally 문

 

자바에서 예외는 크게 **Checked Exception**과 Unchecked Exception 두 가지로 구분됩니다. 두 예외의 차이는 컴파일러가 예외 처리 여부를 검사하는지 여부에 따라 나뉩니다.

throw

📌 프로그래머가 의도적으로 예외를 발생시킬 때 사용하는 키워드입니다. 주로 특정 조건에서 문제가 발생할 가능성이 있다고 판단될 때 직접 예외를 던져서(발생시켜서) 경고하는 용도로 사용됩니다.

public class Main {
    public static void main(String[] args) {
        try {
            checkEligibility(15);  // 18세 미만이므로 예외 발생
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void checkEligibility(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("나이는 18세 이상이어야 합니다.");  // 예외 발생
        }
        System.out.println("자격이 있습니다.");
    }
}

 

  • 실제로 예외가 발생한 후 실행되는 함수이기에, 이 코드가 실행되면 throw 문과 함께 메서드가 종료된다.
  • 이게 있는 이유는 아래 설명할 예외 던지기 때문에 필요하다.
  • 일반적인 상황에서 프로그래머가 수정을 위해서라면, 컴파일 에러면 충분하다.

예외 던지기

  • 메서드에서 발생한 예외를 처리한 후 다시 호출한 쪽으로 전달하거나, 처리하지 않고 바로 호출한 쪽으로 넘기는 경우에 사용됩니다.

 

1. 예외 되던지기를 사용하는 이유

예외를 처리하지 않고 상위 메서드로 넘기는 이유는:

  1. 책임 분리
    • 모든 메서드가 자신의 역할에만 집중해야 하기 때문입니다.
    • 예외를 완전히 처리하기 위한 **문맥(Context)**은 상위 메서드에서 더 많이 알고 있을 가능성이 큽니다.
    • 예를 들어, readFile 메서드가 파일을 읽는 동안 오류가 발생했을 때, 이 메서드가 문제를 해결할 수는 없고, 상위 메서드에서 다른 파일을 선택하거나 사용자에게 알리는 등 더 적절한 작업을 할 수 있습니다.
  2. 공통 처리 로직 적용
    • 상위 계층에서 예외를 한꺼번에 처리하면, 중복 코드가 줄어듭니다.
    • 예를 들어, 여러 메서드에서 파일을 다룬다면, 파일이 없을 경우 기본적으로 보여줄 에러 메시지는 상위 계층에서 일괄 처리할 수 있습니다.
  3. 다양한 예외의 관리
    • 하위 메서드에서 발생하는 여러 유형의 예외를 상위 계층에서 분류하고 관리할 수 있습니다.
    • 예: FileNotFoundException, SQLException 등 서로 다른 예외를 공통된 방식으로 처리하거나 변환.

 

2. 예제 코드: 두 번의 예외 처리

(1) 예외를 처리한 후 다시 던지는 경우

public class ExceptionRethrowingExample {
    public static void main(String[] args) {
        try {
            processFile();
        } catch (Exception e) {
            // 최상위 메서드(main)에서 최종적으로 예외 처리
            System.out.println("main에서 예외 처리: " + e.getMessage());
        }
    }

    static void processFile() throws Exception {
        try {
            readFile();
        } catch (Exception e) {
            // 하위 메서드에서 일부 로그를 남기고 다시 예외를 던짐
            System.out.println("processFile에서 예외 처리 중... 메시지: " + e.getMessage());
            throw e; // 예외를 상위 메서드로 넘김
        }
    }

    static void readFile() throws Exception {
        // 파일 읽기 중 예외 발생
        throw new Exception("파일 읽기 실패");
    }
}

processFile에서 예외 처리 중... 메시지: 파일 읽기 실패
main에서 예외 처리: 파일 읽기 실패
  • readFile()에서 예외 발생 → processFile()에서 로그 처리 후 예외 되던지기 → main()에서 최종 처리.
  • processFile()이 예외를 일부 처리하고 다시 던짐으로써 예외가 호출 계층을 따라 전달됩니다.

 

(2) 예외 정보를 추가하여 되던지기

public class ExceptionRethrowingWithInfo {
    public static void main(String[] args) {
        try {
            processFile();
        } catch (Exception e) {
            // 최종적으로 예외 처리
            e.printStackTrace();
        }
    }

    static void processFile() throws Exception {
        try {
            readFile();
        } catch (Exception e) {
            // 새로운 예외로 감싸서 추가 정보를 전달
            throw new Exception("processFile 중 문제 발생", e);
        }
    }

    static void readFile() throws Exception {
        // 원래 예외 발생
        throw new Exception("파일 읽기 실패");
    }
}


java.lang.Exception: processFile 중 문제 발생
	at ExceptionRethrowingWithInfo.processFile(ExceptionRethrowingWithInfo.java:10)
	at ExceptionRethrowingWithInfo.main(ExceptionRethrowingWithInfo.java:4)
Caused by: java.lang.Exception: 파일 읽기 실패
	at ExceptionRethrowingWithInfo.readFile(ExceptionRethrowingWithInfo.java:16)
	... 2 more
  • 예외를 감싸면서 추가 정보를 전달하기 때문에, 디버깅 시 문제의 흐름을 쉽게 파악할 수 있습니다.

 

3. 예외 되던지기의 사용 사례

대표적인 사용 상황

  1. 모듈 간 책임 분리
    • 예외 처리 책임을 호출한 메서드에 맡기고, 중간 메서드는 일부 로그나 정리 작업만 수행한 뒤 다시 던집니다.
  2. 일부 처리 후 상위 메서드로 전달
    • 예외가 발생했을 때, 중간 메서드에서 로그를 남기거나 상태를 정리한 후 상위 호출자에게 문제를 알립니다.
  3. 예외 타입 변환
    • 저수준 예외(IOException 등)를 고수준 예외(CustomException 등)로 변환해 호출 계층에 전달.

 

4. 정리

  • 예외 되던지기는 하위 메서드에서 처리하지 못하는 예외를 상위 메서드로 전달하는 중요한 기법입니다.
  • 필요한 경우:
    1. 하위 메서드에서 예외를 완전히 처리할 수 없을 때.
    2. 추가 정보를 포함하거나 새로운 예외로 변환할 때.
    3. 호출 계층에서 더 나은 처리를 기대할 때.

 

throws

📌 해당 메서드가 특정 예외를 던질 가능성이 있음을 알리는 역할을 합니다. 예외 처리 책임을 메서드 호출부로 넘길 때 주로 사용됩니다.

 

public class Main {
    public static void main(String[] args) {
        try {
            divide(10, 0);  // 0으로 나누기 시도, 예외 발생
        } catch (ArithmeticException e) {
            System.out.println("예외 발생: " + e.getMessage());
        }
    }

    // 예외가 발생할 가능성이 있으므로 throws로 명시
    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            throw new ArithmeticException("0으로 나눌 수 없습니다.");  // 예외 발생
        }
        return a / b;
    }
}

 

 

예외를 handling

📌 말 그대로 예외 처리를 해주면 된다.

1 위험 감지하기 ( throw )
2 위험 처리 ( try-catch(finally) )

 

  • 이건 팁인데, 타인의 코드를 사용할 경우 클래스, 메소드의 에러처리를 잘 해줘야한다. 추후 꼬이면 아주 고통스러운 시간을 보내게 된다...

 

public class StudyException {
    public static void main(String[] args) {
        OurClass ourClass = new OurClass();

        try {
            // 1. 위험한 메소드의 실행을 "시도" 해 봅니다.
            // "시도" 해보는 코드가 들어가는 블럭입니다.
            ourClass.thisMethodIsDangerous();
        } catch (OurBadException e) {
            // 2. 예외가 발생하면, "잡아서" handling 합니다.
            // 예외가 발생하는경우 "handling" 하는 코드가 들어가는 블럭입니다.
						// 즉 try 블럭 내의 구문을 실행하다가 예외가 발생하면
						// 예외가 발생한 줄에서 바로 코드 실행을 멈추고
						// 여기 있는 catch 블럭 내의 코드가 실행됩니다.
            System.out.println(e.getMessage());
        } finally {
            // 3. 예외의 발생 여부와 상관없이, 실행시켜야 하는 코드가 들어갑니다.
            // 무조건 실행되는 코드가 들어가는 블럭입니다.
            System.out.println("우리는 방금 예외를 handling 했습니다!");
        }

    }
}

 

  1. 위험을 감지했다면, try-catch(finally) 키워드 이용하기
    • **try** - **catch**는 각각 중괄호{}를 통해 실행할 코드들을 담습니다.
    • try 단어의 **“시도한다”**라는 뜻에 맞게 중괄호{} 안에는 예외가 발생할 수 있지만 실행을 시도할 코드를 담습니다.
    • catch 단어의 **“잡는다”**라는 의미에 맞게 중괄호{} 안에는 try 안에 있는 코드를 실행하다가 예외가 났을 때 실행할 코드를 담습니다.
      • catch 는 소괄호()를 통해 어떤 예외 클래스를 받아서 처리할지 정의해 주어야 합니다.
      • catch로 모든 예외를 다 받고 싶으면 Exception 을 넣어주면 됩니다.
      • catch로 일부 예외만 받아서 처리하고 싶으면 해당 예외 클래스명을 넣어주면 됩니다.
      • 1개의 try 문에 catch 문은 여러 개 사용할 수 있습니다. ex) 1try : 4catch
    • 기존 **try - catch**의 맨 마지막에 **finally**를 붙여서 마지막에 반드시 실행할 코드를 넣을 수 있습니다.

 

 

모든 클래스에 다 이렇게 넣는가?

  • 당연히 아니다... 핵심적인 로직이 있는 중요한 메서드에만 예외 처리를 추가하거나,
  • 여러 메서드에서 예외가 발생할 수 있는 경우, 상위 메서드(또는 main 메서드 등)에 try-catch 블록을 두어 한 번에 예외를 처리합니다.

 

 

예외 클래스 구조 이해하기

 

자바의 Throwable Class

    • 시작은 모든 객체의 원형인 Object 클래스에서 시작합니다.
    • 아까 정의한 “문제 상황”을 뜻하는 Throwable 클래스가 Object 클래스를 상속합니다.
    • Throwable 클래스의 자식으로 앞서배운 에러(Error)와 예외(Exception) 클래스가 있습니다.
    • 에러(Error) 클래스와 예외(Exception) 클래스는 각각 IOError 클래스, RuntimeException 클래스와 같이 구분하여 처리됩니다.

  • 참고로 그림의 RuntimeException을 상속한 예외들은 UncheckedException, 반대로 상속하지 않은 예외들은 CheckedException으로 구현되어 있습니다.
즉 NullPointException, ArrayIndexOutOfBoundsException 등의 예외 구현체들은 명시적인 에러 처리를 하지 않아도 컴파일 에러가 발생하지는 않겠죠? 또 Checked Exception에 속하는 에러 구현체들은 핸들링 하지 않으면 컴파일 에러가 발생하는 대신, 컴파일이 됐다면 100% 복구가 가능한 에러였다는 것 역시 알아두시면 좋을 것 같습니다.

 

  • 위의 설명대로 exception은 Object 클래스를 통해 많은 예외 처리를 상속 받은 상태이다. 하지만 결국 프로그램의 세계는 무궁무진 하기에 직접 에러를 정의 구현하는 경우도 있다.
class OurBadException extends Exception {
	public OurBadException() {
		super("위험한 행동을 하면 예외처리를 꼭 해야합니다!");
	}
}

 

 

Chained Exception

📌 원인 예외를 새로운 예외에 등록한 후 다시 새로운 예외를 발생시키는데, 이를 예외 연결이라고 합니다.

원인 예외 연결된 예외
b 라이브러리가 없음 함수가 실행 안됨
  • 이렇게 하는 이유는 추후 한번에 묶어서 다루기가 편하기 때문이다.

 

  • 원인 예외를 다루기 위한 메소드
    • initCause()
      • 지정한 예외를 원인 예외로 등록하는 메소드
    • getCause()
      • 원인 예외를 반환하는 메소드
// 연결된 예외 
public class main {

    public static void main(String[] args) {
        try {
            // 예외 생성
            NumberFormatException ex = new NumberFormatException("가짜 예외이유");

            // 원인 예외 설정(지정한 예외를 원인 예외로 등록)
            ex.initCause(new NullPointerException("진짜 예외이유"));

            // 예외를 직접 던집니다.
            throw ex;
        } catch (NumberFormatException ex) {
            // 예외 로그 출력
            ex.printStackTrace();
            // 예외 원인 조회 후 출력
            ex.getCause().printStackTrace();
        }

        // checked exception 을 감싸서 unchecked exception 안에 넣습니다.
        throw new RuntimeException(new Exception("이것이 진짜 예외 이유 입니다."));
    }
}

// 출력
Caused by: java.lang.NullPointerException: 진짜 예외이유

 

 

예외 처리

📌예외 처리 방식은 크게 3가지로 나뉜다

1 예외 복구하기
2 예외 처리 회피하기
3 예외 전환하기

 

예외 복구하기

public String getDataFromAnotherServer(String dataPath) {
		try {
				return anotherServerClient.getData(dataPath).toString();
		} catch (GetDataException e) {
				return defaultData;
		}
}
  • 실제로 try-catch로 예외를 처리하고 프로그램을 정상 상태로 복구하는 방법입니다.
  • 가장 기본적인 방식이지만, 현실적으로 복구가 가능한 상황이 아닌 경우가 많거나 최소한의 대응만 가능한 경우가 많기 때문에 자주 사용되지는 않습니다.

 

예외 처리 회피하기

public void someMethod() throws Exception { ... }

public void someIrresponsibleMethod() throws Exception {
		this.someMethod();
}
  • 이렇게 처리하면, someMethod()에서 발생한 에러가 someIrresponsibleMethod()의 throws를 통해서 그대로 다시 흘러나가게 되겠죠, 물론 같은 객체 내에서 이런 일은 하지는 않습니다, 예외 처리 회피를 보여드리기 위한 단순한 예시 코드입니다.
  • 관심사를 분리해서 한 레이어에서 처리하기 위해서 이렇게 에러를 회피해서 그대로 흘러 보내는 경우도 있습니다.

 

예외 전환하기

public void someMethod() throws IOException { ... }

public void someResponsibleMethod() throws MoreSpecificException {
		try {
			this.someMethod();
		} catch (IOException e) {
			throw new MoreSpecificException(e.getMessage());
		}
}
  • 예외 처리 회피하기의 방법과 비슷하지만, 조금 더 적절한 예외를 던져주는 경우입니다.
  • 보통은 예외 처리에 더 신경 쓰고 싶은 경우나, 오히려 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 바꿔서 던지고 싶은 경우 사용합니다.

< 추가설명 >

 

  • 추상화된 예외로 변환: 구체적인 예외를 숨기고, 상위 계층에서는 필요하지 않은 세부 사항을 가리지 않도록 상위 계층에서 의미 있는 예외로 전환합니다. 예를 들어, 데이터베이스 접근 중 SQLException이 발생하면, 이 예외를 DataAccessException과 같은 의미 있는 커스텀 예외로 바꾸는 식입니다.
  • 런타임 예외로 일괄 처리: Checked Exception을 RuntimeException 계열로 변환하여 전체 코드에서 예외 처리를 일괄적으로 처리할 수 있게 합니다. 예외 처리를 모든 곳에서 강제하지 않아 코드가 더 간결해지죠.

 

 

< 런타임 예외 일괄 처리 부가 설명 >

1. Checked Exception의 특성

  • Checked Exception은 예외가 발생할 가능성이 있는 모든 곳에서 반드시 try-catch 구문으로 처리하거나 throws 키워드를 이용해 메서드 선언부에 던져야 합니다.
  • 이로 인해 예외가 발생할 가능성이 있는 메서드가 여러 군데 있다면, 그곳마다 예외 처리를 반복적으로 작성해야 해서 코드가 길어지고 복잡해질 수 있어요.

2. RuntimeException으로 전환 시 장점

  • RuntimeException 계열 예외는 일명 "unchecked" 예외라고도 불리며, 이 예외는 컴파일러가 예외 처리를 강제하지 않아요.
  • 그래서 try-catch로 처리하지 않아도 컴파일 오류가 발생하지 않습니다.
  • 즉, 반드시 처리하지 않아도 되는 예외로 바뀌므로 코드가 간결해집니다. 예외가 꼭 필요한 상위 레벨에서만 일괄 처리할 수 있어요.

 

'Back-End (Web) > JAVA' 카테고리의 다른 글

[JAVA] 쓰레드 & 람다 함수 & 스트림  (3) 2024.11.13
[JAVA] Generic  (3) 2024.11.12
[JAVA] 인터페이스  (2) 2024.11.12
[JAVA] 클래스 간의 관계와 상속  (0) 2024.11.12
[JAVA] package와 import  (0) 2024.11.12