예외 - 에러 vs 예외, 종류, 예외 다루는 법

박영준·2022년 12월 7일
0

Java

목록 보기
84/112

1. 에러 vs 예외

에러

  • 프로그램 코드에 의해 수습될 수 없는 심각한 오류

  • java.lang.Error 클래스의 하위 클래스들

  • 시스템이 비정상적인 상황인 경우에 사용(메모리가 부족하는 등...)

  • 주로 JVM 에서 발생시키기 때문에, 애플리케이션 코드에서 잡아서는 X
    잡아서 대응할 수 있는 방법도 X
    → 따라서 시스템 레벨에서 특별한 작업을 하는게 아니라면, 이러한 에러 처리는 하지 않아도 무방

예외 (Exception)

  • 프로그램 코드에 의해 수습될 수 있는 다소 미약한 오류

  • Error와 달리, java.lang.Exception 클래스 + 하위 클래스들은 어플리케이션 코드에서 예외가 발생하였을 경우에 사용

  • Exception 은 체크 예외 / 언체크 예외 로 구분된다.

2. 예외의 종류

(상속계층도 中 에러와 예외계층도)

1) Exception클래스

  • 외적 요인에 의해 발생하는 예외
    • 존재하지 않는 파일 이름을 입력
    • 클래스 이름을 잘못 적은 경우
    • 입력한 데이터 형식이 잘못된 경우
    • ...

2) RuntimeException클래스

  • 프로그래머의 실수로 발생하는 예외
    • 배열의 범위를 벗어난 경우
    • 잘못된 형변환
    • ...

(1) ArithmeticException

산술적인 연산에 오류 있을 시 (어떤 수를 0으로 나누는 경우 등...)

(2) ArrayStoreException

객체 배열에 잘못된 객체 유형을 저장할 경우

(3) ClassCastException

추상 클래스 Animal 의 상속을 받은 Dog 클래스, Cat 클래스가 있다.

public  class ClassCastExceptionExample {
	public static void main(String[] args) {
    	Dog dog = new Dog();
        changeDog(dog);		//Dog 객체의 매개값으로 dog 를 줌
        
        Cat cat = new cat();
        changeDog(cat);		// Cat 객체의 매개값으로 cat 을 줌
        
        public static void changeDog(Animal animal) {
        	if (animal instanceof Dog) {
            	Dog dog = (Dog) animal;		// 이미 animal은 Cat 객체의 매개값으로 cat 을 줬기 때문에, Dog 타입으로 변할 수 없게 되어서ClassCastException 발생
            }
        }
	}
}
  • 클래스 간의 형 변환 오류시 발생

    • 상위 클래스 & 하위 클래스, 인터페이스 & 구현 클래스 간에 타입 변환이 발생
    • 그러나, 이런 관계가 아님에도 타입 변환이 발생할 경우 ClassCastException 이 발생하게 된다.
  • instanceof : 객체의 타입을 확인할 때 사용

    참고: Java 예약어

(4) IllegalArgumentException

매개변수가 의도치 않는 상황 유발시
(메소드의 전달 인자값이 잘못될 경우)

NumberFormatException

public class NumberFormatExceptionExample {
	public static void main(String[] args) {
    	String data1 = "100";		
        String data2 = "a100";		// data2 는 문자
        
        int value1 = Integer.parseInt(data1);		// "100"인 문자 data1 은 숫자로 변환 가능
        int value2 = Integer.parseInt(data2);		// "a100"인 문자 data2 은 숫자로 변환 불가능 → NumberFormatException 발생
        
        int result = value1 + value2;
        System.out.println(data1 + "+" + data2 + "=" + result);
    }
}

숫자형 인자를 요구하는 매개변수에 숫자형이 아닌 인자(문자)가 주어진 경우
("5" → 5, "a" → X)

(5) IndexOutOfBoundsException

인덱스 매개변수 값이 범위(배열, 문자열, 벡터 등...에서 범위 밖의 index)를 벗어났을 때

ArrayIndexOutOfBoundsException

배열에서 인덱스 범위를 초과할 경우 발생
(길이가 3인 배열에서는 arr[0] ~ arr[2]를 사용할 수 있으나, 실제론 arr[3]을 사용할 경우)

잘못된 코드

public class ArrayIndexOutOfBoundsExceptionExample {
	public static void main(String[] args) {
    	String data1 = args[0];		// ArrayIndexOutOfBoundsException 발생 → 실행 매개값이 없으므로, 인덱스 사용이 불가능
        String data2 = args[1];
        
        System.out.println("args[0]: " + data1);
        System.out.println("args[1]: " + data2);
    }
}

올바른 코드

public class ArrayIndexOutOfBoundsExceptionExample {
	public static void main(String[] args) {
    	if(args.length == 2) {		// 배열값을 일기 전에 배열의 길이를 먼저 조사한다
    		String data1 = args[0];
        	String data2 = args[1];
        
        	System.out.println("args[0]: " + data1);
        	System.out.println("args[1]: " + data2);
            
        } else {
        	System.out.println("두 개의 실행 매개값이 필요");
    }
}

(6) IllegalStateException

객체가 메소드 호출하기 위한 상태가 아닐 시

(7) NullPointException (= NPE)

예시 1

public class NullPointExceptionExample {
	public static void main(String[] args) {
    	String data = null;		// 변수 data 는 null값을 가지므로, String 객체를 참조하지 않는다.
        
        System.out.println(data.toStirng());		// 그러나, String 객체의 toString() 메소드를 호출하고 있다.
    }
}

예시 2

// 수정 전
List<String> names = getNames();
names.sort(); 				// names가 null이라면, NPE가 발생

// 수정 후
List<String> names = getNames();
if (names != null) {		// NPE를 방지하기 위해서는 null 검사를 해야 함
    names.sort();
}    
  • null을 허용하지 않는 메서드에 null을 건넸을 때, 매개변수 값이 null 일 때
    (Null 객체 참조 시)

  • null 검사를 해야하는 변수 多 경우, 코드가 복잡해지고 번거로우므로
    null 대신 초기값을 사용하길 권장하기도 함

(1) NPE 의 문제점

/******* NPE 발생 예시 *******/
public static void main(String[] args) {

	String a = null;

	System.out.println("1번째============");
	if (a == "god") {
		System.out.println("참");
	} else {
		System.out.println("거짓"); //거짓 출력
	}

	System.out.println("2번째============");
	if (a.equals("god")) { // NPE 발생! 
		System.out.println("equals => 참");
	} else {
		System.out.println("equals => 거짓");
	}
}
/******* 결과 *******/
1번째============Exception in thread "main" 
거짓
2번째============
java.lang.NullPointerException
  1. null 자체의 의미가 모호해서, 다양한 파생 에러 발생
  2. 에러 발생 이후, 디버깅이 매우 어렵다

(2) NPE 예방 및 해결법

  1. 의미 없는 null 을 Parameter 로 넘기지 않기

    int size = param.getSize();
    
    totalPages = (int) (totalCnt / size); 

    size 파라미터(인자)가 다음 코드로 넘어가는데
    이 경우 null 이 넘어가게 되면, int 타입이므로 default 값인 0 을 선언하게 된다.
    이 떄, '숫자 / 0' 이 되면서 ArithmeticException 가 발생하게 된다.

  2. "문자열"부터 제시해서, 이를 비교의 주체로 삼기

    /******* 예시 1 : 기본 *******/
    String a = null;
    System.out.println(a.indexOf("갓"));
    
    /******* 예시 2 : equals 사용 *******/
    public static void main(String[] args) {
        String a = new String("god");
    
        System.out.println("1번째============");
        if (a == "god") {
            System.out.println("참");
        } else {
            System.out.println("거짓"); // 거짓출력
        }
    
        System.out.println("2번째============");
        if (a.equals("god")) {
            System.out.println("equals => 참"); // 참출력
        } else {
            System.out.println("equals => 거짓");
        }
    }
    /******* 결과 1 *******/
    Exception in thread "main" java.lang.NullPointerException
    
    String a = null;
    if(a != null){
        System.out.println(a.indexOf("갓"));
    }
    
    /******* 결과 2 *******/
    1번째============
    거짓
    2번째============
    equals =>
  3. toString() 대신, valueOf() 사용
    toString() 를 사용할 경우

    public static void main(String[] args) {
        Integer a = 1;
        System.out.println(a.toString());
    
        a = null;
        System.out.println(a.toString());
    }
    // 결과
    1

    a 변수에 null 이 올 경우, NullPointerException 이 발생하게 된다.

    valueOf() 를 사용할 경우

    public static void main(String[] args) {
        Integer a = null;
        System.out.println(String.valueOf(a));
    }
    // 결과
    null
  4. Chaining 메소드 호출 자제하기

    // Method Chaining 기본 문법 형태
    객체.메소드().메소드().메소드(); 
    
    // 예시
    String polcValCont = moStorePolicyService.getStorePolcBase(polcCd).getPolcValCont();

    이런 체이닝 메소드에서 "NPE"가 발생하면, 디버깅 하기가 어렵다.

  5. Apache Commons에서 제공하는 StringUtils 사용하기

    public static void main(String[] args) {
        System.out.println(StringUtils.isEmpty(null)); 	// true
        System.out.println(StringUtils.equals("1", null)); 	// false
        System.out.println(StringUtils.equals(null, "1")); 	// false
        System.out.println(StringUtils.indexOf("갓", null)); 	// -1
        System.out.println(StringUtils.indexOf(null, "갓")); 	// -1
        System.out.println(StringUtils.upperCase(null)); 	// null
    }
    // 결과
    true
    false
    false
    -1
    -1
    null
  6. Spring 을 사용하고 있다면, @NotNull 사용하기

(8) SecurityException

보안 위반 발생 시, 보안 관리 프로그램에서 발생

(9) UnsupportedOperationException

객체가 메소드를 지원하지 않는 경우

3. 예외를 다루는 방법

1) 예외 복구

  • 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것
    만약 예외로 어떤 작업의 처리가 불가능하다면, 다르게 작업을 처리하도록 유도

  • 예시 : 다른 API 호출에 실패하였을 경우, 3회 정도 retry 하여 복구되도록 함

2) 예외 처리 회피

// ResultSet 이나 PreparedStatement 와 같은 경우, 메서드에 SQLException 을 던져 호출하는 쪽에서 처리하게끔 정의
public interface ResultSet extends Wrapper, AutoCloseable {
	...
    boolean next() throws SQLException;
    void close() throws SQLException;
    ...
}
  • 예외 처리를 직접 처리 X, 자신을 호출한 곳으로 던져버리는 것

  • 해당 예외를 처리하는 것이 자신이 해야될 일이 아니라고 느껴진다면, 다른 메소드에서 처리하도록 넘겨줄 때 사용
    (단, 무작정 예외를 넘겨주는 것은 무책임한 회피가 될 수 있으므로 상황에 따라 적절히 사용)

  • 방법 1 : throws 문으로 선언해서 예외가 발생하면, 알아서 던져지게 하기
    방법 2 : catch 문으로 일단 예외를 잡은 후, 로그를 남기고 다시 예외를 던지기(rethrow)

3) 예외 전환

  • 예외 회피와 마찬가지로, 예외를 복구할 수 없는 상황에 사용

  • 예외 처리 회피와 다르게, 적절한 예외로 변환하여 던짐

  • 목적

    • 의미 있고 추상화된 예외로 바꾸는 경우
    • 런타임 예외로 포장하여, 불필요한 처리↓ 경우

언제 예외를 변경하는가?
1. 내부에서 발생한 예외를 그대로 던지는 것이 적절한 의미를 부여하지 못한다

해결법
의미있고 추상화된 예외로 바꾼다.

예시
새로운 사용자를 등록하고자 할 때 동일한 아이디가 존재하면 SQLException이 발생
그러나, SQLException 에러를 그대로 던지면, 이를 이용하는 서비스 계층에서는 예외 발생 이유에 대해 파악이 어려움
→ DuplicatedUserIdException 과 같은 예외로 바꿔서 던지면,
확실히 의미 전달이 가능하며 상황에 따라 복구작업을 시도할 수도 있을 것이다.

  1. 체크 예외에 의해, 불필요한 에러 처리가 多 경우

해결법
런타임 예외로 포장하여(언체크 예외로 변경하여), 불필요한 처리 줄이기
→ 복구하지 못할 예외라면, 불필요하게 체크를 할 필요가 없기 때문

예시
어플리케이션 로직 상에서 런타임 예외로 포장하여 던지고,
자세한 로그를 남기거나 알림을 주는 등...의 방식으로 처리할 수 있다.


참고: [Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)
참고: [Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)
참고: [JSP/Spring]WAS(톰켓)와 웹서버(아파치)의 차이
참고: [Java] 체크 예외(Check Exception)와 언체크 예외/런타임 예외 (Uncheck Exception, Runtime Exception)의 차이와 올바른 예외 처리 방법
참고: Spring Exception, 제대로 처리하기
참고: @RestControllerAdvice 를 이용해서 예외 처리하기
참고: [스프링부트] 예외 처리하기(@ExceptionHandler , @RestControllerAdvice )
참고: Spring Exception Handling
참고: Exception Handling과 Response 코드 개선
참고: [스프링부트] @ExceptionHandler를 통한 예외처리
참고: 예외의 종류
참고: Java – Exception Hierarchy
참고: HTTP 상태 코드 정리
참고: 명쾌한 Custom Exception in Java
참고: [Java] NullPointException 원인, 예방, 해결하기
참고: Java 예외처리 try catch
참고: Java | Exception 예외

profile
개발자로 거듭나기!

0개의 댓글