[JAVA] 예외 처리 (feat. try-with-resources)

FE.1·2024년 4월 16일
4
post-thumbnail

예외(Exception)


프로그램을 실행하다 보면 어떤 원인 때문에 비정상적인 동작을 일으키며 프로그램이 종료되는 상황이 발생한다.

  • 에러의 종류는 컴파일 단계에서 발생하는 컴파일 에러와 실행 중 발생되는 런타임 에러가 있다.
  • 자바에서는 예외를 Checked ExceptionUnchecked Exception으로 분류한다.

Checked Exception

  • 컴파일 시점에 확인되는 예외

  • 예외적인 상황을 안전하게 관리하고, 안정성을 높이기 위해 사용하는데, 체크 예외는 Exception 클래스를 상속하지만, RuntimeException을 상속받지 않는 모든 예외가 여기에 속한다.

  • 명시적으로 처리해야 하며, 이를 위해 try-catch 블록을 사용하거나 throws 키워드를 메서드 선언에 추가하여 예외를 던질 수 있다.


Unchecked Exception

  • 런타임 시점에 확인되는 예외

  • 프로그램의 오류나 잘못된 로직으로 인해 발생한다. RuntimeException 클래스를 상속받아 정의된다.

    • ex. NullPointerException, IndexOutOfBoundsException, IllegalArgumentException
  • 명시적인 처리를 강제하지 않으며, 대부분의 경우 프로그램의 오류를 수정함으로써 해결된다.


스택 트레이스(Stack Trace)

  • 프로그램의 실행 과정에서 호출된 메서드들의 순서와 위치 정보를 나타내는 것

  • 일반적으로 예외가 발생했을 때 예외가 발생한 지점부터 호출 스택의 상위 메서드들까지의 정보를 담고 있다.

    • 예외가 발생한 원인을 추적하고 디버깅하는 데 매우 유용하다는 장점이 있다.
    • try-catch 구문을 사용한 경우, printStackTrace() 메서드를 호출하여 출력할 수 있다.
    • try-catch 구문을 사용하지 않은 경우, 스택 트레이스 정보가 자동으로 출력되지만 오류 발생 지점과 호출 경로를 파악하기 어려워 디버깅과 오류 분석에 제한이 생긴다.
public class ExceptionHandlingRunner {

	public static void main(String[] args) {
		method1(); // 코드 라인 6
	}

	private static void method1() {
		String str = null;
		str.length(); // 코드 라인 수 11
	}
}

실행 결과

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
	at udemy.ExceptionHandlingRunner.method1(ExceptionHandlingRunner.java:11)
	at udemy.ExceptionHandlingRunner.main(ExceptionHandlingRunner.java:6)
  • null값의 길이를 반환하려고 하자 NullPointerException이라는 런타임 에러가 발생되어 스택 트레이스(Stack Trace)가 출력된 것을 확인할 수 있다.

예외 처리 기법


try & catch

  • 예외가 발생할 수 있는 코드를 try 블록 안에 넣고, catch 블록을 사용하여 예외를 처리한다.
  • catch 블록은 필수적 선언이고, finally 블록은 선택적 선언이다.
  • finally 블록은 주로 자원 해제 로직에 사용된다.
public class ExceptionHandlingRunner2 {

	public static void main(String[] args) {
		method1(); // 코드 라인 6
		System.out.println("Main Ended");
	}

	private static void method1() {
		method2(); // 코드 라인 11
		System.out.println("Method1 Ended");
	}

	// try & catch 문으로 method1로 예외를 떠넘기기 방지
	private static void method2() {
		try {
			String str = null;
			str.length(); // 코드 라인 19
			System.out.println("Method2 Ended");
		} catch (NullPointerException ex) {
			System.out.println("Matched NullPointerException");
			ex.printStackTrace();
		} catch (Exception ex) {
			System.out.println("Matched Exception");
			ex.printStackTrace();
		}
	}
}

실행 결과

Matched NullPointerException
Method1 Ended
Main Ended
java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
	at udemy.ExceptionHandlingRunner2.method2(ExceptionHandlingRunner2.java:19)
	at udemy.ExceptionHandlingRunner2.method1(ExceptionHandlingRunner2.java:11)
	at udemy.ExceptionHandlingRunner2.main(ExceptionHandlingRunner2.java:6)
  • method2에서 발생한 예외를 catch문으로 처리하면 상위 메서드인 method1로 예외를 떠넘기지 않는다. 다시 말해, method2에서 적절한 예외 처리를 하지 못하면 method1에서 예외를 던지거나 예외를 처리해야 한다.
  • 예외 처리에도 순위가 존재하여 세부적인 예외를 처리할 경우, 하위 개념 ~ 상위 개념 순으로 catch문을 작성해줘야 한다.

finally문의 필요성

  • 자원 해제를 위해 사용
  • 예외가 존재하든, 안하든, 반환해야 하는 값이 존재하든, 결국에는 실행이 된다.
public class FinallyRunner {

	public static void main(String[] args) {
		Scanner sc = null;
		try {
			sc = new Scanner(System.in);
			int[] numbers = {12, 3, 4, 5};
			int number = numbers[5]; // 코드 라인 12

		} catch (ArrayIndexOutOfBoundsException ex) {
			ex.printStackTrace();
		} finally {
			System.out.println("Before Scanner Close");
			if (sc != null) {
				sc.close();
			}
		}

		System.out.println("Just before closing out main");
	}
}

실행 결과

java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 4
	at udemy.FinallyRunner.main(FinallyRunner.java:12)
Before Scanner Close
Just before closing out main
  • 가끔 정말 확실하게 자원 해제를 확인하고 싶은 경우에 주로 사용하는데 예를 들어 scanner의 생성으로 예외적인 상황이 생길 수도 있다. 이를 확인하기 위해 if(sc ≠ null)을 통해 자원을 해제하면 된다.

[Java7] try-with-resources

  • finally를 명시하지 않아도 자동으로 자원이 해제된다.
  • try & catch 블록에서 catch는 필수적 선언, finally는 선택적 선언이라고 했다.
  • 해당 문법에서는 catch 또한 선택적 선언이다.
public class TryWithResourceRunner {

	public static void main(String[] args) {
		try (Scanner sc = new Scanner(System.in)) {
			int[] numbers = {12, 3, 4, 5};
			int number = numbers[21];
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}

예외 처리 던지기

  • 발생할 수 있는 예외를 메서드 선언부에 throws 키워드를 명시적으로 선언함으로써 메서드를 사용하는 측에서 해당 예외 처리를 위임할 수 있다.
  • UncheckedException의 경우 throws를 선언하지 않아도 된다.
class Amount {
	private String currency;
	private int amount;

	public Amount(String currency, int amount) {
		this.currency = currency;
		this.amount = amount;
	}

	public void add(Amount that) throws Exception {
		if(!this.currency.equals(that.currency)) {
			throw new Exception("Currencies Don't Match " + this.currency + " & " + that.currency);
		}

		this.amount = this.amount + that.amount;
	}

	@Override
	public String toString() {
		return "Amount{" +
			"currency='" + currency + '\'' +
			", amount=" + amount +
			'}';
	}
}

public class ThrowingExceptionRunner {

	public static void main(String[] args) throws Exception {
		Amount amount1 = new Amount("USD", 10);
		Amount amount2 = new Amount("EUR", 20);
		amount1.add(amount2);
		System.out.println(amount1);
	}
}

출력 결과

Exception in thread "main" java.lang.Exception: Currencies Don't Match USD & EUR
	at udemy.Amount.add(ThrowingExceptionRunner.java:14)
	at udemy.ThrowingExceptionRunner.main(ThrowingExceptionRunner.java:42)
  • main 메서드의 선택지는 try & catch로 예외를 처리하거나, throws 키워드를 통해 예외를 던지는 방법이 있다.
  • 해당 코드에서는 throws 키워드를 통해 예외 처리를 위임했다.

사용자 정의 예외 처리 던지기


class Amount {
	private String currency;
	private int amount;

	public Amount(String currency, int amount) {
		this.currency = currency;
		this.amount = amount;
	}

	public void add(Amount that) throws CurrenciesDoNotMatchException {
		if (!this.currency.equals(that.currency)) {
			throw new CurrenciesDoNotMatchException("Currencies Don't Match " + this.currency + " & " + that.currency);
		}

		this.amount = this.amount + that.amount;
	}

	@Override
	public String toString() {
		return "Amount{" +
			"currency='" + currency + '\'' +
			", amount=" + amount +
			'}';
	}
}

// 사용자 정의 예외
class CurrenciesDoNotMatchException extends Exception {
	public CurrenciesDoNotMatchException(String msg) {
		super(msg);
	}
}

public class ThrowingExceptionRunner {

	public static void main(String[] args) throws CurrenciesDoNotMatchException {
		Amount amount1 = new Amount("USD", 10);
		Amount amount2 = new Amount("EUR", 20);
		amount1.add(amount2);
		System.out.println(amount1);
	}
}
profile
공부하자!

2개의 댓글

comment-user-thumbnail
2024년 4월 20일

정말 보기 쉽게 정리해주셨네요! 감사합니다 :)

1개의 답글