본문 바로가기
Back-end

예외처리(5)

by 신재권 2021. 6. 23.

사용자 정의 예외 만들기

기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception클래스 또는 RuntimeException 클래스로부터 상속받아아 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있따.

  • 가능하면 새로운 예외클래스를 만들기보다 기존의 예외클래스를 활용하자
  • class MyException extends Exception{
    	MyException(String msg) { //문자열을 매개변수로 받는 생성자
    			super(msg);//조상인 Exception클래스의 생성자를 호출한다.
    	}
    }

Exception 클래스로부터 상속받아서 MyException 클래스를 만들었다. 필요하다면, 멤버변수나 메서드를 추가할 수 있다. Exception클래스는 생성 시에 String값을 받아서 메시지로 저장할 수 있다. 우리가 만든 사용자 정의 예외 클래스도 메시지를 저장할 수 있으려면, 위에서 보는것과 같이 String을 매개변수로 받는 생성자를 추가해주어야 한다.

class MyException extends Exception{
	//에러 코드 값을 저장하기 위한 필드를 추가 했다.
	private final int ERR_CODE;  //생성자를 통해 초기화 한다.

	MyException(String msg, int errCode){//생성자
		super(msg);
		ERR_CODE = errcode;
	}
	
	MyException(String msg){//생성자
		this(msg, 100);  //ERR_CODE를 100(기본값)으로 초기화한다.
	}
	
	public int getErrCode() { //에러 코드를 얻을 수 잇는 메서드도 추가했다.
		return ERR_CODE; //이 메서드는 주로 getMessage()와 함께 사용될 것이다.
	}
}

이전의 코드를 좀더 개선하여 메시지뿐만 아니라 에러코드 값도 저장할 수 있또록 ERR_CODE와 getErrCode()를 MyException클래스의 멤버로 추가했다.

이렇게 함으로써 MyException이 발생했을 때, catch블럭에서 getMessage()와 getErrCode()를 사용해서 에러코드와 메시지를 모두 얻을 수 있을 것이다.

기존의 예외 클래스는 주로 Exception을 상속받아서 'checked예외'로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어 가고 있다. checked예외는 반드시 예외처리를 해주어야하기 때문에 예외처리가 불필요한 경우에도 try-catch문을 넣어서 코드가 복잡해지기 때문이다.

예외처리를 강제로하도록 한 이유는 프로그래밍경험이 적은 사람들도 보다 견고한 프로그램을 작성할 수 있게 유도하기 위한 것이었는데, 요즘은 자바가 탄생하던 20년 전과 달리 프로그래밍 환경이 많이 달라졌다. 그 때 자바를 설계하던 사람들은 자바가 주로 소형 가전기기나, 데스크탑에서 실행될 것이라고 생각했지만 현재 자바는 모바일이나 웹 프로그래밍에서 주로 쓰이고 있다. 이처럼 프로그래밍 환경이 달라진 만큼 필수적으로 처리해야만 할 것 같았던 예외들이 선택적으로 처리해도 되는 상황으로 바뀌는 경우가 종종 발생하고 있다. 그래서 필요에 따라 예외처리의 여부를 선택할 수 있는 unchecked 예외가 강제저직인 checked예외 보다 더 환영받고 있다.

public class NewExceptionTest {

	public static void main(String[] args) {
		try{
			startInstall(); //프로그램에 설치에 필요한 준비를 한다.
			copyFiles();
		}catch(SpaceException e){
			System.out.println("에러 메시지 : "+e.getMessage());
			e.printStackTrace();
			System.out.println("공간을 확보한 후에 다시 설치하기 바랍니다.");
		}catch (MemoryException me) {
			System.out.println("에러 메시지 : "+me.getMessage());
			me.printStackTrace();
			System.gc();  //Garbage Collection을 수행하여 메모리를 늘려준다.
			System.out.println("다시 설치를 시도하세요.");
		}finally{
			deleteTempFiles();  //프로그램 설치에 사용된 임시파일들을 삭제한다.
		}

	}
	
	static void startInstall() throws SpaceException, MemoryException{
		if(!enoughSpace())	//충분한 설치 공간이 없으면 
			throw new SpaceException("설치할 공간이 부족합니다.");
		if(!enoughMemory()) //충분한 메모리가 없으면
			throw new MemoryException("메모리가 부족합니다.");
	}
	
	static void copyFiles(){
		//파일을 복사하는 코드 
		} 
	
	static void deleteTempFiles() {
		//임시 파일들을 삭제하는 코드
	}
	
	static boolean enoughSpace(){
		//설치하는데 필요한 공간이 있는지 확인하는 코드
		return false;
	}
	static boolean enoughMemory(){
		//설치하는데 필요한 메모리 공간이 있는지 확인하는 코드
		return true;
	}
}

class SpaceException extends Exception{
	public SpaceException(String msg) {
		super(msg);
	}
}


class MemoryException extends Exception{
	public MemoryException(String msg) {
		super(msg);
	}
}
=====================================
에러 메시지 : 설치할 공간이 부족합니다.
SpaceException: 설치할 공간이 부족합니다.
	at NewExceptionTest.startInstall(NewExceptionTest.java:25)
	at NewExceptionTest.main(NewExceptionTest.java:6)
공간을 확보한 후에 다시 설치하기 바랍니다.

MemoryExeption과 SpaceException, 이 두 개의 사용자정의 예외 클래스를 새로 만들어서 사용했다. Space Exception은 프로그램을 설치하려는 곳에 충분한 공간이 없을 경우에 발생하도록 했으며, MemotyException은 설치작업을 수행하는데 메모리가 충분히 확보되지 않았을 경우에 발생하도록 하였다.

이 두 예외는 startInstall()을 수행하는 동안에 발생할 수 있으며, enoughSpace()와 enoughMemory()의 실행결과에 따라서 발생하는 예외의 종류가 달라지도록 했다.

이번 예제에서는 enoughSpace()와 enoughMemory()는 단순히 false와 true를 각각 반환하도록 되어 있지만, 설치공간과 사용 가능한 메모리를 확인하는 기능을 한다고 가정하였다.

예외 되던지기(exception re-throwing)

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다.

그리고 심지어는 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드, 양쪽에서 처리하도록 할 수 있다.

이것은 예외를 처리한 후 에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 예외 되던지기(exception re-throwing)라고 한다.

먼저 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외를 처리해주고 catch문에서 필요한 작업을 행한 후에 throw문을 사용해서 예외를 다시 발생시킨다. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리한다.

이 방법은 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용된다. 이 떄 주의할 점은 예외가 발생할 메서드에서는 try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드의 선언부에 발생할 예외를 throws에 지정해줘야 한다는 것이다.

public class ExceptionEx17 {

	public static void main(String[] args) {
		try{
			method1();
		} catch(Exception e){
			System.out.println("main메서드에서 예외가 처리되었습니다.");
		}
	}
	static void method1() throws Exception{
		try{
			throw new Exception();
		}catch (Exception e){
			System.out.println("method1 메서드에서 예외가 처리되었습니다.");
			throw e;  //다시 예외를 발생시킨다.
		}
		
	}

}
============================
method1 메서드에서 예외가 처리되었습니다.
main메서드에서 예외가 처리되었습니다.

결과에서 알 수 있듯이 method1()과 main메서드 양쪽의 catch블럭이 모두 수행되었음을 알 수 있다. method19)의 catch블럭에서 예외를 처리하고도 throw문을 통해 다시 예외를 발생 시켯다. 그리고 이 예외를 main메서드에서 한 번 더 처리하였다.

반환값이 있는 return문의 경우, catch블럭에도 return문이 있어야 한다. 예외가 발생했을 경우에도 값을 반환해야 하기 때문이다.

static int method1(){
	try{
		System.out.println("method1()이 호출되었습니다.");
		return 0;
	}catch(Exception e){
		e.printStackTrace();
		return 1; //catch블럭 내에도 return문이 필요하다.
	}finally{
		System.out.println("method1()의 finally블럭이 실행되었습니다.");
	}
}

또는 catch블럭에서 예외 되던지기를 해서 호출한 메서드로 예외를 전달하면, return 문이 없어도 된다. 그래서 검증에서도 assert문 대신 AssertError를 생성해서 던진다.

  • assert문은 검증(assertion)을 수행하기 위한 문장이다.
static int method1() throws Exception{ //예외를 선언해야 함
	try{
		System.out.println("method1()이 호출되었습니다.");
		return 0;  //현재 실행중인 메서드를 종료한다.
	}catch(Exception e){
		e.printStackTrace();
	//return 1;  //catch블럭 내에도 return문이 필요하다
		throw new Exception();  //return문 대신 예외를 호출한 메서드로 전달
	}finally{
		System.out.println("method1()의 finally블럭이 실행되었습니다.");
	}
}
  • finally블럭 내에도 return문을 사용할 수 있으며, try블럭이나 catch블럭의 return문 다음에 수행한다. 최종적으로 finally블럭 내에 return문의 값이 반환된다.

연결된 예외(chained exception)

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)라고 한다. 아래의 코드는 예제 21의 일부를 변경한 것으로, SpaceException을 원인 예외로 하는 InstallException을 발생시키는 방법을 보여준다.

try{
	statrInstall(); //SpaceException 발생
	coptyFiles();
}catch (SpaceException e){
	InstallException ie = new InstallException("설치중 예외 발생"); //예외 생성
	ie.initCause(e);  //InstacllException의 원인 예외를 SpaceException으로 지정
	throw ie;     //InstacllException을 발생시키낟.
}catch(MemoryException me){
	...
}

먼저 InstacllException을 생성한 후에, initCause()로 SpaceException을 InstallException의 원인 예외로 등록한다. 그리고throw로 이 예외를 던진다

initCause()는 Exception클래스의 조상인 Throwable클래스에 정의되어 있기 때문에 모든 예외에서 사용이 가능하다.

Throwable initCause(Throwable cause) // 지정한 예외를 원인 예외로 등록
Throwable getCause()  //원인 예외를 반환

발생한 예외를 그냥 처리하면 될텐데, 원인 예외로 등록해서 다시 예외를 발생시키는지 궁금할 것이다. 그 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

그렇다고 아래와 같이 InstacllException을 SpaceException과 MemoryException의 조상으로 해서 catch블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문자게 생긴다. 그리고 SpaceException과 MemoryException의 상속관계를 변경해야 한다는 것도 부담이다.

try{
	startInstall();  //SpaceException발생
	copyFiles();
}catch(InstacllException e) { //InstallException은
		e.printStackTrace(); //SpaceException과 MemoryExcetion의 조상
}

그래서 생각한것이 예외가 원인 예외를 포함할 수 있게 한것이다. 이렇게 하면 두 예외는 상속관계까 아니어도 상관없다.

public class Throwable implements Serializable{
	...
	private Throwable cause = this;  //객체 자신(this)을 원인 예외로 등록
	...
}

또 다른 이유는 checked예외를 unchecked 예외로 바꿀 수 있도록 하기 위해서이다.

checked예외로 예외처리를 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것이였다. 지금은 자바가 처음 개발되던 시절과 컴퓨터 환경이 많이 달라졌다.

그래서 checked 예외가 발생해도 예외를 처리할 수 없는 상황이 하나둘 발생하기 시작했다. 이럴 때 할 수 있는 일이라곤 그저 의미없는 try-catch문을 추가하는 것 뿐인데 , checked 예외를 unchecked예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.

static void startInstall() throws SpaceException, MemoryException{
	if(!enoughSpace())  //충분히 설치할 공간이 없으면...
		throw new SpaceException("설치할 공간이 부족합니다.");
	if(!enoughMemory()) //충분한 메모리가 없으면...
		throw new MemoryException("메모리가 부족합니다.");
}
------------------  ==>>>       -------------
static void startInstall() throws SpaceException{
	if(!enoughSpace())  //충분히 설치할 공간이 없으면...
		throw new SpaceException("설치할 공간이 부족합니다.");
	if(!enoughMemory()) //충분한 메모리가 없으면...
		throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}

MemoryException은 Exception의 자손이므로 반드시 예외를 처리해야 하는데, 이 예외를 RuntimeException으로 감싸버렸기 때문에 unchecked예외가 되었다. 그래서 더 이상 startInstall()의 선언부에 MemoryException을 선언하지 않아도 된다. 참고로 위의 코드에서는 initCause()대신 RuntimeException의 생성자를 사용했다.

RuntimeException(Throwable cause)  //원인 등록을 생성하는 생성자
public class ChainedExceptionEx {

	public static void main(String[] args) {
		try{
			install();
		}catch(InstallException e){
			e.printStackTrace();
		}catch(Exception e){
			e.printStackTrace();
		}

	}
	
	static void install() throws InstallException{
		try{
			startInstall(); //프로그램 설치에 필요한 준비를 한다.
			copyFiles();  //파일들을 복사한다.
		}catch(SpaceException se){
			InstallException ie = new InstallException("설치 중 예외 발생");
			ie.initCause(se);
			throw ie;
		}catch(MemoryException me){
			InstallException ie = new InstallException("설치 중 예외 발생");
			ie.initCause(me);
			throw ie;
		}finally{
			deleteTempFiles();//프로그램 설치에 사용된 임시파일들을 삭제한다.
		}
	}

	static void startInstall() throws SpaceException, MemoryException{
		if(!enoughSpace()){	//충분한 설치공간이 없으면
			throw new SpaceException("설치할 공간이 부족합니다.");
		}
		if(!enoughMemory()){ //충분한 메모리가 없으면
			throw new MemoryException("메모리가 부족합니다.");
//			throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
			
		}
	}
	
	
	static void copyFiles(){
		//파일을 복사하는 코드
	}
	static void deleteTempFiles(){
		//임시 파일들을 삭제하는 코드
	}
	static boolean enoughSpace(){
		//설치하는 필요한 공간이 있는지 확인하는 코드를 적는다.
		return false;
	}
	static boolean enoughMemory(){
		//설치하는데 필요한 메모리공간이 있는지 확인하는 코드를 적는다.
		return false;
	}
}

class InstallException extends Exception{
	InstallException(String msg){
		super(msg);
	}
}
class SpaceException extends Exception{
	SpaceException(String msg){
		super(msg);
	}
}
class MemoryException extends Exception{
	MemoryException(String msg){
		super(msg);
	}
}
================================
InstallException: 설치 중 예외 발생
	at ChainedExceptionEx.install(ChainedExceptionEx.java:20)
	at ChainedExceptionEx.main(ChainedExceptionEx.java:6)
Caused by: SpaceException: 설치할 공간이 부족합니다.
	at ChainedExceptionEx.startInstall(ChainedExceptionEx.java:34)
	at ChainedExceptionEx.install(ChainedExceptionEx.java:17)
	... 1 more

'Back-end' 카테고리의 다른 글

java.lang패키지와 유용한 클래스(2)  (0) 2021.06.24
java.lang패키지와 유용한 클래스(1)  (0) 2021.06.23
예외처리(4)  (0) 2021.06.22
예외처리(3)  (0) 2021.06.21
예외처리(2)  (0) 2021.06.21