예외 처리(Exception Handling)

ppp·2025년 6월 8일

Java 공부

목록 보기
4/13
post-thumbnail

코드를 작성하다 보면 예상치 못한 오류가 발생하는 경우가 자주 있습니다. 예를 들어, 배열의 범위를 초과하거나, 존재하지 않는 파일에 접근하려고 할 때 등등. 이런 오류를 예외(Exception) 라고 부르고, 자바에서는 예외를 안전하게 처리할 수 있는 예외 처리(Exception Handling) 시스템을 제공합니다.

예외 처리는 안정적인 프로그램을 위한 필수 개념입니다.


프로그램 오류의 종류와 런타임 에러

코드를 작성할 때 흔히 마주치는 것이 프로그램 오류예요. 오류에는 여러 종류가 있는데, 대표적으로 아래처럼 나눌 수 있어요.

컴파일 에러

소스코드를 컴파일할 때, 컴파일러가 문법 오류나 자료형 체크 등 기본적인 검사를 수행하면서 발견되는 오류예요. 컴파일 에러를 해결하지 않으면 클래스 파일이 생성되지 않아서 프로그램 실행 자체가 불가능하죠.

런타임 에러

컴파일을 성공적으로 마친 후에도, 프로그램이 실행되다가 예상치 못한 상황(예: 배열의 범위 초과, 0으로 나누기 등)이 발생해 프로그램이 비정상적으로 종료되는 오류를 말해요. 이건 컴파일러가 미리 검증할 수 없는 영역이라서, 실행 도중에만 드러나요.

논리적 에러

프로그램이 정상적으로 실행되지만, 의도와 다른 결과가 나오는 오류예요. 예를 들어, 계산 로직이 잘못되거나 조건문 분기 처리가 잘못된 경우 등이 있죠.


런타임 에러를 피하기 위한 대비

컴파일을 무사히 마쳤다고 안심할 수는 없어요. 프로그램 실행 중에는 생각지도 못한 예외 상황이 발생할 수 있기 때문이에요. 그래서 자바에서는 런타임 시 발생할 수 있는 오류를 ‘에러(error)’‘예외(exception)’로 구분해서 다루고 있어요.

에러(error)

프로그램 코드로는 수습할 수 없는, 아주 심각한 오류예요. 주로 JVM 자체의 문제(OutOfMemoryError, StackOverflowError 등)로, 코드 레벨에서 처리하지 않습니다.

예외(exception)

프로그램 코드로 충분히 수습할 수 있는 오류예요. 예를 들어 파일이 없거나, 네트워크 연결이 끊기는 상황 등을 예외로 다뤄서, 프로그램이 비정상 종료되지 않도록 처리할 수 있어요.

“컴파일 에러 → 컴파일 단계에서 체크”
“런타임 에러 → 실행 중 발생 가능성 대비”


예외 클래스의 계층 구조

자바에서는 프로그램 실행 중에 발생할 수 있는 오류들을 모두 클래스로 정의해두었어요.
이 예외 처리 관련 클래스들도 결국엔 Object 클래스의 자손이라는 점이 공통입니다.

자바의 예외 클래스들은 크게 아래처럼 2가지 그룹으로 나뉘어요.


Exception 클래스

  • 예외 처리의 최고 조상 클래스는 Exception 이에요.

  • 보통 사용자 입력 오류나 외부적인 요인으로 인해 발생하는 예외들이 여기에 속해요.

예시

  • FileNotFoundException : 없는 파일을 입력한 경우

  • ClassNotFoundException : 클래스를 찾을 수 없는 경우

  • DataFormatException : 잘못된 형식의 데이터 입력 등

💡 여기서 ClassNotFoundException도 외부 요인으로 봐요. 예를 들어 사용자가 올바른 클래스 이름을 입력하지 않은 경우나, 클래스 경로를 잘못 설정한 경우를 말합니다.

RuntimeException 클래스

  • RuntimeException 도 Exception 의 자손이에요.

  • 주로 프로그래머의 실수로 인해 발생하는 예외들이 여기에 속합니다.

예시

  • ArrayIndexOutOfBoundsException : 배열의 범위를 벗어난 경우

  • NullPointerException : 값이 null인 참조변수의 멤버를 호출

  • ClassCastException : 클래스간 잘못된 형변환

  • ArithmeticException : 0으로 나누는 실수 등


계층 구조 한눈에 보기

Object
 └── Throwable
      ├── Error
      └── Exception
           ├── RuntimeException
           └── IOException, SQLException, ...

예외 처리하기

프로그래밍을 하다 보면, 예외(exception) 는 언제든 발생할 수 있어요.
에러는 어쩔 수 없지만, 예외는 프로그래머가 미리 대비해서 처리할 수 있습니다.

예외 처리란?

예외 처리(exception handling) 는 프로그램 실행 중에 발생할 수 있는 예외를 미리 처리하는 코드를 작성하는 것을 말해요.

목적설명
비정상 종료 방지프로그램이 갑자기 멈추지 않도록 해줍니다.
정상 상태 유지사용자 경험을 해치지 않고, 다음 작업을 계속할 수 있어요.

만약 예외를 처리하지 않으면, 프로그램은 비정상적으로 종료되며 JVM의 “예외처리기 (UncaughtExceptionHandler)” 가 예외 원인을 화면에 출력하게 됩니다.


try-catch 문 기본 구조

자바에서는 try-catch 문을 사용해 예외를 처리해요.
try 블럭 안에서 예외가 발생하지 않으면, catch 블럭은 실행되지 않아요.

class Main {
	public static void main(String[] args) {
		System.out.println(1);
		try {
			System.out.println(2);
			System.out.println(3);
		} catch (Exception e) {
			System.out.println(4); // 실행X
		}
		System.out.println(5);
	}
}

예외가 발생하면, try 블럭을 바로 빠져나와서 해당 예외를 처리할 수 있는 catch 블럭으로 이동합니다.
catch 블럭 실행이 끝나면 전체 try-catch 문을 벗어나 다음 문장부터 실행이 이어져요.

만약 일치하는 catch 블럭을 찾지 못하면, 예외는 처리되지 않고 프로그램이 비정상적으로 종료됩니다.

class Main {
	public static void main(String[] args) {
		System.out.println(1);

		try {
			System.out.println(0/0); // ArithmeticException 발생
			System.out.println(2);   // 실행X
		} catch (ArithmeticException e) {
			System.out.println(3);
		}

		System.out.println(4);
	}
}

예외 처리 순서

1️⃣ 예외가 발생하면, 해당 예외 클래스의 인스턴스가 만들어져요.
예를 들어, ArithmeticException이 발생하면 ArithmeticException 인스턴스가 생성됩니다.

2️⃣ catch 블럭을 위에서부터 차례대로 검사해요.
이때, instanceof 연산자를 통해 참조 변수 타입과 예외 인스턴스의 타입을 비교합니다.

3️⃣ 검사 결과가 true인 catch 블럭에서 예외를 처리합니다.
만약 일치하는 catch 블럭이 없으면 예외는 처리되지 못하고 프로그램이 종료됩니다.


Exception 타입으로 안전하게 처리하기

모든 예외 클래스는 Exception 클래스의 자손이기 때문에, catch 블럭에서 Exception 타입의 참조 변수를 선언해두면 모든 예외를 처리할 수 있어요.

class Ex8_3 {
	public static void main(String[] args) {
		System.out.println(1);
		System.out.println(2);

		try {
			System.out.println(3);
			System.out.println(0/0);
			System.out.println(4);
		} catch (Exception e) { // ArithmeticException도 처리 가능
			System.out.println(5);
		}

		System.out.println(6);
	}
}

다중 catch 블럭 사용 예시

여러 종류의 예외를 따로 처리해야 할 때, 다중 catch 블럭을 사용하면 좋아요.

class Ex8_4 {
	public static void main(String[] args) {
		System.out.println(1);			
		System.out.println(2);

		try {
			System.out.println(3);
			System.out.println(0/0);
			System.out.println(4);
		} catch (ArithmeticException ae) {
			if (ae instanceof ArithmeticException) 
				System.out.println("true");

			System.out.println("ArithmeticException");
		} catch (Exception e){
			System.out.println("Exception");
		}

		System.out.println(6);
	}
}

finally 블럭

예외 처리에서 try-catch문과 함께 자주 사용되는 게 바로 finally 블럭이에요.
finally는 예외 발생 여부와 상관없이 항상 실행되는 코드를 담는 블럭이에요.

finally 블럭의 특징

  • try나 catch 블럭을 빠져나갈 때 항상 실행돼요.

  • 예외가 발생해도, 발생하지 않아도 무조건 실행됩니다.

  • 자원 정리, 파일 닫기, 네트워크 연결 해제 등 반드시 실행해야 하는 코드를 적을 때 사용해요.

기본 구조

try {
    // 예외가 발생할 수 있는 코드
} catch (Exception e) {
    // 예외 처리 코드
} finally {
    // 항상 실행되는 코드
}

예제 코드

class Main {
	public static void main(String[] args) {
		try {
			System.out.println("try 블럭 실행");
			// 예외 발생 가능 코드
			System.out.println(10/0); // ArithmeticException 발생
		} catch (ArithmeticException e) {
			System.out.println("예외 처리: " + e);
		} finally {
			System.out.println("finally 블럭 실행");
		}
	}
}

출력 결과

try 블럭 실행
예외 처리: java.lang.ArithmeticException: / by zero
finally 블럭 실행

작은 팁

  • try 블럭에서 return을 만나더라도, finally 블럭은 무조건 실행됩니다.

  • 예외 처리의 흐름을 이해할 때 finally를 항상 염두에 두면 좋아요!


예외 정보 얻기: PrintStackTrace()와 getMessage()

자바에서 예외가 발생하면, 그 예외를 처리할 뿐만 아니라 무엇이 문제였는지를 알아내는 것도 중요해요.
이를 위해 예외 클래스의 인스턴스가 제공하는 유용한 메서드 두 가지가 있어요!

PrintStackTrace()

예외가 발생했을 때, 예외의 종류, 메시지, 호출 스택(Call Stack) 정보를 화면에 출력해줘요.

디버깅할 때 매우 유용해요!

getMessage()

예외 인스턴스에 저장된 간단한 예외 메시지를 반환해요.

사용자에게 예외 상황을 간단히 알려줄 때 좋아요.


예제 코드

class Ex8_5 {
	public static void main(String[] args) {
		System.out.println(1);			
		System.out.println(2);

		try {
			System.out.println(3);
			System.out.println(0/0); // ArithmeticException 발생
			System.out.println(4);   // 실행X
		} catch (ArithmeticException ae) {
			ae.printStackTrace(); // 예외 발생 위치까지 콜스택 정보 출력
			System.out.println("예외 메시지: " + ae.getMessage());
		}

		System.out.println(6);
	}
}

출력 결과

1
2
3
java.lang.ArithmeticException: / by zero
	at Ex8_5.main(Ex8_5.java:8)
예외 메시지: / by zero
6

포인트 정리

  • printStackTrace() 는 예외 발생 위치까지의 모든 호출 경로를 알려줘서, 문제를 쉽게 추적할 수 있어요.

  • getMessage() 는 짧은 예외 설명만 간단히 알려줍니다.

실무에서는 둘을 함께 활용해서, 예외의 상세 정보도 로그로 남기고, 사용자에게는 간단한 메시지를 보여주면 좋아요!


예외 발생시키기: throw 키워드

때로는 프로그래머가 직접 예외를 발생시켜야 할 상황이 생길 수 있어요.
이럴 때 자바는 throw 키워드를 통해 고의로 예외를 발생시킬 수 있게 해줍니다.

기본 사용 방법

throw 예외_인스턴스;
  • throw 뒤에 Exception 객체를 넣어주면, 그 순간 예외가 발생해요.

  • 주로 조건 검증 후 문제가 있을 때, 직접 예외를 발생시켜 코드 흐름을 제어할 때 사용합니다.


예제 코드

class Ex8_6 {
	public static void main(String[] args) {
		try {
			Exception e = new Exception("고의로 발생시켰음.");
			throw e;
		} catch (Exception e) {
			System.out.println("에러 메시지 : " + e.getMessage());
			e.printStackTrace();
		}

		System.out.println("프로그램이 정상 종료되었음.");
	}
}

출력 결과

에러 메시지 : 고의로 발생시켰음.
java.lang.Exception: 고의로 발생시켰음.
	at Ex8_6.main(Ex8_6.java:4)
프로그램이 정상 종료되었음.

checked 예외 vs unchecked 예외

자바에서는 예외를 크게 두 가지 종류로 나눌 수 있어요.

1️⃣ checked 예외
2️⃣ unchecked 예외

이 구분을 이해하면 컴파일 에러와 코드 작성 시의 예외 처리 방침을 더 쉽게 정리할 수 있답니다!

checked 예외

  • Exception 클래스와 그 자손들을 말해요.

  • 발생 가능성이 있는 코드에서 반드시 예외 처리를 강제해야 해요.

  • 처리하지 않으면 컴파일 에러가 발생!

class Ex8_7 {
	public static void main(String[] args) {
		throw new Exception(); // 컴파일 에러 발생
	}
}

출력 결과

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	Unhandled exception type Exception

	at Ex8_7.main(Ex8_7.java:3)

unchecked 예외

  • RuntimeException 클래스와 그 자손들을 말해요.

  • 보통 프로그래머의 실수로 인해 발생하는 예외예요.

  • 예외 처리를 강제하지 않기 때문에, 컴파일은 정상적으로 됩니다.

  • 만일 RuntimeException 클래스들에 속하는 예외가 발생할 가능성이 있는 모든 코드에도 예외 처리를 해야 한다면, 참조 변수와 배열이 사용되는 모든 곳에 예외 처리를 해주어야 할거에요

class Ex8_8 {
	public static void main(String[] args) {
		throw new RuntimeException(); // 컴파일 OK!
	}
}

출력 결과

Exception in thread "main" java.lang.RuntimeException
	at Ex8_8.main(Ex8_8.java:3)

checked vs unchecked 예외 차이 정리

구분checked 예외unchecked 예외
대표 클래스Exception과 그 자손RuntimeException과 그 자손
컴파일 시점예외 처리 강제 (컴파일 에러 발생)예외 처리 강제 X (컴파일 OK)
예시IOException, SQLExceptionNullPointerException, ArrayIndexOutOfBoundsException
주로 언제 발생?외부적 오류, 예측 가능한 상황프로그래머 실수, 잘못된 로직
  • checked 예외: 외부적인 상황으로 발생 → 예외 처리 반드시 필요
  • unchecked 예외: 프로그래머 실수 → 예외 처리 강제 X, 필요 시 직접 처리

메서드에 예외 선언하기: throws 키워드

메서드에서 발생할 수 있는 예외를 호출한 쪽으로 넘겨주기 위해 사용하는 키워드가 바로 throws 입니다.
throws 는 메서드 선언부에 작성하며, 여러 예외가 있을 경우 쉼표(,)로 구분해 적어줍니다.

void method() throws Exception1, Exception2, ExceptionN {}

예외 선언의 목적

예외가 발생했을 때 자체적으로 처리할 수 없는 경우,
예외를 자신을 호출한 메서드에게 넘겨주기 위해 throws 를 사용합니다.

하지만 이렇게 예외를 넘긴다고 해서 예외가 처리된 건 아니고,
언젠가는 반드시 try-catch문으로 처리해야 한다는 점을 기억해 주세요


예외 전달 흐름 예제

예외가 선언되어 있으면 Exception과 같은 checked 예외를 try-catch문으로 처리하지 않아도 컴파일 에러가 발생하지 않습니다.

class Ex8_9 {
	public static void main(String[] args) throws Exception {
		method1();
  	}

	static void method1() throws Exception {
		method2();
	}

	static void method2() throws Exception {
		throw new Exception();
	}
}

출력 결과

Exception in thread "main" java.lang.Exception
	at Ex8_9.method2(Ex8_9.java:11)
	at Ex8_9.method1(Ex8_9.java:7)
	at Ex8_9.main(Ex8_9.java:3)

흐름 설명

1️⃣ method2() 에서 예외가 발생하지만, 처리하지 않고 선언만 해둠

2️⃣ method1()method2() 의 예외를 다시 넘겨줌

3️⃣ main() 도 예외를 처리하지 않아, 결국 JVM의 예외 처리기로 전달 → 프로그램 비정상 종료


실전 예제: 파일 이름 검증

import java.io.*;

class Ex8_10 {
	public static void main(String[] args) {
		try {
			File f = createFile(args[0]);
			System.out.println(f.getName() + "파일이 성공적으로 생성되었습니다.");
		} catch (Exception e) {
			System.out.println(e.getMessage() + " 다시 입력해 주시기 바랍니다.");
		}
	}	

	static File createFile(String fileName) throws Exception {
		if (fileName == null || fileName.equals("")) {
			throw new Exception("파일이름이 유효하지 않습니다.");
		}
		
		File f = new File(fileName);
		f.createNewFile();
		
		return f;
	}
}

출력 결과

test파일이 성공적으로 생성되었습니다.
파일이름이 유효하지 않습니다. 다시 입력해 주시기 바랍니다.
  • throws 를 사용하면 예외를 메서드 밖으로 전달할 수 있습니다.
  • 언제 처리할지 결정할 수 있는 유연성을 주지만, 결국에는 반드시 처리해야 함을 잊지 마세요.
  • 외부에서 발생할 가능성이 높은 예외(예: 파일 처리, 네트워크 연결 등)는 이런 방식으로 예외를 넘기고 호출부에서 처리하는 경우가 많아요.

사용자 정의 예외 만들기

자바에서 기본적으로 제공되는 예외 클래스들도 충분히 유용하지만,
상황에 따라 예외를 직접 만들어서 더 명확하게 코드의 의미를 표현할 수도 있습니다.
이렇게 만든 예외를 사용자 정의 예외 라고 부릅니다.


기본 개념

  • 기존 예외 클래스 (Exception, RuntimeException 등)를 상속해서 만듭니다.

  • 예외 메시지나 추가 정보(에러 코드 등) 를 함께 담을 수 있어,상황에 딱 맞는 예외를 정의할 수 있습니다.

  • 최근에는 checked 예외보다는 unchecked 예외(RunTimeException 상속) 로 작성하는 경우가 더 많습니다.


사용자 정의 예외 예제

아래 예시는 Exception을 상속받아 에러 메시지 + 에러 코드를 함께 담는 예외 클래스입니다.

class MyException extends Exception {
	private final int ERR_CODE;

	MyException(String msg, int errCode) {
		super(msg); // Exception 클래스의 생성자 호출 (메시지 저장)
		ERR_CODE = errCode;
	}

	MyException(String msg) {
		this(msg, 100); // 기본 에러 코드 100으로 설정
	}

	public int getErrCode() {
		return ERR_CODE;
	}
}

class Main {
	public static void main(String[] args) {
		try {
			throw new MyException("사용자 정의 예외 발생!", 404);
		} catch (MyException e) {
			System.out.println("에러 메시지: " + e.getMessage());
			System.out.println("에러 코드: " + e.getErrCode());
		}
	}
}

출력 결과

에러 메시지: 사용자 정의 예외 발생!
에러 코드: 404

연결된 예외 (Chained Exception)

프로그램을 작성하다 보면, 하나의 예외가 다른 예외를 발생시키는 경우가 생기곤 합니다.
이렇게 서로 연결된 예외를 연결된 예외(Chained Exception) 라고 부릅니다.


기본 개념

  • 원인 예외 : 새로운 예외가 발생하기 전에 발생했던 예외

  • 새로운 예외 : 원인 예외를 감싸서 던져지는 예외

Throwable 클래스(예외의 최상위 클래스)가 제공하는 두 가지 메서드로 연결할 수 있습니다.

메서드설명
initCause(Throwable cause)원인 예외 등록하기
getCause()등록된 원인 예외 가져오기

왜 연결된 예외를 사용할까?

1️⃣ 다양한 예외를 하나로 묶어 처리

  • 서로 상속 관계가 아닌 예외도 원인 예외로 연결하면, 하나의 큰 예외로 다룰 수 있어요.

2️⃣ checked 예외를 unchecked 예외로 변환

  • 예를 들어, Exception을 RuntimeException으로 감싸면 예외 처리 강제성을 해제할 수 있어요.

예제 코드

아래는 설치 중 발생할 수 있는 공간 부족 예외(SpaceException)설치 예외(InstallException) 로 감싸서 던지는 예제입니다.

try {
	startInstall();
	copyFiles();
} catch (SpaceException e) {
	InstallException ie = new InstallException("설치 중 예외발생");
	ie.initCause(e); // 원인 예외 등록
	throw ie;
}

checked 예외를 unchecked 예외로 변환하는 예제입니다.

throw new RuntimeException(new MemoryException("메모리가 부족합니다."));

마무리하며

이번 글에서는 자바의 예외 처리의 기본 개념부터, try-catch-finally 구조, throw/throws 키워드, 사용자 정의 예외, 그리고 연결된 예외까지 정리했습니다.

예외 처리는 단순히 “에러를 막기 위한 기술”을 넘어서, 프로그램의 안정성과 유지보수성을 높이는 중요한 부분입니다.
어떤 예외를 어떻게 처리할지, 어떤 예외를 호출자에게 넘길지를 고민하는 습관은 개발자로서의 역량을 한 단계 더 업그레이드시켜줄 거예요.

0개의 댓글