본문 바로가기

TIL/Java

[Java] 예외 처리(Exception) - Throws, try-catch, multi-catch, 사용자정의 Exception, try-with-resource, Exception 오버라이딩

오류(Error)

- 시스템 상에서 프로그램에 심각한 문제를 발생하여 실행중인 프로그램이 종료되는 것

- 이러한 오류는 개발자가 미리 예측하여 처리하는 것이 불가능하다. 오류에 대한 처리는 할 수 없다.

 

예외(Exception)

- 오류와 마찬가지로 실행 중인 프로그램을 비정상적으로 종료시키지만 발생할 수 있는 상황을 미리 예측하고 처리할 수 있는 미약한 오류

- 개발자는 이러한 예외에 대해 예외 처리를 통해 예외 상황을 적절히 처리하여 코드의 흐름을 컨트롤 할 수 있다.

 

예외클래스 계층 구조

- Exceptio과 Error클래스 모두 Throwable 클래스 자손이다.

- 예외 클래스들의 최상위 클래스는 Exception 클래스이며 예외처리를 해야하는 Checked Exception과 해주지 않아도 되는 Unchecked Exception으로 나뉜다.

 

예외 처리 방법

1. throws로 위임

2. try-catch로 처리


throws로 위임 - ExceptionTest클래스

- Exception 처리를 호출한 메소드에게 위임

- 강제로 예외 발생
- 예외를 발생시킨 뒤 메소드 헤드에 throws 구문을 추가한다.
- 예외를 발생시킨 쪽에서는 throws로 예외에 대한 책임을 위임해서 해당 예외에 대한 처리를 강제화 시킨다.

//예외를 발생시키는 메소드 작성
	public void checkEnoughMoney(int price, int money) throws Exception {
		
		System.out.println("가지고 계신 돈은 " + money + "원 입니다.");
		
		if(money >= price) {
			System.out.println("상품을 구입하기 위한 금액이 충분합니다.");
		} else {
			
			throw new Exception();
		}
		/* 예외가 발생하지 않는 경우에만 실행한다 */
		System.out.println("즐거운 쇼핑하세요~");
	}

 

Application1

- 예외 발생 구문 이하 구문은 동작하지 않고 되돌아온다. (상품가격 50000, 가진 돈 10000 코드 이하구문)
- 메인메소드 또한 예외를 처리하지 않고 위임했다.
- 따라서 프로그램은 비정상적으로 종료되고 "프로그램을 종료합니다." 부분은 출력되지 않는다.

		ExceptionTest et = new ExceptionTest();
        		
		/* 상품 가격 10000원, 가진 돈 50000원 */
		et.checkEnoughMoney(10000, 50000);//정상 동작한다.
		
		/* 상품 가격은 50000원, 가진 돈 10000원 */
		et.checkEnoughMoney(50000, 10000);
		
		System.out.println("프로그램을 종료합니다.");


try-catch를 이용한 방법(Application2)

- Exception이 발생한 곳에서 직접 처리

- try : exception 발생할 가능성이 있는 코드를 안에 기술

- catch : try 구문에서 exception 발생시 해당하는 exception에 대한 처리 기술

           여러 개의 exception 처리가 가능하나 exception간의 상속 관계 고려

- finally : exception 발생 여부와 관계없이 꼭 처리해야 하는 로직 기술

            중간에 return문을 만나도 finally구문은 실행되지만 System.exit();를 만나면 무조건 프로그램 종료

		ExceptionTest et = new ExceptionTest();
		
		try {
			/* 상품 가격 10000원, 가진 돈 50000원 - 예외 발생하지 않음 */
			//et.checkEnoughMoney(10000, 50000);
			
			/* 상품 가격 50000원, 가진 돈 10000원 - 예외 발생 */
			et.checkEnoughMoney(50000, 10000);
			
			System.out.println("==================== 상품 구입 가능 =================");
		} catch (Exception e) {

			System.out.println("=================== 상품 구입 불가 =================");
		}
		
		/* 프로그램 종료 확인 */
		System.out.println("프로그램을 종료합니다.");
	}


사용자 정의 예외

-Exception 클래스를 상속받아 예외 클래스를 작성하는 것

 

NotEnoughMoneyException 클래스

- 사용자 정의 예외 클래스를 만들기 위해서는 Exception 클래스를 상속 받으면 된다.

- Exception 클래스는 Throwable 클래스를 상속 받아 구현 되어 있다.

- Throwable은 Error와 Exception 두 가지를 추상화 해서 만들었다.

- 예외는 Exception을 가장 최상위 클래스로 여긴다.

	/* 기본 생성자 */
	public NotEnoughMoneyException () {}
	
	/* 문자열을 부모 생성자 쪽으로 전달하며 초기화 하는 생성자 */
	public NotEnoughMoneyException(String message) {
		/* 예외 인스턴스 생성 시점에 전달한 예외 메세지를 부모 생성자 쪽으로 전달해서 인스턴스를 생성한다. */
		super(message);
	}

=> money와 price가 각각 음수로 입력되는 경우 일반적인 상식에서 벗어나는 프로그램이 된다.

     만약 각각 음수로 입력되는 경우 발생시킬 예외를 NegativeException으로 정의한다.

     그리고 이를 상속받는 PriceNegativeExceptionMoneyNegativeException을 정의한다

 

NegativeException 클래스 (최상위 클래스인 Exception을 상속받는다.)

public class NegativeException extends Exception{

	public NegativeException() {}
	
	public NegativeException(String message) {
		super(message);
	}
}

 

PriceNegativeException 클래스 (NegativeException을 상속받는다.)

public class PriceNegativeException extends NegativeException{

	public PriceNegativeException() {}
	
	public PriceNegativeException(String message) {
		super(message);
	}
}

 

MoneyNegativeException 클래스 (NegativeException을 상속받는다.)

public class MoneyNegativeException extends NegativeException{

	public MoneyNegativeException() {}
	
	public MoneyNegativeException(String message) {
		super(message);
	}
}

 

ExceptionTest 클래스

- 3개의 예외 클래스는 모두 Exception 클래스의 후손이므로 Exception 만으로도 처리할 수 있다.

- 이전의 예시들은 Exception으로 예외를 발생시켰지만 그냥 예외일 뿐, 예외 클래스의 이름만으로 어떤 예외가 발생했는지 알 수 있도록 하기 위해서 사용자 예외 클래스들을 상단에서 추가해줬다. 

public class ExceptionTest {
	
	public void checkEnoughMoney(int price, int money) 
			throws PriceNegativeException, MoneyNegativeException, NotEnoughMoneyException {
		//throws Exception{
		
		/* 먼저 상품 가격이 음수인지 확인하고, 음수인 경우 예외를 발생시킨다. */
		if(price < 0) {
			throw new PriceNegativeException("상품 가격은 음수일 수 없습니다.");
		}
		
		/* 가진 돈도 음수인지 확인하고, 음수인 경우 예외를 발생시킨다. */
		if(money < 0) {
			throw new MoneyNegativeException("가지고 있는 돈은 음수일 수 없습니다.");
		}
		
		/* 위의 두 값이 정상 입력 되었더라도 상품 가격이 가진 돈보다 큰 경우 예외 발생 */
		if(money < price) {
			throw new NotEnoughMoneyException("가진 돈 보다 상품 가격이 더 비쌉니다.");
		}
		
		/* 모든 조건을 만족하는 경우 정상적으로 물건 구입 가능 */
		System.out.println("가진 돈이 충분합니다. 즐거운 쇼핑 하세요~");
	}

 

Application1 클래스 (가진 돈이 충분할 경우) 

- 사전에 정의 되어 있는 Exception의 종류는 굉장히 많이 있다.
- 하지만 RuntimeException의 후손 대부분은 예외 처리를 강제화 하지 않는다.
- 간단한 조건문 등으로 처리가 가능하기 때문에 따로 강제화 하지 않았다.

- 정상적인 흐름을 만들기 위해서 try-catch 블럭을 작성하는게 좋다.

public class Application1 {

	public static void main(String[] args) {

		ExceptionTest et = new ExceptionTest();
		
		try {
			/* 상품 가격보다 가진 돈이 적은 경우 */
			/* 실행해보면 예외 종류와 에러 메세지가 출력 된다. */
			//et.checkEnoughMoney(50000, 30000);
			
			/* 상품 가격을 음수로 입력한 경우 */
			//et.checkEnoughMoney(-50000, 50000);
			
			/* 가진 돈을 음수로 입력한 경우 */
			//et.checkEnoughMoney(50000, -50000);
			
			/* 정상적으로 구매가 가능한 돈을 가진 경우 
			 * 위의 코드를 주석하지 않으면 이 코드는 동작하지 않는다. catch 블럭으로 가는 것이다.
			 * */
			et.checkEnoughMoney(30000, 50000);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}

 

Application2 클래스 (예외 상황 별로 catch 블럭을 여러개 작성해서 처리 가능)

public class Application2 {

	public static void main(String[] args) {
		
		ExceptionTest et = new ExceptionTest();
		
		try {
			/* 예외 발생 가능성이 있는 메소드 호출 */
			et.checkEnoughMoney(-30000, 50000);
			
		/* 예외 상황 별로 catch 블럭을 따로 작성해서 처리할 수 있다. */	
		} catch (PriceNegativeException e) {
			
			/* 예외 인스턴스 생성 시 전달한 메세지를 getMessage()로 가져올 수 있다. */
			System.out.println("PriceNegativeException 발생!");
			System.out.println(e.getMessage());
			
		} catch (MoneyNegativeException e) {
			
			System.out.println("MoneyNegativeException 발생!");
			System.out.println(e.getMessage());
			
		} catch (NotEnoughMoneyException e) {
			
			System.out.println("NotEnoughMoneyException 발생!");
			System.out.println(e.getMessage());
			
		} finally {
			/* 예외 발생 여부와 상관 없이 실행할 내용 */
			System.out.println("finally 블럭의 내용이 동작함");
		}
		
		/* 프로그램을 종료하는지 확인 */
		System.out.println("프로그램을 종료합니다.");
	}
}

 

Application3 (multi-catch)

- JDK 1.7에서 추가된 구문

- 동일한 레벨의 다른 타입의 예외를 하나의 catch 블럭으로 다룰 수 있다.

- Exception의 상속관계를 고려해서 나열해야 한다. 

  => Exception은 모든 타입의 Exception의 부모이기 때문에 하위 Exception들보다 먼저 작성하면 X

public class Application3 {

	public static void main(String[] args) {

		ExceptionTest et = new ExceptionTest();
		
		/* 예외 발생 가능성이 있는 메소드 호출 */
		try {
			et.checkEnoughMoney(20000, -10000);
		}
		/* catch 블럭을 나열하는 경우 각 Exception의 상속 관계를 고려해서 나열한다.
		/* catch(Exception e) {
		System.out.println("모든 종류의 Exception 발생!");}*/ 
            
            catch (PriceNegativeException | MoneyNegativeException | NotEnoughMoneyException e) {
			/* 예외 클래스명, 예외발생 위치, 예외 메세지 등을 
			 * stack 호출 역순으로 빨간색 글씨를 이용해서 로그 형태로 출력해주는 기능 */
			e.printStackTrace();
			
			System.out.println(e.getClass() + " 발생!");
			System.out.println(e.getMessage());
		} finally {
			System.out.println("finally 블럭의 내용이 동작함");
		}
		
		System.out.println("프로그램을 종료합니다.");
	}
}


try-catch 블럭에 대한 이해 코드

- 위에서 배운 예외처리를 가장 많이 활용하는 것이 io(input/output) 패키지이다.
- 아직 io를 배우지 않았지만 io 문법보다는 try-catch 블럭의 실제 사용과 흐름에 집중해서 살펴보자.

	public static void main(String[] args) {

		/* finally 블럭에서 사용하려면 레퍼런스 변수를 try 블럭 밖에서 선언해야 한다. */
		BufferedReader in = null;
		
		try {
			/* FileReader라는 클래스의 생성자에 예외를 throws 해 놓았다.
			 * 사용하는 쪽에서 반드시 예외 처리를 해야하기 때문에 try-catch 블럭 안에서 
			 * 생성자를 호출하여 인스턴스를 생성해야 한다.
			 * */
			in = new BufferedReader(new FileReader("test.dat"));
			
			String s;
			
			/* readLine() 메소드도 IOException을 throws 하므로
			 * catch 블럭을 추가해서 예외처리 구문을 작성해야 한다.
			 * */
			while((s = in.readLine()) != null) {
				System.out.println(s);
			}
			
		/* FileNotFoundException과 EOFException을 동시에 처리할 수 있다.
		 * 같은 레벨의 자손을 한 번에 처리할 수 있다.
		 * */	
		} catch (FileNotFoundException | EOFException e) {
			e.printStackTrace();
		} catch (IOException e) {
			
			/* 입출력에 관한 추상화 된 예외 클래스이다.
			 * FileNotFoundException, EOFException은 IOException의 후손이다.
			 * 
			 * catch블럭은 여러 개를 작성할 시 상위 타입이 하단에 오고 후손 타입이 먼저 작성 되어야 한다.
			 * */
			e.printStackTrace();
		} finally {
			/* 예외 처리 구문과 상관 없이 반드시 수행해야 하는 경우 작성하며
			 * 보통 사용한 자원을 반납할 목적으로 사용하게 된다.
			 * */
			
			try {
				/* 입출력에 사용한 스트림을 닫아주는 메소드이다.
				 * IOException을 위임한 메소드이기 때문에 finally 블럭 안이더라도
				 * 예외 처리를 중첩으로 해주어야 한다.
				 * */
				
				/* 그냥 실행하면 NullPointerException이 발생한다.
				 * 파일을 찾지 못해 객체를 생성하지 못하고 레퍼런스 변수는 null 값을 가지고 있는데
				 * null을 참조하는 상태에서 참조연산자 사용 시 발생하는 예외이다.
				 * NullPointerException은 unchecked Exception 이므로 try-catch로 처리하기 보다는
				 * 보통은 if - else 구문으로 해결 가능하다.
				 * */
				//in.close();
				
				if(in != null)
					in.close();
				
			} catch (IOException e) {
				e.printStackTrace();
			}

		}
		
	}

try-with-resource (상단의 코드를 좀 더 간편하게 작성하는 법)

- JDK 1.7에서 추가된 문법

-close 해야 하는 인스턴스의 경우 try 옆에 괄호 안에서 생성하면 해당 try-catch 블럭이 완료될 때 자동으로 close 처리해줌

public class Application2 {

	public static void main(String[] args) {
		
		try (BufferedReader in = new BufferedReader(new FileReader("test.dat"))) {
			
			String s;
			
			while((s = in.readLine()) != null) {
				System.out.println(s);
			}
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

}

Exception 오버라이딩

- 오버라이딩 시 throws하는 Exception의 개수와 상관없이 같거나 후손범위여야 한다.

public class SuperClass {
	
	/* 예외를 던지는 메소드 하나 작성한다. */
	public void method() throws IOException {}
}
public class SubClass extends SuperClass {
	
	/* 예외 없이 오버라이딩 할 수 있다. */
//	@Override
//	public void method() {}
	
	/* 같은 예외를 던져주는 구문으로 오버라이딩 할 수 있다. */
//	@Override
//	public void method() throws IOException {}
	
	/* 부모의 예외처리 클래스보다 상위의 예외로는 후손클래스에서 오버라이딩 할 수 없다. */
//	@Override
//	public void method() throws Exception {}
	
	/* 하지만 부모의 예외처리 클래스보다 더 하위에 있는 예외(즉, 더 구체적인 예외 상황)인 경우에는
	 * 오버라이딩 할 수 있다. */
	@Override
	public void method() throws FileNotFoundException {}
	
}