예외 처리

반응형
반응형

자바가 제공하는 예외 계층 구조

비유
우리가 병이 생긴다면, 병원으로 간다.
그 병이 치명적일 수도 있고, 치명적이지 않을 수 있다.
만약, 그 병이 치명적이라고 가정한다면, 제대로 치료를 받지 않으면 죽게 될 수도 있다.
만약, 그 병이 치명적이지 않는다고 한다면, 병원에 가지 않고 자가 치료가 가능할지도 모른다.

실제
프로그램도 마찬가지다.
에러가 치명적이라면, 그 예외를 해결을 해야된다.
그래야 프로그램을 계속 유지할 수 있기 때문이다.

컴파일 에러

가벼운 감기 처럼(아닌 분들도 계시지만 ㅜㅜ),우리가 코드상에서 스스로 해결할 수 있는 에러도 존재한다.
이것을 컴파일 에러라고 한다.
하지만 이것도 치료하지 않는다면, 죽을 수도 있다.

자료형이 없다는 것을 단번에 알 수 있다.

이것을 치료조차 하지 않는다면?
여기서 알 수 있는 건 아무리 가벼운 병이라도 헛으로 보면 안된다는 점을 배울 수 있다.

런 타임 에러

이것을 런 타임 에러라고 부른다.
즉, 프로그램이 구동이 될때 발생된 에러라고 할 수 있다.

public class exeptions {
    public static void main(String[] args) {
        test();
    }
    static public void test() {
        test();
    }
}

간단하게 재귀로 작성하였다.
코드상에서는 아무런 문제가 보이지 않는다. 하지만...

스택 오버플로우가 발생하였다 ㅜㅜ

런타임 에러에는 2가지 종류가 존재하는데,
첫 번째는 에러이고, 두 번째는 예외다.

이 둘의 차이점은 수습이 가능하냐, 가능하지 않는다의 차이로 말할 수 있을 것같다.
즉, 현실로 따진다면 치료가 가능하냐, 치료가 완전히 불가능하냐의 차이라 생각한다,

에러 (Error)

프로그램상에서 수습될 수 없는 심각한 오류
대표적으로 스택 오버플로우랑, 메모리 부족을 들 수 있다.
위 같은 경우는 런 타임 에러 -> 에러라고 말 할 수 있다.

예외(Exception)

프로그램상에서 수습 될수 있는 오류
냅두면 사망각이지만, 다행히 치료가 가능한 오류라 할 수 있으며,
우리가 학습할 부분도 여기에 해당된다.

모든 에러와 예외는 Throwable라는 같은 조상을 가진다는 것을 알 수 있다.

출처 : 자바의 정석
Exception
Error

Runtime Exception과 RuntimeException이 아닌것들의 차이는?

Runtime Exception 프로그래머의 실수
Runtime Exception를 제외한 다른 Exception 외부의 영향 ( 사용자의 실수)

이렇게 요약할 수 있다.
RuntimeException 에는 ClassCastException라는 예외가 존재한다. 
아직, 어떻게 동작하는지 잘 모르지만,
아마 클래스의 캐스팅이 잘못되어서 발생한 "예외"가 아닌가 라는 추측을 해본다.

Object x = 0;
System.out.println((String)x);

 코드상으로 봤을 때는 틀린것이 없어보인다.
왜냐하면, Integer타입이 Object타입이 맞고,
String도 Object타입이 맞기 때문이다.

Integer => Object
String => Object

 하지만 결과는...

이 사진들로 미뤄봤을때 ClassCastException이 발생했다는 것을 알 수 있다.
다행스럽게도 우리는 이 "예외"를 해결하는 방법을 알고 있다.

1. Object 와 x의 값을 String으로 변경한다.

String x = "0";
System.out.println((String)x);

2. (String)으로 형변환이 아닌, String.valueOf()를 사용해서 값자체를 String으로 변경시킨다.

Object x = 0;
System.out.println(String.valueOf(x));

3. (String)을 지운다.

Object x = 0;
System.out.println(x);

이 처럼 RuntimeException은 프로그래머의 "실수"에 발생된 예외다.
즉, 우리는 해결방법을 이미 알 고 있다고 생각한다.
그렇다면, 이 예외가 발생했다는 이야기는 어떤 의미일까?
프로그래머가 이 실수를 인지한다면 언제든지 수정할 수 있다는 뜻이 된다.
하지만 실수를 인지하지 못한다면, 컴파일시에는 아무 문제가 존재하지 않는 코드일뿐이다.
그렇다는 이야기는 RuntimeException은 "런타임"시 결정된다고 이야기 해도 무방할 것 같다.

RuntimeException이 아닌 Exception들

fileNotFoundException를 예시로 들어보자.
파일을 찾지 못해서 발생한 에러로,
IOException에 속한다고 한다. 

파일을 찾지 못했다는 건 파일이 경로에 없다는 뜻인데,
프로그래머가 일일이 경로를 지정해줄 수는 없다.
10개의 파일의 경로는 추가가 가능할지 몰라도
수백만개의 파일들을 어떻게 추가할것이며, 그 파일들을 어떻게 찾을 수 있을까?

사용자가 파일명을 잘못입력해서
파일을 찾을 수 없다고 한다면, 어떤 조치가 취해져야 할까?

그럴때 필요한 솔루션이 있어야 하지 않을까?

프로그래머는 사용자가 이러한 실수에 미연에 방지하고자, 
FileNotFoundException이라는 "예외"를 사용할 수 있다.
결국, 실수에 미연에 방지하기위해 코드가 추가되야 된다는 뜻이된다.
이 예외를 발생하면 안되기 때문에,  "컴파일"시 결정된다고 해도 무방할 것 같다.

이제 지금까지 학습한 내용을 토대로 다시 표를 그려보면

  Runtime Exception Runtime Exception를 제외한 다른 Exception
발생 원인 프로그래머의 실수 외부의 영향 ( 사용자의 실수)
확인 시점 런타임시 결정 컴파일 시 결정

RuntimeException은 UnChecked Exception이라고 하고,
그 나머지를 Checked Exception이라고 부른다고 한다.

 

Java 예외(Exception) 처리에 대한 작은 생각

일상생활에서도 기본적인 것은 고민하지 않고 습관처럼 사용하는 경우가 있다. 초급 개발자인 나에게 ‘예외(Exception)’이 바로 그런 것이었다. 처음 JAVA수업 때 강사님께 "왜 로직을 try문으로

www.nextree.co.kr

만약, 에러를 해결하지 못한다면 어떻게 될까?
콘솔창에 어떤 에러가 발생했는지 알려준다.
이것이 가능한 이유는 "UncaughtExceptionHandler"라는 녀석 때문이라고 한다.

 

Thread.UncaughtExceptionHandler (Java Platform SE 7 )

Interface for handlers invoked when a Thread abruptly terminates due to an uncaught exception. When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using Thread.get

docs.oracle.com

스레드에서 동작하는 것 같은데... 잘 모르겠다.

자바에서 예외 처리 방법(try, catch, throw, throws, finally)

우리가 예외를 처리할 수 있는 것은 런 타임시 발생되는 에러이며, 그 중에서 오류라고 불릴 수 있는 것들을 
예외 처리가 가능하다.

자바에서는 다양한 방법으로 예외를 처리하는 방법을 제공하고 있다.

try - catch

가장 간단하게 사용 될 수 있는 형태이며,
아래 처럼 작성된다.

try {
    예외 처리할 구문.        
  } catch (Exception e) {
    예외 처리     
}


만약, 코드를 실행하였는데, 에러가 발생했다고 생각해보자.
(스택 오버플로우, 메모리 부족 제외)

이러한 코드를 작성했다고 가정하자.

int  a = 0/0;

 컴파일 관점에서 보면 어차피 두 개다 숫자이므로 아무 이상 없이 코딩 할 수 있다.
하지만 이것을 실행하면,

0으로 나눌 수 없다고 한다.
하지만 나는 이것을 무시하고 싶다.
그럴때 try - catch 구문을 사용하면 된다.

try {
   int a = 0/0;
   System.out.println(a);
  } catch (Exception e) {
 }

 이렇게 작성하면 언제 그랬냐는 듯이 실행 결과는 

아무것도 나오지 않는다. 즉, int a = 0/0; 이 구문을 해결하였다.

try 에는 오류가 발생 할것같은 구문을 작성하고,
catch에는 해결 방안을 작성하면 된다. 지금은 Exception만 작성하였지만, 추후에 Exception의 종류에 대해 설명할 예정이다.

하지만 이 방법은 굉장히 위험하다.
왜냐하면, 어떤 에러가 발생하였는지 전혀 모르기 때문이다.
이때 크게 2가지 방법으로 어떤 에러가 동작하였는지 알 수 있다.

2가지 방법모두 어떤 에러가 동작했는지 알 수 있다는 점에서 비슷하다고 생각할 수 있다.
하지만 이것을 메소드에서 실행한다면 얘기가 달라진다.
(리턴값이 void라면 상관없지만, 리턴값이 객체라면..얘기가 달라지지.)

   Exception 인스턴스 변수로 확인하는 방법

      - e.printStackTrace(), 인스턴스 변수로등등 다양한 방법으로 확인 할 수 있다.

 public Integer test(int num) {
  try {
      return 0/num;

      } catch (Exception e) {
         e.printStackTrace();
      }
   return null;
}

   강제로 에러를 발생시키는 방법 (throw)

      - throw new RuntimeException() 계열
         (다른 Exception도 가능하지만, 사용시 강제적으로 예외처리를 해줘야한다.)

public Integer test(int num) {
  try {
     return 0/num;
  } catch (Exception e) {
    throw new RuntimeException();
  }
}

이 둘의 가장 큰 차이점은 return의 유무인데,
밑에 return을 추가함으로써 쓸데없는 값이 입력 되어진다.

따라서 Exception 인스턴스를 이용하는 것 보다,
throw를 사용하는 것이 더 좋다고 생각한다.

multi - Exception

 exception을 여러개 하는 방법으로
반드시 catch를 한 번만 할 이유는 없다.

try {
    구문..
  } catch (RuntimeException e1) {
     에러 처리
  } catch (IOException e2) {
     에러 처리
  } 
  ...
}

이들은 같은 지역으로 동작하기 때문에 변수값이 같아서는 안 된다.
또한 서로 같은 계층에 속하면 안된다.
즉, RuntimeException과 Exception은 사용이 불가능하다는 이야기다.
이렇게 catch문을 계속 증가시키면,
코드가 더러워질 가능성이 높아진다.
다행히 JDK1.7부터 catch문을 하나로 합치는 방법을 제공하고 있다.

try {

} catch (RuntimeException | IOException e) {

} 

finally

try-catch이에외도
finally이라는 것도 존재하는데,
반드시 try는 catch와 함께 사용해야되는 것은 아니다.
finally는 try-catch둘다 사용하고 사용해도 상관없지만,
try만 사용해도 사용이 가능하다. 

try {

} catch (Exception e) {

}  finally {

}

또는

try {

} finally {

}

이런식으로 작성되며,
finally는 try 또는 try-catch를 실행한뒤, 무조건 실행해야 되는 코드를 작성하기 위함이다.

그럼 finally는 어디에서 사용이 되어질까?

주로 자원을 반납할때 사용된다고 한다.
이 부분은 추후에 학습할 커스텀한 예외 만드는 방법을 학습한 뒤, 
예제를 살펴보도록 하자.

즉, finally는 에러가 발생이 되면 무조건 실행이 되는 코드라고 생각하면 될것 같다.

throws

지금까지 학습한 내용들은 try가 주축이 되어서 설명하였다.
(throw는 단독으로 사용할 수 도 있지만...)

아무튼 이 방법은 try를 전혀 사용되지 않는 방법이다.
다음 처럼 작성되어진다.

public void test() throws Exception{
        
}

이 코드가 특별한 이유는
try - catch같은 경우는 자기 자신이 알아서 처리 하지만,
이 방법은 이 메소드를 사용한다면 무조건 에러 처리를 해줘야한다.

어떻게 보면, 바이러스를 퍼트려서 주변 사람들도 힘들게 하는 것 같다.
하지만 코드는 try-catch를 사용하는 것보다 깔끔해졌으니 좋다...

throws를 한 뒤, try - catch 구문으로 쌓아도 된다.

try catch

 그러면 throws의 영향력은 어디까지 미치는지 알아보자.

 오버라이딩

확인결과 : 직접적으로 사용하지 않으면, 사용되지 않는 것 같다.

오버로딩

statict없어도 되는데.. 추가했다.

여기서 알 수 있는 점은 직접 사용하지 않는다면, 에러를 던질 필요가 없다는 사실을 알게 되었다.

지금까지 Exeption으로만 코딩을 하였다. 도중에 RuntimeException, IOException, 등장하였지만,
대 부분의 코드를 Exception으로만 코딩하였다.

커스텀한 예외 만드는 방법

커스텀한 예외를 만드는 방법은 생각보다 쉽다.
기존에 가지고 있는 예외들을 상속해서 사용하면 되기 때문이다.
(자바에 없는 예외를 재 창조하지 않는 이상 불 가능[근데 그런 방법이 존재하나...])

Exception이나 그 하위의 Exception으로 상속을 받은뒤,
적절하게 재 정의하는 방법으로 커스텀한 예외를 만들 수 있습니다.

그렇지만, 사용자 정의 Exception을 사용하는 것이 좋을까?
자바에서 제공해주는 Exception들로 충분히 커버 가능 하기 때문이다.

잘모르지만, 자바에서 Exception 재 정의가 있는 이유가 분명히 있을 것 같다.

과거에는 프로그래밍을 강제하기 위해 checked Exception을 사용했다고는 하지만,
최근에는 환경이 바뀐 만큼 그럴 필요까지는 없을 것 같다.

그렇다는 이야기는 지금은 unchecked Exception을 사용된다는 것인데,
이것들을 커스텀 예외로 만들시 어떤 이점이 존재할까?

 

custom exception을 언제 써야 할까?

우아한테크코스의 두 크루인 오렌지와 우가 싸우고 있다. 왜 싸우고 있는지 알아보러 가볼까? 오렌지 : 아니 굳이 사용자 정의 예외 안 써도 됩니다!! 우 : 아닙니다!! 써야 합니다!!! 사용자 정의

woowacourse.github.io

모쪼록 두 분 잘 해결되시길 바랍니다.;; (이젠 그럴 필요는 없나...)

이들이 말하길
custome exception을 사용하는 이유는 

첫 번째, 이름으로도 정보 전달이 가능하다.

이 부분같은 경우는 커스텀이 아니여도 충분히 의미 전달이 가능하기 때문이다.
예를 들어, 위에서 언급한 "ClassCastException"만 봐도 충분히 알 수 있다.
캐스팅이 실패 했다는 것을....
충분히 커스텀이 아니여도 의미 전달이 가능하다.

두 번째, 좀더 상세하게 에러를 전달 할 수 있다.

많은 RuntimeExeption중에서 나는 NegativeArraySizeException라는 익셉션을 선택하였다.
이 익셥션은 간단히 말해, 배열의 크기가 음수이면 안된다는 것이다.

일단, 이 예외같은 경우 배열에 직접적인 영향을 주는 Exception이기 때문에,
말은 안되지만, 배열의 크기가 양수일때 이 예외를 터트리는걸로 수정하였다.

int[] arr = new int[10];
if (arr.length > 0) {
     throw new NegativeArraySizeException("배열의 크기가 양수입니다.");
}

 근데 나는 배열의 크기도 함께 알고 싶다.
왜냐하면, 그래야 나중에 수정하기 용이하기 때문이다.
(IndexOutOfBoundsException를 예시로 두면 더 와닿을 수 있겠지만, 이미 저 링크에서 사용했기 때문에 이렇게 하려고 한다.)

아무튼,
지금 배열의 크기도 알고 싶다면,

int[] arr = new int[10];
   if (arr.length > 0) {
       throw new NegativeArraySizeException(arr.length +" " + "배열의 크기가 양수입니다.");
  }

이렇게 해야되나...
arr.length나 문자열이 거슬린다. 치워버리고 싶다. 
그래서 이렇게 수정했다.

public class PositiveArraySizeException extends NegativeArraySizeException{
    private static final String MESSAGE = "배열의 크기가 양수입니다.";

    public PositiveArraySizeException(int[] arr) {
        super(MESSAGE + "size: " + arr.length);
    }
}
     int[] arr = new int[10];
     if (arr.length > 0) {
          throw new PositiveArraySizeException(arr);
}

보다 깔끔해졌다.
만약에, 예외 처리가 한 개더 존재한다고 했을때,
이런식으로,

private static final String MESSAGE = "배열의 크기가 양수입니다.";

public PositiveArraySizeException(int[] arr, int index) {
    super(MESSAGE + "size: " + arr.length);

     try {

     }catch (IndexOutOfBoundsException ex) {
        throw new IndexOutOfBoundsException("범위를 벗어났습니다.");
     }
}

뭔가... 반대가 된것 같은 느낌이지만...,
아무튼 내가 하고 싶은 말은 예외처리를 조금더 체계적으로 할 수 있을 것 같다.
하긴, 배열의 크기가 양수라면, 에러가 발생한것 부터 말이 안되긴 했지만...

세 번째, 예외에 대한 응집도가 향상된다.

일단, 응집도에 대해 알아봐야 할것 같다.

응집 : 한군데에 엉겨서 모이는 것

응집이 하나로 뭉치는거라는데... 그렇다는 이야기는 함께 사용할 수 있어야 된다는 뜻이다.
그렇다고, 결합도를 높여서 사용하면 안된다.
결합도는
A클래스,B클래스가 존재하는데, B클래스를 이용하기 위해 A클래스는 이용해야되는 상황을 뜻한다.
그렇다면 어떻게 응집도를 높일 수 있을 까?
간단히 생각해서
매개변수(인자) 로 만들면 쉽게 해결할 수 있다. 매개변수(인자)로 만들었다는 이야기는
그곳에 값을 추가할 수 있다는 뜻이다.
그렇게되면, 두 클래스는 함께 사용이 되고, 서로의 코드는 융화된다.
이것이 응집도다.

예외에 대핸 응집도가 향상된다는 의미는,
언제든지, 예외 클래스들을 함께 사용할 수 있다는 의미가 아닐까 추측한다.

public class PositiveArraySizeException extends NegativeArraySizeException{
    private static final String MESSAGE = "배열의 크기가 양수입니다.";

    public PositiveArraySizeException(int[] arr, IndexOutOfBoundsException ex) {
        super("배열의 크기: "+arr.length +" " + MESSAGE);
        ex.printStackTrace();
    }
}

이런식으로 작성하게 되면, 이 예외 클래스는 IndexOutBoundsException과 함께 사용해야 된다.

int[] arr = new int[10];
     throw new PositiveArraySizeException(arr,100,
         new IndexOutOfBoundsException()
);

잘 모르겠지만, 이런게 응집도가 아닐까?

네 번째, 예외 발생후 처리가 용이하다.

위 글을 참조해서 읽어보면, 표준 예외들은 재 사용성이 높기 때문에 좋다고 하지만,
그 편리성때문에  정확한 위치에서 발생했는지 헷갈리는 경우가 많다고 한다.
예를 들어,

int[] first = new int[10];
int[] two = new int[5];
int[] second = new int[10];
int[] four = new int[10];
int[] five = new int[10];

배열이 5개가 존재하는데,
배열의 크기가 10이 안되는 것들을 찾고 싶졌다.
그러면,

public class SecondIndexOutOfBoundsException extends RuntimeException{
    public SecondIndexOutOfBoundsException(List<?> list, int size) {
        for (int i = 0; i < list.size(); i++) {
            int[] o = (int[])list.get(i);
            if (o.length < size) {
                throw new IndexOutOfBoundsException(i+1+"번째 배열에서 에러가 발생하였습니다.");
            }
        }
    }
}

이렇게 만들 수 있는데, 
여기에는 한 가지 잘못된 점이 존재한다.
그건 에러를 한번 찾으면 다음것은 순회하지 않는다는 점이다.
고칠려고 한다면, 고칠수야 있겠지만,
여기서 알 수 있는건 2번째 배열에서 틀렸는지 단번에 알아냈다는 것이 가장 크다.

 위 글 예제에서는 스프링을 예시로 들었는데, 솔직히 잘 와닿지를 않는다.
그래서 이렇게 코드를 작성했다. 틀린 코드라는건 알고 있는데, 그래도 의미만 재대로 전달하면 되지 않을까 해서 이렇게 작성해봤다.

다섯 번째, 예외 생성 비용을 절감시킨다.

에러가 깊어지면, 마치 스택오버플로우가 뜬것 처럼 에러 스택이 발생된다.
그러면 그 에러 스택을 하나하나 뜯어보며 어디서 에러가 발생했는지,
역 추적 해볼 수 있다.
하지만, 협업 상황이라면,
에러 내용들을 적절히 묶어서 조금더 명시적으로 작성할 수 있다.
즉, 에러 스택에 100개가 되었다면 10개로 줄인다는 뜻이된다.
그렇게 된다면, 조금더 효율적으로 할 수 있을지 않을까 생각한다.

지금까지 커스텀 예외와 이것이 왜 필요할것인지에 대해 우테코글을 참고해서 작성해봤다.
내가 생각할때, 커스텀 예외는 일반적인 상황에서는 사용하지 않는 편이 좋을 것같다고 생각한다.
하지만, 커스텀 예외를 사용해서 조금더 효율적으로 코드를 짤 수 있을때 사용하는 것이 좋지 않을까?
물론, 판단근거는 찾기 굉장히 어렵지만 말이다.(공부 열시미 하자)
아마 많은 사람들의 의견이 나뉠거라 생각한다.

+) Try - with -Resource 

준비물 : resouce와 여러가지 예외를 준비한다.

public class Resource implements AutoCloseable {

    public void action() {
        System.out.println("action");
        throw new ARuntimeException();
    }

    @Override
    public void close() {
        System.out.println("close");
        throw new BRuntimeException();
    }
}
public class ARuntimeException extends RuntimeException{
}

public class BRuntimeException extends RuntimeException{
}

준비가 끝나면 이것을 실행해본다.

public static void main(String[] args) {
    Resource resource = new Resource();
    resource.action();
    resource.close();
}

하지만 이건 action에서 에러가 발생했기 때문에 프로그램은 죽었고, close가 되지 않았다.

try - finally를 사용해서 close가 되게 하자.

Resource resource = new Resource();
try {
     resource.action();
     } finally {
      resource.close();
}

이것 같은 경우는 close라는것을 호출하긴 했지만, 위에서 보인것 처럼 close도 에러를 호출하게 되어있다.

이렇게 해서는 제대로된 디버깅이 어렵다.
왜냐하면, 에러가 close만 될때 발생되었고, action일때는 에러가 발생하지 않았기 때문이다.
이를 해결하기 위해,

try(Resource resource = new Resource()) {
      resource.action();
}

코드를 이렇게 수정하였다.

내가 원하는데로 A,B모두 에러가 나오는것을 확인되었다.
이런것이 try-with-resource라는 방법이고,
기존 방법보다 디버깅하기 쉽다는 장점을 가지고 있으며,
설령, 에러스택이 다보인다고 해도, 우리 눈에 보기쉽게 순차적으로 보여진다는 특징을 가지고 있다.

 

아래는 백기선님의 이펙티브 강의 영상이구 그 밑은 stackoverflow글이다.
www.youtube.com/watch?v=zqjZBSqHs0s


stackoverflow.com/questions/48449093/what-is-wrong-with-this-java-puzzlers-piece-of-code

 

What is wrong with this Java Puzzlers piece of code?

In new, third edition of Effective Java Joshua Bloch mentions piece of code from Java Puzzlers (it's about closing resources in try-finally): For starters, I got it wrong on page 88 of Java Puzz...

stackoverflow.com

'프로그래밍 언어 > 자바' 카테고리의 다른 글

어노테이션  (0) 2021.01.31
ENUM  (0) 2021.01.24
예외 처리  (0) 2021.01.14
프록시  (0) 2021.01.04
인터페이스  (0) 2021.01.04
나만의 DI프로그램 만들기  (0) 2021.01.03

댓글(0)

Designed by JB FACTORY