예외 처리란?
프로그램 실행 시 발생할 수 있는 예외에 대비하는 것으로 프로그램 비정상 종료를 막고 실행 상태를 유지하는 것입니다.
예를 들어
학생이 버스를 타고 커피를 산 다음에 학교에 걸어가는 로직이 있습니다.
그래서 학생이 버스를 타고 커피를 사러 왔는데 돈이 없는 경우, 줄이 너무 길어서 시간이 부족한 경우, 원하는 메뉴가 매진되었을 경우 등 이러한 이유로 커피를 못 사게 되면 원하는 로직으로 진행이 되지 않습니다.
그래서 사람인 저희가 무사히 학교에 걸어갈 수 있게 해줘야 하는데요!
그렇다면 프로그래밍에서 오류는 뭐고, 또 예외는 뭘까요.
에러 즉 오류는 외부 요인에 의해서 발생하는 것으로 좀 심각한 오류입니다.
그래서 한번 발생하면 이를 되돌릴 수 없고 이걸 개발자가 예측하기가 되기 힘든 요소입니다.
예를 들면 아까 프로그램에서 컴퓨터 메모리가 부족하여 학생이 프로그램 자체를 못 돌리게 되는 경우가 속할 수 있고 예외는 아까 상점이 문을 닫은 예외처럼 개발자의 로직에 의해서 발생한 예외인데요 아까 저 에러보다는 덜 심각한 상황이고 우리가 대비해 줘야 되는 예외라는 것이 여기에 속하는 것입니다.
그러면 예외 처리하는 법에 대하여 알아볼게요.
public void buyCoffee(Store store) throws ClosedStoredException, SoldOutCoffeeException, CrowedstoreException{
if(store.closed()){
throw new ClosedStoredException("상점이 문을 닫았습니다.");
}
if(store.CoffeeSoldOut()){
throw new SoldOutCoffeeException("커피가 다 팔렸습니다.");
}
if(store.crowed()){
throw new CrowedstoreException("줄이 너무 깁니다.");
}
}
여기에는 buyCoffee라는 함수가 있고 throw라는 명령어는 예외를 강제적으로 던져 준다는 것인데요.
개발자가 임의로 예외를 발생시킬 때 사용하는 명령어입니다. 그리고 이쪽 예외 처리에서 생성자 파라미터 안에 메시지는 예외 메시지, 제가 넣어 준 것이고요. 그리고 throws라는 명령어는 지금 선언된 이 예외를 여기서 처리하지 않고 이 함수를 호출하는 곳에서 처리해 주겠다는 뜻인데요.
이 함수를 호출하는 곳에 가보면
Student student = new Student();
try{ //예외가 발생할 수 있는 문장
student.rideBus();
student.buyCoffee(store);
student.happy();
} catch (ClosedStoreException e){ //잡아 줄 예외 클래스
student.passCoffeeStore();//예외 발생시 실행시킬 문장
} finally{//예외 발생과 상관없이 무조건 실행시킬 문장
student.arriveCampus();
}
try-catch를 통해서 이 작동을 실행하는데요.
예시를 통해서 먼저 설명드릴게요.
학생이라는 객체를 만들고 버스를 타고 커피를 샀어요.
그러면 학생은 행복합니다. 그리고 학교에 옵니다.
그런데 버스를 타고 왔더니 카페가 문을 닫은 거예요.
그러면 happy 또한 하지 않겠죠.
그리고 이제 catch문안에 있는 카페를 지나친다는 메서드를 수행하고 마지막으로 캠퍼스에 도착한다는 이 메소드를 실행합니다.
즉, try 블럭 안에서는 예외가 발생할 가능성이 있는 문장이 들어오게 되고요 이 catch 소괄호 안에는 잡아 줄 예외 클래스 그 안에는 예외 발생 시 실행시킬 문장이 들어오고 마지막으로 finally 안에는 예외 발생과 상관없이 무조건 실행시킬 문장이 들어오게 되는 것입니다.
예외가 발생했을 때 예외 로그가 남겨져 있다면 예외에 대한 정보를 저희가 알 수 있는데요.
크게 2가지만 알아보겠습니다.
여기 참조변수 e를 통해서 발생한 이 예외에 대해 저희가 접근할 수 있는데요 이 인스턴스 안에는 발생한 예외에 대한 정보들이 있어요. 이 printStackTrace를 사용하면 발생한 예외의 메소드가 어디서 발생했는지와 같은 이런 정보들과 호출 스택에 저장돼 있던 이런 예외 정보들을 꺼내와서 보여주고 예외 메시지도 보여줍니다.
getMessage는 아까 제가 넣어줬던 예외 메시지를 인스턴스에서 가져와서 보여주는 겁니다.
지금까지 계속 커스텀 예외를 사용해 왔는데요.
커스텀 예외를 만드는 방법에 대해서 또 알아보겠습니다.
public class ClosedStoreException extends Exception{
public ClosedStoreException(){
super();
}
public ClosedStoreException(String errorMsg){
super(System.lineSeparator() + "[예외 메세지] "+ errorMsg);
}
}
커스텀 예외 클래스는 이름의 마지막을 Exception으로 작성하는 것이 컨벤션이고 Exception이나 RuntimeExceptiom을 기본적으로 extends(상속) 받는데요.
그럼 어떨 때는 Exception을 받고 어떨 때는 Runtime을 받는지 한번 알아보도록 할게요.
Exception을 상속받는 예외 클래스가 Checked Exception인데요.
이 예외 클래스는 컴파일 단계에서 예외를 확인하기 때문에 예외 처리를 강제적으로 하게 되어있어요. 때문에 개발자의 실수가 줄어들 수 있다는 장점이 있지만 모든 예외를 핸들링 해줘야 하기 때문에 번거롭고요. 예외를 계속해서 throw 해줄 수 있잖아요? 그러면 이게 계속 클래스를 throw throw 하니까 레이어 간의 의존성이 높아질 수 있다는 단점이 있고 또 Stream 내에서 사용이 불가능합니다.
반대로 이제 RutimeExceptio을 상속하는 Unchecked Exception은 실행단계에서 예외를 확인하고요. 예외 처리를 강제적으로 명시하고 있지 않아요. 그래서 이제 throw를 해줄 일이 없기 때문에 레이어 간의 의존성이 줄어들고 반대로 개발자의 실수가 늘어날 수 있다는 단점이 있습니다.
계속해서 얘기해 보자면 기본 생성자를 하나 만들고 예외 메시지를 아까 제가 써줬던 것처럼 받아서 오버 로딩 해주는 것이 기본 원칙입니다. 제가 계속해서 커스텀 예외 클래스를 사용해서 마음이 불편하실 수 있을 건데 커스텀 예외 클래스도 이제 장단점이 있기 때문에 알고 사용하는 게 좋겠죠.
장점으로는 클래스 이름만으로도 정보 전달이 가능하잖아요.
그래서 지금까지 계속해서 커스텀 예외 클래스를 사용해왔는데요.
만약에 이런 커스텀 예외 클래스가 너무 많고, 만약 개발자가 예외 클래스 이름을 잘못 지을 수도 있습니다.
그런 상황에서는 저희가 이미 정리되어 있는 표준 예외를 쓰는 것이 오히려 정보를 한 번에 볼 수가 더 좋을 수 있습니다.
커스텀 예외 클래스를 사용해서 해당 클래스 안에서 예외 메시지나 예외 정보, 정보에 대한 처리 그리고 아니면 예외 후처리 같은 것을 한 클래스 안에서 해 줌으로써 해당 예외에 대한 응집도가 향상될 수 있다는 장점이 있습니다.
대신에 커스텀 예외 클래스도 클래스이기 때문에 많아지면 복잡하고 관리해야 될 클래스가 많아진다는 단점이 있습니다.
그래서 저라면 이미 있는 표준 예외라면 표준 예외를 사용하되 표준 예외가 좀 부족하다 싶을 때 커스텀 예외를 만들어 사용할 것 같은데요. 그러면 표준 예외가 어떤 게 있는지 알아야겠죠.
자주 쓰는 몇가지만 설명해 드릴게요.
먼저 Exception을 상속하는 예외 들은 기본적으로 외부 요인에 의해 발생하는 예외들이 많고요.
IOException은 이제 자바의 모든 입출력에 대한 예외 발생 시 사용하고 FileNotFoundException은 디스크에 없는 파일에 대한 엑세스 시도가 실패한 경우 사용합니다.
RuntimeException을 상속하는 예외 클래스들은 이제 개발자의 실수에 의한 그런 예외들이 많은데요.
IllegalArgument Exception은 허용하지 않는 값이 인수로 건네줬을 때 발생할 예외에 사용하고요.
IlligalState Exception은 객체가 메서드를 수행하기에 적절치 않은 상태일 경우,
NullPointerException은 null을 허용하지 않는 메서드에 null 값을 건넸을 경우,
IndexOutOfBoundsException은 인덱스가 허용 범위를 넘겼을 경우에 사용합니다.
예외 처리를 잘 해준다면 프로그램의 안정성이 향상되고, 예외 발생 시 시스템의 중단을 방지하고 정상적인 실행을 유지할 수 있습니다.
그리고 유지 보수와 디버깅이 쉬워진다는 장점들이 있는데 단점들에는 예외 처리를 남발하면은 코드의 가독성을 떨어뜨릴 수 고 로직에 많이 들어가면 성능상 많이 느려진다는 단점들이 있습니다.
그러면 예외를 진짜 예외 상황에만 써주는 게 좋겠죠.
그러면 진짜 예외 상황이란 무엇일까요.
그렇다면 아까 student 코드를 아래와 같이 바꾸면 어떨까요.
Student student = new Student();
student.rideBus();
if(!store.closed()){
student.buyCoffee(store);
student.happy();
} else {
student.arriveCampus();
}
student.arriveCampus();
이렇게 바꾸면 좀 더 보기 좋고 편하지 않나요?
저희가 TDD를 하면서 계속해서 예외를 throw 해주고 그리고 try-catch를 사용해서 그것을 재귀 함수처럼 쓰시는 분들이 많은데 그런 사람들에게 묻고 싶습니다 정말 예외 상황에 썼다고 생각하시나요?
플레이어의 이름이 5글자 이상이면 정말 예외인가요?
한번 질문해 본 적이 없다면 스스로 질문해 보시면 좋겠습니다.
참고자료 및 출처
https://www.youtube.com/watch?v=mrrEPbGF6hQ - [10분 테코톡] 케로의 예외처리