Language/Java

[자바 스터디 9주차] 예외 처리

소프 2021. 1. 15.

github.com/whiteship/live-study/issues/9

 

9주차 과제: 예외 처리 · Issue #9 · whiteship/live-study

목표 자바의 예외 처리에 대해 학습하세요. 학습할 것 (필수) 자바에서 예외 처리 방법 (try, catch, throw, throws, finally) 자바가 제공하는 예외 계층 구조 Exception과 Error의 차이는? RuntimeException과 RE가

github.com

목표

자바의 예외 처리에 대해 학습하세요.

학습할것

  • 예외 처리란? & 예외 처리의 필요성(자체 추가)
  • 자바에서 예외 처리 방법 (try-catch, throw, throws, finally) + Multicatch Block, try-with-resources
  • 예외처리 전략(자체 추가)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

예외 처리란?

세상에 완벽한 프로그램은 없다. 코드에 문제가 없을 것 같더라도 외부환경요인 등에 의해 문제가 발생하곤 한다.

예외 처리란 문제가 발생될 만한 곳을 예상하여 사전에 '문제가 발생하면 이렇게 해라'라고 프로그래밍 하는 것이다.

 

예외 처리의 필요성

금융 시스템 또는 365일 24시간 항상 구동되어야할 시스템이 Exception으로 인해 시스템이 불능되거나 프로그램이 죽어버리는 것을 막기 위해..

즉, 어느 한 부분에서 예외가 발생하더라도 계속해서 프로그램이 동작되도록 하는데 목적이 있다.

 

Ex) 실제 사례

실제로 한 공공기관의 신규 ERP 시스템을 시연하던 중에 발생한 상황을 예로 들 수 있다. 당시 빈번한 시스템 요구사항 변경으로 인해 개발 기간이 매우 촉박했고, 이를 뒷받침할 여유 인력도 부족했다. 시연 직전까지도 신규 시스템을 개발 서버 환경에서 막 운영 서버 환경으로 이전만 했을 뿐 전체적인 시스템 점검을 할 시간이 부족했다. 시연을 위해 전체적인 시스템 점검은 불가능했고, 미리 정해진 점검 목록에 따라 최대한 점검하고, 실제 구현 중 발생할 수 있는 돌발 상황에 대비해 모든 담당자가 대기 중이었다.

 

그러다 시연 직전 마지막 리허설에서 보고서가 업로드되지 않는 문제점이 발견됐고, 시연 화면에는 “업로드 실패[CODE202] – XXX를 확인하세요.”라는 오류가 화면에 나타났다. 그 즉시 대기하고 있던 개발자는 화면상의 오류 메시지와 로그 파일을 비교/검토하고 해당 메시지를 토대로 상세 예외 정보를 검토한 결과 문제가 발생한 보고서 테이블의 컬럼 길이가 설계했던 것보다 짧다는 점을 발견할 수 있었다. 문제의 원인은 데이터베이스를 이전하는 과정에서 보고서 테이블이 최종 버전의 테이블 구조로 이전되지 않았던 것이었다. DBA는 즉시 보고서 테이블 구조를 최종 버전으로 수정했고, 해당 문제는 완벽하게 해결됐다. 문제가 해결되기까지 채 10분이 걸리지 않았고, 시스템 시연은 아무런 문제 없이 끝날 수 있었다.

 

오류 정보가 정확하지 않았거나 전혀 없었다면 문제가 발생한 위치를 재현하고 소스코드를 따라가며 문제점을 추적하는 방법 외에는 없었을 것이다. 하지만 당시 매우 긴급한 상황에서 정상적으로 이런 방식을 수행할 수 있었을지는 미지수이고, 최악의 경우 많은 시간을 소비하고 미궁에 빠져버릴 수도 있었다. 이처럼 정확한 오류 정보는 소프트웨어 품질 관리에 매우 중요한 역할을 한다.

 

 

※ 초급 개발자가 반드시 주의해야 하는 점

바로 예외를 잡고 아무런 처리도 하지않는 것!!

try/catch문으로 예외를 잡아놓고 catch문을 비워놓는다?

  - 컴파일 오류는 나지 않지만 예외가 발생했을 때 원인 파악이 어려워 개발 & 유지보수에 치명적

  - 따라서, 무작정 catch하고 무시하거나, throw해버리는 행위는 지양해야 한다.

 

※ 좀더 자세한 예외 설명

메서드내에서 에러가 발생하면, 메서드는 객체를 만들고 런타임 시스템에 전달합니다. 이 객체는 예외 객체라고 불리며 에러에 대한 정보와 이에 대한 타입, 에러가 발생한 시점의 프로그램의 상태에 대한 정보를 담고 있습니다. 예외 객체를 생성하여 런타임 시스템에 전달하는 것을 예외를 던진다고 표현합니다.

 

메서드가 예외를 던지면, 런타임 시스템은 해결할 수 있는 '무엇'을 찾기위해 시도합니다. 예외를 처리할 수 있는 '무엇'의 집합은 오류가 발생한 메소드를 사용하기 위해 불려진 메소드의 순서가 있는 리스트입니다. 메서드의 리스트는 콜 스택이라고 알려져 있습니다.

https://docs.oracle.com/javase/tutorial/essential/exceptions/definition.html

런타임 시스템은 예외를 제어할 수 있는 코드 블럭을 가지고 있는 메서드를 찾기 위해 콜스택에서 검색합니다. 이 코드 블럭을 exception handler라고 부릅니다. 검색은 오류가 발생한 메서드로부터 시작하여 메서드가 호출된 역순으로 콜 스택을 통해서 진행됩니다. 적절한 핸들러가 발견되면, 런타임 시스템은 예외를 핸들러로 전달합니다. exceptiton handler는 던져진 예외 오브젝트의 타입이 핸들러가 제어할 수 있는 타입과 일치하는 경우 적절하다고 간주됩니다.

 

선택된 exception handler는 예외를 잡았다고 말합니다. 만약에 런타임 시스템이 적절한 exception handler를 찾지 못하고 콜 스택의 모든 메서드를 철저하게 검색하면, 런타임 시스템(결과적으로 프로그램)은 종료됩니다. (이는 비정상 종료로 볼 수 있습니다.)

https://docs.oracle.com/javase/tutorial/essential/exceptions/definition.html

exception handler를 찾기 위해 콜 스택을 찾는 과정

예외를 사용하여 에러를 처리하면 기존 처리 기술에 비해 몇 가지 장점이 있습니다.

 

기존 처리 기술이라고 한다면, return을 할 때, 에러라는 응답을 해준다거나, C언어에서는 goto문을 사용하는 등의 방법이 있습니다.

1. 에러를 처리하는 코드와 일반 코드가 분리될 수 있다.

2. 콜 스택을 따라 에러 전파가 가능해 실질적으로 처리가 될 수 있는 지점에서 처리를 해줄 수 있다.

3. 오류를 그룹화 할 수 있고, 분류를 할 수 있다.

 

 

자바에서 예외 처리 방법(try-catch, throw, throws, finally) + multicatch clock, try-with-resources

 

try-catch-finally

try,catch,finally는 예외가 발생한 메소드 내에서 직접 처리하고자 할 때 작성되는 코드이다.

https://catch-me-java.tistory.com/46

try 블록에서는 예외 발생 가능성이 있는 코드를 작성하도록 합니다. 만약 try 스코프에서 예외 발생이 하지 않는다면, catch문을 거치지 않고, 바로 즉각적으로 finally 스코프 코드를 실행하며, 만약 try 스코프에서 예외처리가 발생하게 된다면, catch 블럭으로 넘어가 코드를 실행하고 finally 코드를 실행하게 된다.

 

try{
	// 예외 발생 가능성이 있는 코드
}catch(예외 타입 1 매개변수명){
	// 예외타입1의 예외가 발생할 경우 처리 코드
}catch(예외 타입 2 매개변수명){
	// 예외타입2의 예외가 발생할 경우 처리 코드
}finally{
	// 항상 처리할 필요가 있는 코드
}

try

블럭은 예외가 발생할 가능성이 있는 범위를 지적하는 블록이다.

try 블럭은 최소한 하나의 catch 블럭이 있어야 하며, catch 블럭은 try 블록 다음에 위치한다.

 

catch

블록은 매개변수의 예외 객체가 발생했을 때 참조하는 변수명으로 반드시  java.lang.Throwable 클래스 하위 클래스 타입으로 선언되어야 한다.

 

지정된 타입의 예외 객체가 발생하면 try 블록의 나머지 문장들을 수행하지 않고, JVM은 발생한 예외 객체를 발생시키며 발생한 예외 객체 타입이 동일한 catch 블록을 수행한다.

 

💡 catc문의 예외타입을 작성할 때는 항상 명확한 예외 타입을 작성하는 것을 우선해야 한다.
오류 처리가 귀찮아서 Exception 예외타입(상위)을 이용해 묶어서 catch 처리하는 경우도 있는데 이 부분은 차후 실무에서 보안에 대한 부분을 대응하다보면, 명시적이지 않기 때문에 이슈가 된다.
또한, Exception에 대한 처리 가독성이 현저히 저하된다.(어떤 이슈가 발생될지 모른다)
우가 작성하는 코드임으로 명확한 Exception 타입을 작성하여 해당 Exception에 적절한 처리가 될 수 있도록 작성하자.

※ Exception으로 catch처리하는 안티 패턴

  public void errorMethod3() {
        try {
            /*
             * 뭔가를 실행하는 코드
             */
        } catch (Exception e) {
            if (e instanceof FileNotFoundException) {
                // 오류 처리
            } else if (e instanceof IOException) {
                // 오류 처리
            }
        }
    }

 단 하나의 catch 절을 사용해 모든 오류 정보를 전달받고 특정 오류만 instanceof를 이용해 선택적으로 처리할 경우 예측 가능한 오류만 처리하고 그 밖의 모든 예외는 무시된다. 예측 불가능한 예외는 무시되거나 잘못된 오류 절차로 이어질 수 있으며, 특히 외부 오류로 발생한 오류도 예측 불가능한 오류로 분류되어 오류가 발생해도 오류가 발생한 정확한 이유를 알 수 없거나 예외 자체가 무시되어 오류가 발생하지 않은 상태로 가장할 수 있다

 

해결책

  - 내/외부 오류를 분리해서 예외를 처리

public class FileErrorExample {
    // public static Logger log = Logger.getLogger(FileErrorExample.class);

    public static void main(String[] args) {
        FileErrorExample example = new FileErrorExample();
        try {
            example.errorMethod2("test.txt", "ttt");
        } catch (FileAlreadyExistsException e) {
            // e.printStackTrace();
            logger.error("[FILE002] 파일 생성 오류");
            logger.error(e.getMessage());
        }
    }

    /*
     * FileAlreadyExistsException 파일이 이미 존재한다는 예외는 외부에서 이 메서드를 호출할 때 
     * 중복된 경로를 지정해서 발생한 오류로서 내부 오류가 아닌 외부 오류로 봐아 한다.
     */
    public void errorMethod2(String path, String str)
            throws FileAlreadyExistsException {
        try {
            File file = new File(path);
            Files.createFile(file.toPath());
            BufferedWriter output = new BufferedWriter(new FileWriter(file));
            output.write(str);
            output.close();
        } catch (IOException e) {
            // e.printStackTrace();
            logger.error("[FILE001] 파일 쓰기 오류");
            logger.error(e.getMessage());
        }
    }
}

예측 불가능한 오류는 그대로 발생하게 해서 나중에 문제가 발생했을 때 이를 처리할 수 있게 작성.

 

 

finally

필수 블록은 아니다.

finally 블록이 사용되면 finally 블록에 작성된 내용은 예외 발생 유무나 예외 catch 유무와 상관없이 무조껀 수행된다.

따라서, DB나 파일을 사용 후 닫는 기능과 같이 수행해야 할 필요가 있는 경우에 사용된다.

※ 조건에 만족하면 try-with-resources를 쓰자.

 

※ 반복문 내에서는 Checked Exception에 대한 처리는 지양해야 한다.

for (String item : items) {
    try {
        insert(item);
    }catch (SQLException e) {
        e.printStackTrace();
    }
}

반복문 내에서 Checked Exception에 대한 예외처리 구문이 들어가게 되면 성능은 2배 3배 떨어지게 된다. 이러한 경우에는 insert에서 예외 발생 시, RuntimeException으로 한번 Wrapping하여 Exception이 발생 되도록 하고 반복문 내에서는 최대한 예외처리에 대한 코드를 제거하는 것이 성능 상 유리하다.

 

 

※ finally 안에서 return을 하는 경우에는 신중해야 한다.

  • try 안에 return: finally 블럭을 거쳐 정상 실행
  • catch 만에 return : finally 블럭을 거쳐 정상 실행
  • finally 안에 return : try 블록 안에서 발생한 예외는 무시되고 finally 거쳐 정상종료(예외를 알 수 없음)

(1) try 블록 안에 return 있는 경우

public class Print {
    public String strongStringPrint(String name) {

        String str;

        try {
            StringBuilder sb = new StringBuilder();
            sb.append("*");
            sb.append(name);
            sb.append("*");
            str = "try pass";
            System.out.println(str);
            return sb.toString();
        } catch (Exception e) {
            str = "catch pass";
            System.out.println(str);
            return str;
        } finally {
            str = "finally pass";
            System.out.println(str);
        }
    }
    public static void main(String[] args) {
        Print p = new Print();
        System.out.println("정상 결과: " + p.strongStringPrint("study"));
    }
}

catch문을 통과하지 않고 출력되었다.

 

(2) 예외 발생 상황에서 catch 블록 안에 return이 있는 경우

public class Print {
    public String strongStringPrint(String name) {

        String str;

        try {
            StringBuilder sb = new StringBuilder();
            sb.append("*");
            sb.append(name);
            sb.append("*");
            str = "try pass";
            System.out.println(str);
            throw new Exception(); // 예외 발생
        } catch (Exception e) {
            str = "catch pass";
            System.out.println(str);
            return str;
        } finally {
            str = "finally pass";
            System.out.println(str);
        }
    }
    public static void main(String[] args) {
        Print p = new Print();
        System.out.println("정상 결과: " + p.strongStringPrint("study"));
    }
}

예상대로 try → catch → finally 통과 하였고 정상 결과에는 catch 블록에서 처리한 값이 출력되었다.

(3) finally에 return 이 존재하는 경우

  - 값이 변경되는 현상 발생

 

정상 종료 되는 케이스

public class Print {
    public String strongStringPrint(String name) {

        String str;

        try {
            StringBuilder sb = new StringBuilder();
            sb.append("*");
            sb.append(name);
            sb.append("*");
            str = "try pass";
            System.out.println(str);
            return sb.toString();
        } catch (Exception e) {
            str = "catch pass";
            System.out.println(str);
            return str;
        } finally {
            str = "finally pass";
            System.out.println(str);
            return "finally";
        }
    }
    public static void main(String[] args) {
        Print p = new Print();
        System.out.println("정상 결과: " + p.strongStringPrint("study"));
    }
}

정상 처리 했음에도 정상 결과에 원하는 결과가 나오지 않는다. → finally 에서 값을 변경

 

예외 발생 케이스

public class Print {
    public String strongStringPrint(String name) {

        String str;

        try {
            StringBuilder sb = new StringBuilder();
            sb.append("*");
            sb.append(name);
            sb.append("*");
            str = "try pass";
            System.out.println(str);
            throw new Exception(); // 예외 발생
        } catch (Exception e) {
            str = "catch pass";
            System.out.println(str);
            return str;
        } finally {
            str = "finally pass";
            System.out.println(str);
            return "finally";
        }
    }
    public static void main(String[] args) {
        Print p = new Print();
        System.out.println("정상 결과: " + p.strongStringPrint("study"));
    }
}

catch 에서 처리한 "catch pass" 기대했지만 예외가 발생하지 않은 것 처럼 보인다.

 

throw

throw 키워드를 통해서 고의로 예외를 발생 시킬 수 있다. 프로그램 동작 중에 개발자가 원하는 조건을 만족하지 않을 때 더 이상 코드가 진행하지 못하게 예외를 발생시킬 때 사용한다. 대표적으로 라이브러리를 만들 때 사용한다고 한다.

 

아래의 예제는 N까지의 양수의 합을 구하는 메소드이다. 해당 메소드에서 파라미터 N은 음수가 들어오면 안되기 때문에 예외를 발생시켜서 넘겨주었다.

// N까지의 양수의 합
private static int sumPositiveNum(int N) throws Exception {
	if( N < 0)
	  throw new Exception("N은 양의 정수이어야 합니다");
        
  int sum = 0;
  for(int i =1;i<=N;i++){
	  sum += i;
  }
  return sum;
}

만약 sumPositiveNum() 메소드의 파라미터로 음수를 넣어준다면 위의 코드에서 정의한 것과 동일하게 아래의 사진처럼 예외를 발생시킬 것이다.

throws

예외가 발생한 메소드를 호출한 곳으로 예외 객체를 넘기는 방법.

자바의 예외 처리 방법은 예외가 발생한 지점에서 try-catch 혹은 try-catch-finally 블록을 이용하여 직접처리 하지 않아도 예외가 발생한 메소드를 호출한 지점으로 예외를 전달하여 처리하는 방법이 있다.

→ 이 때 사용되는 예약어가 throws 이다.

public class Test{
	static void callDriver() throws ClassNotFoundException{
		Class.forName("oracle.jdbc.driver.OracleDriver");
		System.out.println("완료");
	}

	public static void main(String[] args){
		try{
			callDriver();
		}catch(ClassNotFoundException e){
			System.out.println("클래스를 찾을 수 없습니다.");
		}finally{
			System.out.println("시스템 종료");
		}
	}

}

callDrvier() 메소드에서는 ClassNotFoundException 에 대한 처리를 직접 메소드 내에서 try/catch로 처리하지 않고 throws 하여 호출한 영역에서 처리하도록 한다.

 

multicatch block

JDK1.7이상 부터는 여러개의 catch문들을 하나로 합칠 수 있게 되었다.

public static void main(String[] args) {
        try {
            Class clazz = Class.forName("java.lang.String2");
            //ClassNotFoundException 발생.

            // 다중 catch 발생하지 않음
            Main main = new Main();
            System.out.println(main.x + 1);
            // NullPointerException 발생
        } catch (ClassNotFoundException | NullPointerException e) {
            System.out.println("ClassNotFoundException 발생 또는 NullPointerException 발생");
            e.printStackTrace();
            // Error 스택 트레이스 찍음.
        }  finally {
            System.out.println("HelloWorld");
            // HelloWorld 출력.
        }

    }

※ 주의할 점

단, 나열된 예외 클래스들이 부모-자식 관계에 있다면 오류가 발생한다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            System.out.println(1 / 0);
        } catch (RuntimeException | ArithmeticException e) {
                 // 에러 발생: ArithmeticException은 RuntimeException을 상속받는 클래스이다.
            System.out.println(e.getMessage());
        }
    }
}

왜냐하면, 자식 클래스로 잡아낼 수 있는 예외는 부모 클래스로도 잡아낼 수 있기 때문데 사실상 코드가 중복된 것이나 마찬가지이기 때문이다. 이때 컴파일러는 중복된 코드를 제거하라는 의미에서 에러를 발생시킨다.

또한, multicatch는 하나의 블록으로 여러 예외를 처리하는 것이기 때문에 multicatch 블럭 내에서는 발생한 예외가 정확히 어디에 속한 것인지 알 수 없다. 그래서 참조 변수 e에는 '|'로 연결된 예외들의 공통 조상 클래스에 대한 정보가 담긴다.

 

 

try-with-resources

 

JDK 1.7부터 나온것으로 try에 자원 객체를 전달하면, try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능이다.

즉, 따로 finally 블럭이나 모든 catch 블럭에 종료 처리를 하지 않아도 된다.

try (SomeResource resource = getResource()) {
    use(resource);
} catch(...) {
    ...
}

그런데 이 때, try에 전달할 수 있는 자원은 AutoCloseable 인터페이스의 구현체로 한정된다.

/**
 * @author Josh Bloch
 * @since 1.7
 */
public interface AutoCloseable {
    void close() throws Exception;
}

그리고 아래와 같이 try() 안에 복수의 자원 객체를 전달할 수 있다.

try(Something1 s1 = new Something1();
    Something2 s2 = new Something2()) {

} catch(...) {
    ...
}

BufferReader와 InputStreamReader 클래스가 추상 클래스 Reader를 상속받았고, Reader는 Closeable 인터페이스를 상속받았으며, Closeable 인터페이스는 AutoCloseable 인터페이스를 상속받았다.

때문에 BufferReader 사용시 try-with-resources를 활용하여 아래와 같이 작성할 수 있다.

public static String getHtml(String url) throws IOException {

	URL targetUrl = new URL(url);

	try (BufferedReader reader = new BufferedReader(new InputStreamReader(targetUrl.openStream()))){
		StringBuffer html = new StringBuffer();
		String tmp;

		while ((tmp = reader.readLine()) != null) {
			html.append(tmp);
		}
		return html.toString();
	}
}

 

 

예외처리 전략

위 그림은 예외를 처리하는 일반적인 방법 3가지이다. 

 

(1) 예외 복구

  - 재시도를 통해 예외를 복구하는 코드

int maxretry = MAX_RETRY;
while(maxretry -- > 0) {
    try {
        // 예외가 발생할 가능성이 있는 시도
        return; // 작업성공시 리턴
    }
    catch (SomeException e) {
        // 로그 출력. 정해진 시간만큼 대기
    } 
    finally {
        // 리소스 반납 및 정리 작업
    }
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생

예외복구의 핵심은 예외가 발생하여도 애플리케이션은 정상적인 흐름으로 진행된다는 것이다. 위 리스트[1]은 재시도를 통해 예외를 복구하는 코드이다. 이 예제는 네트워크 환경이 좋지 않아 서버에 접속이 안되는 상황의 시스템에 적용하면 효율적이다. 예외가 발생하면 그 예외를 잡아서 일정 시간만큼 대기하고 다시 재시도를 반복한다. 그리고 최대 재시도 횟수를 넘기면 예외를 발생시킨다. 재시도를 통해 정상적인 흐름을 타게 한다거나, 예외가 발생하면 이를 미치 예측하여 다른 흐름으로 유도시키도록 구현하면 비록 예외가 발생하였어도 정상적으로 작업을 종료할 수 있을 것이다.

 

(2) 예외처리 회피

public void add() throws SQLException {
    ... // 구현 로직
}

위 코드는 간단해보이지만 아주 신중해야하는 로직이다. 예외가 발생하면 throws를 통해 호출한쪽으로 예외를 던지고 그 처리를 회피하는 것이다. 하지만 무책임하게 던지는 것은 위험하다.

※ 외부로 던져버리면 문제점의 발생 원인이 정확히 내부의 잘못에서 발생한 문제인지 외부에서 잘못 호출해서 발생한 문제인지 구분하거나 그 원인을 파악하기가 매우 어렵다.

 

호출한 쪽에서 다시 예외를 받아 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을 때만 사용해야 한다.

 

(3) 예외 전환

  - 예외전환을 위한 중첩 예외

// 예외의 에러 코드에 따라 분기하여 예외 전환
public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // code ..
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw new DuplicateUserIdException();
        }

        throw e;
    }
}

위 코드는 예외를 잡아서 다른 예외를 던지는 것이다. 호출한 쪽에서 예외를 받아서 처리할 때 좀더 명확하게 인지할 수 있도록 돕기 위한 방법이다. 어떤 예외인지 분명해야 처리가 수월해지기 때문이다. 예를 들어 Checked Exception중 복구가 불가능한 예외가 잡혔다면 이를 UnChecked Exception으로 전환하여 다른 계층에서 일일이 예외를 선언할 필요가 없도록 할 수도 있다.

 

 

 

 

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

https://velog.io/@youngerjesus/%EC%9E%90%EB%B0%94-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC

 

 

java.lang.Throwable class가 Java Exception의 Root Class이다.

  • Exception과 Error 클래스는 Throwable 클래스를 상속받아 처리하도록 되어있다.
  • Exception이나 Error를 처리할 때 Throwable로 처리해도 무관하다.
  • Throwable클래스에 선언되어있고, Exception클래스에서 오버라이딩한 메소드는 10개가 넘으며, 가장 많이 사용되는 메소드는 아래와 같다.
    • getMessage()
      • 예외 메시지를 String형태로 제공받는다.
      • 예외가 출력되었을 때 어떤 예외가 발생되었는지를 확인할 때 매우 유용하다.
      • 메시지를 활용하여 별도의 예외 메시지를 사용자에게 보여주려고 할 때 좋다.
    • toString()
      • 예외 메시지를 String형태로 제공 받는다.
      • getMessage()메소드 보다는 약간 더 자세하게, 예외 클래스 이름도 같이 제공하낟.
    • printStackTrace()
      • 가장 첫 줄에는 예외 메시지를 출력하고, 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출 관계(Stack Trace)를 출력해준다.
      • printStackTrace()는 서비스 운용시 사용하면 안된다.
        • 개발할 때는 디버깅에 유용해서 좋다. 하지만 성능을 생각하면 지양해야 한다. 왜냐하면 java reflection을 사용하여 trace를 추적하는 것이기 때문에 꽤 오버헤드가 발생한다. 성능이 중시되는 어플리케이션

 

계층은 크게 Exception과 Error로 나뉜다.

 

Exception클래스는 많은 예외들(자식클래스)을 가지고 있어모든 예외를 습득하는 것은 불가능하며 그럴 필요도 없다.

아래의 대표적인 것만 알아두자.

 

 

Built-in Exceptions in Java with examples

 

RuntimeException 

1. Arithmetic exception

  - 산술 연산에서 예외 조건이 발생했을 때 발생

class ArithmeticException_Demo { 
public static void main(String args[]) 
    { 
        try { 
            int a = 30, b = 0; 
            int c = a / b; // cannot divide by zero 
            System.out.println("Result = " + c); 
        } 
        catch (ArithmeticException e) { 
            System.out.println("Can't divide a number by 0"); // expected output
        } 
    } 
} 

 

2. ArrayIndexOutOfBounds Exception

잘못된 인덱스로 Array에 액세스했을 때 발생, 인덱스가 음수이거나 배열 크기보다 크거나 같을 때 발생

class ArrayIndexOutOfBound_Demo { 
public static void main(String args[]) 
    { 
        try { 
            int a[] = new int[5]; 
            a[6] = 9; // accessing 7th element in an array of 
            // size 5 
        } 
        catch (ArrayIndexOutOfBoundsException e) { 
            System.out.println("Array Index is Out Of Bounds"); // expected output
        } 
    } 
} 

 

3. NullPointerException

null 객체의 멤버를 참조할 때 발생

public static void main(String args[]) 
    { 
        try { 
            String a = null; // null value 
            System.out.println(a.charAt(0)); 
        } 
        catch (NullPointerException e) { 
            System.out.println("NullPointerException.."); 
        } 
    } 
} 

※ NullPointerException 을 피하는 방법

 

(1) equals 메소드 사용시

문자열 비교시 non-null String 기준으로 비교합니다. equals 메소드는 symmetric(대칭)하므로 a.equals(b)와 b.equals(a)가 동일합니다. 그렇다면 null 일 수 있는 객체에서 equals 메소드 호출은 피하는게 좋다.

public void doSomething() {
   // name이 null일 경우, NPE 발생!
   if (name.equals("BAD")) {
      // do something
   }
}

public void doSomething() {
   // name이 null이어도 NPE 발생 안함
   if ("BAD".equals(name)) {
      // do something
   }
}

다만 equals을 통과한 null 객체가 if문 안에서 메소드 호출 등의 이유로 다시 사용된다면 동일한 NPE가 발생할 수 있으니 미리 null체크를 하는것이 좋다.

 

(2) toString()보다는 valueOf()를 사용할 것
1번 처럼 null 일 수 있는 객체에서 메소드 호출은 NPE 발생 위험이 있다. static으로 제공되는 valueOf()를 사용하면 null을 Parameter로 넘겨도 null을 return할 뿐 NPE는 발생하지 않는다.

(valueOf()는 String, Boxed Primitives 클래스(Integer, Double 등)에서 static 메소드로 제공된다.)

BigDecimal bd = getPrice();
System.out.println(String.valueOf(bd)); // NPE 발생안함
System.out.println(bd.toString()); // NPE 발생

 

(3) 메소드 체이닝 호출 자제하기

String city = getPerson(id).getAddress().getCity();

중간에 return 받은 값이 null일 경우 NPE가 발생하여 Stack Trace에서도 해당 line 위치만 출력되기 때문에 어디서 에러가 발생했는지 디버깅하기도 어렵다.

 

라이브러리 이용하기

(1) Apache Commons의 StringUtils

String의 null 체크를 간단히 할 때 많이 사용하는 클래스. StringUtils.isNotEmpty(), isBlank(), isNumeric(), isWhiteSpace() 등이 있다.

// StringUtils methods are null safe, they don't throw NullPointerException
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));
 
Output:
true
true
false
false

(2) Optional 을 사용

Optional이란 "존재할 수도 있지만 안할 수도 있는 객체", 즉, "null이 될 수도 있는 객체"를 감싸고 있는 일종의 래퍼 클래스.

Optional로 객체를 감싸서 사용하면 효과

  - NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.

  - 수고롭게 null체크를 직접하지 않아도 된다.

  - 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있어 불필요한 방어 로직을 줄일 수 있다.

Optional.ofNullable(ne.num).ifPresent(
  num -> System.out.println("num은 " + num + "이다.")
);

 

4. IllegalArgumentException

어떤 메서드를 호출할때, 잘못된 변수를 전달하면 발생하는 예외입니다.

 public class ExceptionExam3 {
        public static void main(String[] args) {
            int i = 10;
            int j = 0;
            int k = divide(i, j);
            System.out.println(k);

        }       
        public static int divide(int i, int j) throws IllegalArgumentException{
            if(j == 0){
                throw new IllegalArgumentException("0으로 나눌 수 없어요.");
            }
            int k = i / j;
            return k;
        }   
    }

위와 같이 예외가 나타내는 의미랑 비슷할 때 직접 new해서 조건부합시 예외를 던질 수 있다.

 

Ex)

Mybatis 사용 시 Mapper XML에서 해당 쿼리를 찾지 못하는 현상(Mapper XML을 찾지 못하는 문제)

 

 

5. IllegalStateException

메서드 호출이 잘못된 시점에 호출되었을때 발생하는 예외입니다.

public class Example {
  public static void main(String[] args) {
    ListIterator<Object> it = new ArrayList<>().listIterator();
    it.remove(); // illegalStateException 발생
  }
}

 

6. ClassCastException

런타임시 객체를 다운 캐스팅할때, 맞는 인스턴스가 아닌 객체를 캐스팅할때 발생하는 예외입니다.

public static class Parent {}
public static class Child extends Parent {}
public static class Child2 extends Parent {}

public class Example {
  public static void main(String[] args) {
    Parent child = new Child();
    Child2 child2 = (Child2) child; // ClassCastException 발생
  }
}

 


Checked Exception

1. ClassNotFoundException

정의한 클래스를 찾을 수 없을 때 발생하는 예외

public static void main(String[] args) 
    { 
        Object o = class.forName(args[0]).newInstance(); 
        System.out.println("Class created for" + o.getClass().getName()); 
    } 
} 

2. FileNotFoundException

IOException을 상속받는 클래스, 파일에 액세스 할 수 없거나 열리지 않을 때 발생

public static void main(String args[]) 
    { 
        try { 
  
            // Following file does not exist 
            File file = new File("E:// file.txt"); 
  
            FileReader fr = new FileReader(file); 
        } 
        catch (FileNotFoundException e) { 
            System.out.println("File does not exist"); 
        } 
    } 
} 

3. IOException

입출력 작업이 실패하거나 중단될 때 발생

public static void main(String args[]) 
    { 
        FileInputStream f = null; 
        f = new FileInputStream("abc.txt"); 
        int i; 
        while ((i = f.read()) != -1) { 
            System.out.print((char)i); 
        } 
        f.close(); 
    } 
} 

4. InterruptedException

Thread가 waiting, sleeping 또는 어떤 처리를 하고 있을 때 interrupte가 되면 발생하는 예외.

 

 

5. NumberFormatException

메서드가 문자열을 숫자 형식으로 변환할 수 없는 경우 이 예외가 발생하는 예외

public static void main(String args[]) 
    { 
        try { 
            // "test" is not a number 
            int num = Integer.parseInt("test"); 
  
            System.out.println(num); 
        } 
        catch (NumberFormatException e) { 
            System.out.println("Number format exception"); 
        } 
    } 
} 

 

 

Exception과 Error의 차이는?

 

Exception과 Error은 모두 최상위 예외 클래스인 Throwable 클래스의 하위클래스이다.

 

Error

  - 시스템에 비정상적인 상황이 생겼을 때 발생

  - 시스템 레벨에서 발생하기 때문에 심각한 수준의 오류

  - 따라서 개발자가 미리 예측하여 처리할 수 없기 때문에, 애플리케이션에서 오류에 대한 처리를 신경 쓰지 않아도 된다.

  - 주로, 시스템 리소스 부족 혹은 시스템 충돌 오류

 

Exception

  - 개발자가 구현한 로직에서 발생

  - 발생할 상황을 미리 예측하여 처리할 수 있다.

  - 그럼으로 예외를 구분하고 그에 따른 처리 방법을 명확히 알고 적용하는 것이 중요하다.

 

 

RuntimeException과 RE가 아닌 것의 차이는?

 

Exception은 Checked Exception과 Unchecked Exception(=RuntimeException)으로 나뉜다.

가장 큰 차이는 '꼭 처리를 해야 하느냐' 이다.

Checked Exception이 발생할 가능성이 있는 메소드라면 반드시 try/catch로 감싸거나 throw로 던져서 처리해야 한다. 즉, 꼭 처리해야 하는 Exception이다.

 

반면에, Uchecked Exception은 명시적인 예외처리를 하지 않아도 된다.

이 예외는 피할 수 있지만 개발자가 부주의해서 발생하는 경우가 대부분이고, 미리 예측하지 못했던 상황에서 발생하는 예외가 아니기 때문에 굳이 로직으로 처리를 할 필요가 없도록 만들어져 있다.

 

※ 오해하지 말아야 할것

Springframework에서는 Transaction 설정과 관련하여 UnCheckeed Exception에 대해 roll-back 기능을 지원한다. 하지만 이는 Springframework의 tranaction설정이 제공하는 것이지, 순수 자바 언어에서 지원하는 것은 아니다. 이 기능은 springframework가 구현한 기능일 뿐, java가 제공하는 unchecked exception은 roll-back 기능이 없다.

 

Unchecked Exception vs Checked Exception

  • Unchecked Exception(=RuntimeException)
    • Unchecked Exception은 RuntimeException을 상속받은 Exception
    • 주로 Programming Errors나 외부 API 사용에 의해 발생한다.
    • 주로 프로그래머의 실수에 의해서 발생
    • 개발자가 굳이 예외처리를 할 필요가 없으며, 메소드 시그니처에 throws를 이용하여 상위로직으로 Exception을 throw할 필요가 없다.
    • 주로 복구 불가능한 상황에 대해 사용한다.
    • 프로그램 실행 시에 발생하는 예외
  • Checked Exception
    • Exception의 하위 클래스 중 RuntimeException을 제외한 Exception을 상속받은 Exception
    • 복구 가능한 상황에서 사용한다.
    • 컴파일 시점에 발생하는 예외

https://www.nextree.co.kr/p3239/

 

커스텀한 예외 만드는 방법

 

커스 텀 예외를 만들 때 참고해야 할 Best Practice 4가지

 

1. Always Provide a Benefit

자바 표준 예외들에는 포함되어 있는 다양한 장점을 가지는 기능들이 있다.

이미 JDK가 제공하고 있는 방대한 수의 예외들과 비교했을 때 만들고자 하는 커스텀 예외는 어떠한 장점도 제공하지 못한다면? 커스텀 예외를 만드는 이유를 다시 생각해볼 필요가 있다.

 

어떠한 장점을 제공할 수 없는 예외를 만드는 것 보다 오히려 UnsupportedOperationException 이나, IllegalArgumentException과 같은 표준 예외 중 하나를 사용하는 것이 낫다.

 

2. Follow the Naming Convention

JDK가 제공하는 예외 클래스들을 보면 클래스의 이름이 모두 "Exception"으로 끝나는 것을 알 수 있다. 

이러한 네이밍 규칙은 자바 생태계 전체에 사용되는 규칙이다.

즉 만들고자 하는 커스텀 예외 클래스도 이 네이밍 규칙을 따르는 것이 좋다.

 

3. Provided javadoc Comments for Your Exception Class

많은 커스텀 예외들이 어떠한 javadoc 코멘트도 없이 만들어진 경우들이 있다.

 

기본적으로 API의 모든 클래스, 멤버변수, 생성자들에 대해서느 문서화 하는 것이 일반적인 Best Practices이다.

잘 알겠지만 문서화되지 않은 API들은 사용하기 매우 어렵다.

 

예외 클래스들은 API에 크게 드러나지 않는 부분일 수 있으나 사실상 그렇지 않다.

클라이언트와 직접 관련된 메소드들 중 하낙 예외를 던지면 그 예외는 바로 예외의 일부가 된다.

그렇다는 것은 잘 만들어진 JavaDoc와 문서화가 필요하다는 뜻이다.

 

JavaDoc은 예외가 발생할 수 있는 상황과 예외의 알반적인 의미를 기술한다.

목적은 다른 개발자들이 API를 이해하고 일반적인 에러상황들을 피하도록 돕는 것이다.

/**
 * The MyBusinessException wraps all checked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyBusinessException extends Exception { ... }

 

4. Provide a Constructor That Sets the Cause

커스텀 예외를 던지기 전에 표준 예외를 Catch하는 케이스가 꽤 많다.

이 사실을 간과하지 말자.

 

보통 캐치된 예외에는 제품에 발생한 오류를 분석하는데 필요한 중요한 정보가 포함되어 있다.

아래 예제를 보면 NumberFormatException은 에러에 대한 상세 정보를 제공한다.

MyBusinessException의 cause처럼 cause 정보를 설정하지 않으면 중요한 정보를 잃을 것이다.

```java
public void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e, ErrorCode.INVALID_PORT_CONFIGURATION);
    }
}
```

Exception과 RuntimeException

Exception과 RuntimeException은 예외의 원인을 기술하고 있는 Throwable을 받을 수 있는 생성자 메소드를 제공한다.

만들고자 하는 커스텀 예외도 이렇게 하는 것이 좋다.

발생한 Throwable을 파라미터를 통해 가져올 수 있는 생성자를 최소한 하나를 구현하고 SuperClass에 Throwable을 전달해줘야 한다.

public class MyBusinessException extends Exception {
    public MyBusinessException(String message, Throwable cause, ErrorCode code) {
            super(message, cause);
            this.code = code;
        }
        ...
}

Custom Checked Exception

Custom Checked Exception의 생성은 간단하다.

Checked Exception을 구현하기 위해서는 Exception 클래스를 상속받아야 하는데, 커스텀 예외를 구현하기 위해 필요한 필수사항이다.

하지만 위에 4가지 Best Practices에서 설명했듯이 발생한 예외를 생성자에 주입하기 위한 생성자 메소드를 제공해야 하며, 표준 예외보다 더 나은 이점들을 제공해야 한다.

 

아래 예제는 설명해 온 것들을 보여준다.

  • 예외를 기술하는 JavaDoc 주석을 추가했으며,
  • SuperClass에 발생한 예외를 주입하는 생성자 메소드를 구현했다.
  • 또한, 표준 예외보다 더 나은 장점을 제공하기 위해 MyBusinessException은 문제 식별을 위한 에라코드를 저장하는 커스텀 enumeration을 사용한다.(ErrorCode는 enum타입, 밑의 Spring 예외처리와 같음)
  • 클라이언트들은 에러메시지를 보여주기 위해 이 코드를 사용할 수 있으며, support ticket 내에 이 코드를 포함하도록 유도할 수 있다.
/**
 * The MyBusinessException wraps all checked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyBusinessException extends Exception {
    private static final long serialVersionUID = 7718828512143293558 L;
    private final ErrorCode code;
    public MyBusinessException(ErrorCode code) {
        super();
        this.code = code;
    }
    public MyBusinessException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
    public MyBusinessException(String message, ErrorCode code) {
        super(message);
        this.code = code;
    }
    public MyBusinessException(Throwable cause, ErrorCode code) {
        super(cause);
        this.code = code;
    }
    public ErrorCode getCode() {
        return this.code;
    }
}

이 것들이 Checked 예외 구현을 위해 필요한 것들이다.

 

사용자는 코드에서 MyBusinessException을 던질 수도 있고 또는 메소드 시그니처에 표기할 수도 있고 tryt-catch절에서 처리할 수 있다.

public void handleExceptionInOneBlock() {
    try {
        wrapException(new String("99999999"));
    } catch (MyBusinessException e) {
        // handle exception
        log.error(e);
    }
}
private void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e, ErrorCode.INVALID_PORT_CONFIGURATION);
    }
}

Custom Unchecked Exception

 

Custom Unchecked Exception 예외 구현은 checked exception 예외 구현가 동일하다.

한가지 차이가 있는데 "Exception"을 확장하는 것이 아닌 "RuntimeException"을 확장한다.

/**
 * The MyUncheckedBusinessException wraps all unchecked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyUncheckedBusinessException extends RuntimeException {
    private static final long serialVersionUID = -8460356990632230194 L;
    private final ErrorCode code;
    public MyUncheckedBusinessException(ErrorCode code) {
        super();
        this.code = code;
    }
    public MyUncheckedBusinessException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
    public MyUncheckedBusinessException(String message, ErrorCode code) {
        super(message);
        this.code = code;
    }
    public MyUncheckedBusinessException(Throwable cause, ErrorCode code) {
        super(cause);
        this.code = code;
    }
    public ErrorCode getCode() {
        return this.code;
    }
}

다른 Unchecked 예외를 사용하는 것 처럼 MyUncheckedBusinessException 을 사용할 수 있다.

코드에서 이 예외를 던질 수 있으며 Catch 절에서 이 예외를 사용할 수 있다.

작성한 메소드가 이 예외를 던진다고 기술 할 수도 있지만 굳이 그럴 필요는 없다.

private void wrapException(String input) {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyUncheckedBusinessException("A message that describes the error.", e, ErrorCode.INVALID_PORT_CONFIGURATION);
    }
}

 

※ 추가적으로..

개발자 정의 Exception을 생성하면 주로 RuntimeException으로 많이 생성할 것이다.
왜냐하면 발생 즉시 시스템 실행 스택을 중단 할 수 있으며, 명시적인 throw 처리를 안해줘도 되는 편리함 때문에 사용하는 것 같다.
하지만, "Checked Exception"을 절대 쓰지말자"라는 말은 틀리다.
Checked Exception으로 처리하는 이유는 API를 이용하여 개발하는 개발자가 반드시 이 단계에서 예외에 대한 처리를 하여 대행 할 수 있는 로직을 추가 하던, 아예 throw를 시켜 중지 하던 개발자에게 어느정도 선택의 기회를 주는 것이라고 생각한다. 
만약, Check Exception이 발생하는 코드가 메소드 내의 메소드 내의...로 간다고 하면 밑에서 부터 계속 throw..throw.. 해야하는 상황이 발생할 수 있을 때 이러한 Exception에 대해서 전역적인 처리를 하는 경우에는 "Check Exception을 절대 쓰지 말자!", "Checked Exception이 발생하더라도 UnChecked Exception으로 Wrapping 하자!'라는 의견이 당연히 맞는 소리이다. Exception의 성질을 생각해보고, 왜 Java가 굳이 Checked Exception, Unchecked Exception을 구분했는지를 생각을 해봐야 할 것 같다.

 

Spring 예외 처리

@RestControllerAdvice
public static class ExceptionHandlers {
    @ExceptionHandler(RuntimeException.class)
    ResponseEntity<ErrorResponse> handleConstraintViolationException(RuntimeException e) {
        final ErrorResponse response = ErrorResponse.of(ErrorCode.MEMID_NOT_FOUNDED);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
}

Exception이 발생할 경우 이를 캐치해줄 ExceptionHandler을 만들어준다.

위 코드서는 RuntimeException이 발생한 경우 커스텀한 Response로 예외를 뿌려주게 된다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

    private String message;
    private int status;
    private String code;

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

}

리턴할 ResponseEntity에 담아줄 ErrorResponse 객체를 만든다.

public enum ErrorCode {
    MEMID_NOT_FOUNDED(400, "member.memId", "memId는 반드시 입력되어야 합니다."),
		MEMCODE_NOT_FOUNDED(401, "member.memCode", "memCode는 반드시 입력되어야 합니다.");

    private final String code;
    private final String message;
    private final int status;

    ErrorCode(final int status, final String code, final String message) {
        this.status = status;
        this.message = message;
        this.code = code;
    }

    public String getMessage() {
        return this.message;
    }

    public String getCode() {
        return code;
    }

    public int getStatus() {
        return status;
    }
}

enum을 만들어 객 CASE에 대한 값을 지정해 넣어준다.

public static class NoResultMemCodeException extends RuntimeException{
        public NoResultMemCodeException(String message) {
            super(message);
        }

        public NoResultMemCodeException() {
            super();
        }
    }

마지막으로 커스텀 Exception을 만들어 준다.

예외처리 비용

leegicheol.github.io/whiteship-live-study/whiteship-live-study-09-exception-handling/

 

 

예외의 전파, 

예외 복구 roll-back

www.notion.so/3565a9689f714638af34125cbb8abbe8

 

 

참고

www.notion.so/cce3fc21976f4400aa4ed8d3fb26497b

w00ks.tistory.com/entry/NullPointerException%EC%9D%84-%EC%98%88%EB%B0%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

www.daleseo.com/java8-optional-after/

jaehun2841.github.io/2018/08/30/2018-08-29-java-exception/#%EC%97%90%EB%9F%ACerror%EC%99%80-%EC%98%88%EC%99%B8exception

ryan-han.com/post/java/try_with_resources/

wikibook.co.kr/article/java-coding-with-pmd-exceptions/

www.notion.so/9-17a778bba6ed4436ac3d7b9415b6babb

 

댓글