[Java] Java 기초 - 예외, 에러 처리

Hyunjun Kim·2025년 4월 5일
0

Data_Engineering

목록 보기
22/153

10. 예외, 에러 처리

프로그램을 만들다 보면 다양한 예외상황 또는 에러가 발생하곤 한다. 에러를 처리하는 방법에 대해 알아보자

10.1 예외 처리의 목적

10.1.1 예외처리란(Exception, Error Handling)

코드를 완벽하게 짰다고 해서 항상 프로그램이 성공적으로 도는 것은 아니다. 다양한 예외 상황이 발생할 수 있고, 이것에 대응하기 위해서 예외 처리 코드가 필요하다.

10.1.2 예외처리의 목적

  1. 예외의 발생으로 인한 실행 중인 프로그램의 비정상 종료를 막기 위해서
  2. 개발자에게 알려서 코드를 보완할 수 있도록 하게 위해서 (메소드 시그니처와 함께 사용)

10.2 자바의 예외 클래스 위계구조 (hieararchy)

자바에서는 상속을 이용해서 모든 예외를 표현한다. 모든 클래스는 Object를 상속받는다.
모든 예외 클래스는 그 중 Throwable의 자손 클래스다. (extend로 상속받는)

이미지 출처 : https://www.protechtraining.com/bookshelf/java_fundamentals_tutorial/exceptions

Java에서는 예외 처리를 위해 Throwable 클래스를 기준으로 두 가지 주요 하위 클래스가 존재한다.

10.2.1 Throwable에는 크게 두 종류의 자식 클래스가 있다

1) Error

시스템 수준의 치명적인 문제로 인해 프로그램이 강제 종료된다.
대부분 JVM 또는 시스템 리소스와 관련된 문제로 인해 발생한다.

  • 예외 처리를 통해 복구할 수 없기 때문에 try-catch로 처리하지 않는다.

대표적인 하위 클래스

  • StackOverflowError

    • 메서드를 재귀 호출할 때 스택 메모리의 한계를 초과하면 발생한다.
    • 예: 무한 재귀 호출
  • OutOfMemoryError (OOM)

    • JVM의 힙 메모리가 부족할 경우 발생한다.
    • Java는 운영체제로부터 힙 메모리를 할당받아 사용하는데, 그 한계를 초과하면 프로그램이 종료된다.
  • LinkageError

    • 클래스나 라이브러리 간의 링크 오류가 발생했을 때 나타난다.
    • 예: 잘못된 라이브러리 연결, 클래스 버전 불일치 등

2) Exception

일반적인 예외 상황을 나타내며, 예외 처리를 통해 복구할 수 있는 경우가 많다. 프로그램을 종료하지 않고 문제를 우회하거나 적절히 처리할 수 있다.


Checked Exception (컴파일 시 예외 처리 강제)

  • 대표적으로 I/O 관련 예외가 여기에 해당된다.

IOException
외부 자원 (파일, 네트워크 등)과의 입출력 중 발생한다.

  • 예: 파일이 존재하지 않거나 디스크에 쓰기를 실패하는 경우
  • 하위 클래스 예:
    - SocketException : 네트워크 통신 시 포트가 이미 사용 중이거나 소켓을 열 수 없는 경우 발생한다.

Unchecked Exception (RuntimeException 계열)

  • 실행 도중 발생하는 예외이며, 컴파일러가 예외 처리를 강제하지 않는다.

RuntimeException
논리적 오류나 잘못된 프로그래밍으로 인해 발생한다.

  • 하위 클래스 예:
    - IllegalArgumentException : 메서드에 전달된 파라미터가 유효하지 않을 때 발생한다.

10.2.2 커스텀 예외는 어떻게 만들어야 할까?

대부분의 커스텀 예외 클래스는 RuntimeException을 상속받는 방식으로 설계한다. 이유는 다음과 같다:

  • 예외 처리를 선택적으로 할 수 있다 (try-catch 처리 선택 가능).
  • 프로그램의 정상적인 흐름을 깨뜨리지 않고 예외 상황을 제어할 수 있다.

예외 클래스는 프로그램의 안정성과 유지보수성을 높이기 위해 체계적으로 설계되어 있다. 상황에 맞는 예외 클래스를 상속하고, 의미 있는 예외 메시지를 남기는 것이 중요하다.

10.3 사용자 정의 예외 클래스

10.2 그림처럼 자바에 미리 정의 되어있는 예외 클래스 들이 있다. 기본적으로 이미 있는 것을 사용하시되, 필요한 것으로 표현할 수 없거나 구체적인 목적을 가진 예외를 정의하고 싶다면, Throwable 또는 그 하위에 있는 예외 클래스를 상속받아서 자신만의 예외 클래스를 정의할 수 있다.

  • 우리가 표현하려는 예외 상황은 대부분 Exception 종류일 것이다.
  • 실행도중 발생하는 Exception은 RuntimeException 을 상속받아서 정의하자
  • 파일을 읽고 쓰거나, 원격에 있는 저장소로부터 데이터를 읽고 쓸 때 나는 에러를 표현하려면 IOException 을 상속받아서 정의하자
  • 메소드 등에 잘못된 인자(parameter, arguments)를 받은 경우에는 IllegalArgumentException 을 사용
  • Error 는 프로그램이 강제로 종료되어야 하는 경우에만 상속에서 에러 클래스를 구현하고, 해당 에러가 발생하면 프로그램이 종료되도록 구현해야한다.

10.4 예외 처리

10.4.1 try-catch

1) 기본구조

try {
	// 예외가 발생할 가능성이 있는 코드를 구현한다.
} catch (FileNotFoundException e) {
	// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현.
} catch (IOException e) {
	// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현.
}

catch는 예외 처리를 하고 싶은 최상위 클래스의 타입으로 변수를 catch의 괄호 안에 선언한다.

  • Throwablecatch문을 작성하면, Throwable은 모든 예외의 부모이기 때문에 모든 예외으 경우가 잡힌다.
    catch는 이어서 작성할 수 있고, 예외 발생시, 순차적으로 확인한다.

2) 핵심 포인트

  • try: 예외가 발생할 수 있는 코드 작성
  • catch: 특정 예외 타입을 잡아 처리
    • 자식 클래스부터 순서대로 작성해야 한다.
      예: IOExceptionException
  • 여러 catch 블럭을 순차적으로 사용할 수 있으며,
    앞 블럭에서 예외가 처리되면 뒤 블럭은 실행되지 않는다.

앞의 catch 블럭에서 잡혔다면, 뒤의 catch 블럭으로는 전파되지 않는다. 좁은 범위의 예외부터 앞에 선언하는 것이 좋다. 여기서 좁은 범위란 상속관계에서 자식 클래스에 위치 할수록 좁은 범위다. 예를 들어서 IOException 이 발생할 것 같아 예외처리를 하고, 그 외의 예외도 예외처리를 하고 싶다면 IOException 을 catch 하는 구문을 먼저, Exception 을 catch 하는 구문을 그 뒤에 작성한다. catch 를 앞에서 했지만 에러를 또 전파하고 싶다면, catch 문 안에서 throw 로 예외를 발생시키면 된다.

3) 범용 예외 처리

  • catch (Throwable e) 사용 시, 모든 예외와 에러를 포괄 가능
    → 단, 일반적인 예외 처리에서는 지양

4) 예외 전파

  • catch 블럭에서 throw로 예외를 다시 발생시켜
    상위 호출자에게 전파할 수 있다.
catch (IOException e) {
    // 로그 처리 등
    throw e; // 예외 재전파
}

FileNotFoundException은 특별히 처리하고, FileNotFoundException 포함해서 모든 IOException은 뭔가 처리를 하고 싶은 경우 throw로 다음 catch문에 전파할 수 있다. 하지만 계속 전파하는 경우는 권장하지 않고 다른 디자인을 하는 것을 추천. 하지만 필요하면 써야지.

try{
	//my code
}catch (FileNotFoundException e){
	// my code
	throw e;
}catch (IOException e){

}

10.4.2 try-catch-finally

try {
	// 예외가 발생할 가능성이 있는 코드를 구현합니다.
} catch (FileNotFoundException e) {
	// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현합니다.
} catch (IOException e) {
	// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현합니다.
} finally {
	// 예외의 발생여부에 관계없이 항상 수행되어야하는 코드를 구현합니다.
}
  • finally 구문은 필수는 아니다.
  • 만약, 예외가 발생하지 않는다면 try → finally 순으로 실행됨.

1) try-catch-finally 예제

public class Main {
	public static void main(String[] args) {
		int number = 10;
		int result;

		for (int i = 10; i >= 0; i--) {
			try {
				result = number / i;
				System.out.println(result);
			} catch (Exception e) {
				System.out.println("Exception발생: " + e.getMessage());
			} finally {
				System.out.println("항상 실행되는 finally 구문");
			}
		}
	}
}

10.4.3 try-with-resource

1) try-with-resource 형식

import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
	public static void main(String[] args) {
	
    	try (FileOutputStream out = new FileOutputStream("test.txt")) {
			// test.txt file 에 Hello Sparta 를 출력
			out.write("Hello World".getBytes());
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

try 구문 안에서만 사용되는 자원을 try-catch 구문이 끝나면 자동으로 닫을 수 있도록 할 수 있도록 하는 문법이다.

  • 입출력(I/O)과 함께 자주 쓰이는 구문이다.
  • 기존의 try-catch(-finally)문은 자원을 닫고 싶다면 명시적으로 Closable.close() 를 사용해야 한다.
  • try-with-resource문은 try문을 벗어나는 순간 자동적으로 close() 가 호출된다.
  • 단, try()안에서 입출력 스트림을 생성하는 객체가 AutoClosable 인터페이스를 구현한 객체여야 한다.

2) AutoClosable 인터페이스를 사용하는 이유

AutoClosable 인터페이스에는 예외가 발생할 경우 close() 메소드를 호출하기로 정의되어있기 때문에

3) try-with-resource 사용하지 않는다면?

만약에 try-with-resource가 아니라 일반 try catch문을 사용했다면 아래와 같은 코드가 된다. 코드가 길어질 뿐만 아니라 FileOutputStream 을 열고 닫을때 생기는 Exception 까지 그 상위에서 catch를 하거나 throws 로 감싸줘함.

import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
	public static void main(String[] args) throws IOException {
		FileOutputStream out = new FileOutputStream("test.txt");
		try {
			// test.txt file 에 Hello Sparta 를 출력
			out.write("Hello World".getBytes());
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
		out.close();
	}
}

10.4.4 메소드에서의 예외 선언

void methodThrowsException() throws Exception {
//메소드의 내용
}

void methodThrowsRuntimeException() throws RuntimeException {

}

void caller() {
	// methodThrowsException(); compile error
	try {
		methodThrowsException()
	} catch (Exception exception) {
	}
	
    methodThrowsRuntimeException(); // no compile error
}

catch문을 이용해서 예외처리를 하지 않은 경우, 메소드에 throws로 예외가 발생할 수 있다는 것을 알려줘야 함.
throws 키워드가 있는 함수를 호출한다면, caller 쪽에서 catch와 관련된 코드를 작성해줘야 함.

이처럼, 키워드 throws를 사용해서 메소드 내에서 발생할 수 있는 예외, caller 쪽에서 꼭 처리해주어야하는 예외를 적어주면 된다.

단, java.lang.RntimeException 과 그것을 상속받은 예외 클래스는 throws 가 있고 caller에서 그 처리를 안해주더라도 compile error가 발생하지 않는다. 사용자 예외 클래스를 throws 에 대해서 예외처리를 강제하고 싶은 경우에는 java.lang.Exception 클래스를 상속받아서 구현하자.

10.5 예외, 에러 처리 퀴즈

10.5.1 퀴즈 1

다음 스니펫에 있는 divide() 함수는 매개변수(parameter)에 들어오는 값에 따라서 ArithmeticException
ArrayIndexOutOfBoundsException 이 발생할 수 있다.

  1. throws 키워드를 통해서 divide() 함수에서 발생할 수 있는 exception의 종류가 무엇인지 알게 해주세요.
  2. Main 함수에서 try-catch 문을 이용해서, 다음 동작을 구현하세요.
    a. ArithmeticException 이 발생할 때는 잘못된 계산임을 알리는 문구를 출력하세요.
    b. ArrayIndexOutOfBoundsException 이 발생할 때는 현재 배열의 index범위를 알려주는 문구를 출력하세요.

[코드스니펫] 예외처리 퀴즈 - Main 함수

class ArrayCalculation {

	int[] arr = { 0, 1, 2, 3, 4 };

	public int divide(int denominatorIndex, int numeratorIndex) {
	return arr[denominatorIndex] / arr[numeratorIndex];
	}
}

public class Main {
	public static void main(String[] args) {
		ArrayCalculation arrayCalculation = new ArrayCalculation();
	
    	System.out.println("2 / 1 = " + arrayCalculation.divide(2, 1));
		System.out.println("1 / 0 = " + arrayCalculation.divide(1, 0)); // java.lang.ArithmeticException: "/ by zero"
		System.out.println("Try to divide using out of index element = " 
        + arrayCalculation.divide(5, 0)); // java.lang.ArrayIndexOutOfBoundsException: 5
	}
}

10.5.2 퀴즈 2

입력한 경로의 파일을 여는 프로그램을 만드시오.
접근 불가능한 경로를 접근했다는 사용자 정의 Error 클래스를 만드시오.(Error 타입을 상속)
경로가 /Users 경로의 하위 경로가 아니라면, 접근 불가능한 경로를 접근했다는 Error를 발생시키고, 프로그램이 강제종료 된다는 문구를
출력하고, 프로그램을 강제 종료 System.exit(1) 하시오.
접근 가능한 경로라면, 실제 파일이 존재하는지를 출력하고 정상 종료 하시오.

참고: 문자열 입력 받는 법

Scanner scanner = new Scanner(System.in);
String path = scanner.nextLine();

참고: File 확인하는 법

File file = new File("경로");
if(file.exists()) {
	// file 존재
} else {
	// 해당 파일 없음
}

답안

FileValidator.java

public class FileValidator {
	public static boolean validate(String path) throws IllegalPathAccessError {
		if(path.startsWith("/Users/")){
			File file = new File(path);
			return file.exists();
		} else {
			throw new IllegalPathAccessError(path);
		}
	}
}

IllegalPathAccessError

public class IllegalPathAccessError extends Error{
	private String path;
	public IllegalPathAccessError(String path) {
		super();
		this.path = path;
	}

	@Override
	public String getMessage() {
		return path + " is not allowed to access.";
	}
}

Main.java

import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		String path = scanner.nextLine();

		try {
			if(FileValidator.validate(path)) {
				System.out.println("File " + path + " exists.");
			} else {
				System.out.println("File " + path + " doesn't exist.");
			}
		} catch (IllegalPathAccessError illegalPathAccessError) {
			System.out.println(illegalPathAccessError.getMessage() + "\n");
			illegalPathAccessError.printStackTrace();
			System.out.println("Program is forced to quit.");
			System.exit(1);
		}
	}
}
profile
Data Analytics Engineer 가 되

0개의 댓글