[After Java Semina] 예외 처리

Jiwon-Woo·2021년 9월 11일
0

After Java Semina

목록 보기
4/5

1. 예외처리 클래스

1.1 오류란?

프로그램의 오류는 크게 컴파일 오류와 실행 오류로 나눌 수 있다.

컴파일 오류는 문법에 맞지 않게 작성된 코드를 컴파일러가 사전에 알려주기 때문에 디버깅이 어렵지 않다. 하지만 실행 오류는 문법은 맞지만 프로그램이 의도와 다르게 흘러가는 오류이기 때문에 예측과 디버깅이 비교적 어렵다.

1.2 오류와 예외

실행 오류는 다시 한번 자바 가상 머신에서 발생하는 시스템 오류와 프로그래머가 제어할 수 있는 예외로 나뉜다. 예외의 대표적인 예는 다음과 같다.

  • 정수를 0으로 나누는 경우
  • 배열의 크기보다 큰 인덱스로 배열의 요소에 접근하는 경우
  • 존재하지 않는 파일을 읽으려고 하는 경우
  • 정수 입력을 기다리는 프로그램에 문자를 입력한 경우
  • 네트워크로 데이터를 전송하려는데 연결이 안된 경우

예외처리를 사용하는 이유 : 일반적으로 에러와 예외는 발생시 프로그램이 종료된다. 다만, 예외의 경우 예외처리를 통해 프로그램을 종료시키지않고 정상실행상태가 유지되게 할 수 있다.

자바에서는 컴파일시 예외가 발생할 가능성이 높은 코드(특정 클래스, 메소드 등)을 사용할 때 적합한 예외처리를 적용하였는지 확인한다. 만약 적합한 예외처리를 하지 않았다면 컴파일 오류를 낸다. 또한, 발생한 예외는 클래스를 통해 관리한다. JVM은 프로그램이 실행되는 도중에 예외가 발생한다면, 예외클래스를 통해 객체를 생성하고, 예외처리코드()에서 예외 객체를 이용 할 수 있게 한다.

1.3 예외 클래스의 종류

오류 클래스는 모두 Throwable 클래스에서 상속 받는다. Throwable 클래스의 하위 클래스는 크게 시스템에서 발생하는 오류를 다루는 Error 클래스와 예외 클래스의 최상위 클래스인 Exception 클래스로 나뉜다. 예외 클래스의 대략적인 구조와, 자주 사용되는 클래스는 다음과 같다.

Oracle Docs - Exception

  • OutOfMemoryError : 메모리가 부족한 경우 발생(Error)
  • IOException : 입출력 동작 실패 또는 인터럽트 시 발생 (인터럽트?)
  • FileNotFoundException : 존재하지 않는 파일에 접근하려고 할 때 발생
  • SocketException : 네트워크에 오류가 발생할 때 발생
  • RuntimeException : 프로그램 실행 중 발생할 수 있는 오류에 대한 예외를 처리
    • ArithmeticException : 정수를 0으로 나눌 때 발생
    • IndexOutOfBoundsException : 리스트나 배열의 범위를 벗어난 접근 시 발생
      • ArrayIndexOutOfBoundsException : 배열의 인덱스 범위를 초가할 경우 실행 예외.
    • NullpointerException : null 레퍼런스를 참조할 때 발생
    • ClassCastException : 클래스나 인터페이스를 변환할 수 없는 타입으로 객체를 변환할 때 발생
    • IllegalArgumentException : 잘못된 인자 전달 시 발생
      • NumberFormatException : 문자열이 나타내는 숫자와 일치하지 않는 타입의 숫자로 변환 시 발생
    • InputMismatchException : Scanner 클래스의 nextInt() 메서드를 호출하여 정수를 입력받으려고 했을 때, 정수가 아닌 문자가 입력으로 들어온 경우 발생. NoSuchElementException을 상속받음.

2. 예외처리하기

2.1 컴파일러가 잡는예외, 개발자가 잡아야 하는 예외

예외는 다시 일반 예외(Exception)과 실행예외(Runtime Exception)으로 나뉜다. java.lang.Excption 밑에는 여러 예외들이 있는데, java.lang.Excption.RuntimeException 에 있는 예외만이 실행예외이다. 나머지는 모두 일반예외이다.

이때 일반예외가 발생할 가능성이 있는 코드는 컴파일러단에서 에러를 발생시켜, 강제적으로 예외처리코드를 작성하도록 요구한다. 하지만 실행예외가 발생할 가능성이 있는 코드는 이를 요구하지 않는다.

2.2 try-catch-final문

이미지 출처 : https://tutorial.eyehunts.com/java/try-catch-finally-java-blocks-exception-handling-examples/

코드 출처: Today I Learned. @cheers_hena 치얼스헤나

try { // ... **(1)**
	// 예외발생할 가능성이 있는 문장 
}catch(Exception1 e1) { // ... **(2)**
	// Exception1이 발생했을 경우, 이를 처리하지 위한 문장적는다.
	// 보통 이곳에 예외메세지를 출력하고 **로그**로 남김. 
}catch(Exception2 e2) { 
	// Exception2이 발생했을 경우, 이를 처리하지 위한 문장적는다. 
}catch(ExceptionN eN) { 
	// ExceptionN이 발생했을 경우, 이를 처리하지 위한 문장적는다. 
}catch(Exception e) { // ... **(3)**
	// 지정된 예외타입 이외의 모든 예외처리를 수행할 부분.
}finally{ 
	// 예외발생여부에 관계없이 상항 수행되어야 하는 문장적는다.
	// 정리코드(DB 연결종료, 파일닫기, 자원해제 등등)
}
  • try block : 예외 발생한 코드가 위치. 예외가 발생하면 즉시 실행을 멈추고 catch block으로 이동한다.
  • catch block : 예외처리 코드를 입력.
  • final block (생략 가능) : 예외 발생 여부와 상관 없이 항상 실행할 코드. 심지어 try, catch block내부에 return이 있더라도 final block내부의 내용은 항상 실행된다.

2.3 각 블록의 동작 방식.

  1. try안에 있는 코드를 실행한다. 문제가 없는 경우는 final로 이동한다.
  2. 예외가 발생한경우 발생한 예외와 일치하는 예외타입(예외클래스)를 선언하는 catch를 찾는다. 있으면 해당 catch로 이동하고, 예외타입(ExceptionN)을 참조변수(eN)로 인스턴스화 한다. 하지만 없으면 처리되지 못한 상태에서 에러가 발생하고 프로그램이 종료된다.
  3. 보통 catch의 마지막에는 예외타입을 Exception으로 가지는 catch문이 온다. 모든 세부 예외타입은 Exception 예외타입을 상속받기 때문에, 어떠한 예외라도 Exception 예외타입에 걸린다. 마치 if조건문의 else처럼, 지정되지 않은 모든 예외는 해당 catch문으로 처리되게 된다.

catch 예시

출처: Today I Learned. @cheers_hena 치얼스헤나

catch (AuthException e) {
	// 예외정보 출력 
	e.printStackTrace(); 
	// 로그남김 
	log.error("AuthException ERROR: {} ", e.getMassage());
	// 예외 던지기
	throw e;  
} catch (Exception e ) { 
	e.printStackTrace(); 
	log.error("Exception ERROR: {} ", e.getMassage());
	throw e; 
}

참고 : cheershennah의 블로그

2.4 다중 예외처리(다중 catch문)

사실 다중 예외처리는 위에서 여러 catch문를 쓰면서 작성하였다. 해당 부분에서는 다중 catch를 작성하면서 주의해야할 부분에 대해 서술한다.

try {
	...
} catch (하위Exception e) {
	...
} catch (상위Exception e) {
	...
}

try에서 예외가 발생하면 가장 위의 catch문부터 차례로 내려오면서 확인한다. 따라서 하위의 예외타입(...Exception)을 catch문 위에, 가장 상위의 예외타입(Exception)을 catch문 아래에 작성한다. 가장 상위의 예외타입인 Exception을 위에 작성하면, 모든 예외타입이 해당 타입에 걸리기 때문이다. 이와 같은 이유로, 예외타입을 작성할 때 예외타입간의 관계를 확인하여 부모 예외타입은 아래에, 상속받는 예외타입은 아래에 작성한다.

try {
	...
} catch (Exception e) { // 해당 부분에서 모든 예외가 걸리므로, 
	예외처리문 1             // 예외처리문 2는 결코 수행되지 않는다.
} catch (하위Exception e) {
	예외처리문 2
}

그냥 catch(Exception e)하면 모두 다 퉁칠수 있는데... 굳이 예외타입을 찾아서 쓰는 이유는?

→ 최적화를 위해 상위 예외인 Exception보다 적합한 각각의 하위 예외를 권고하는데... 그 이유는 찾기 힘들다.
(Ref - Performance improvement techniques in Exceptions)

2.5 try-with-resources문

자바 1.7(7버전)에 추가된 내용(향상된 예외처리). 외부 자원에 접근하는 경우, try-catch-final문에서는 final문에서 자원을 반환을 한다. 하지만 try-with-resources을 사용하면 더 간결하게 자원 반납 문제를 해결할 수 있다.
자원을 할당한 객체(이하 자원할당객체)를 try문이 끝나면 자동으로 close()메서드를 호출하여 자원을 반환해준다.

try {
	**자원할당객체 선언
	...**
} catch(...) {
	...
} final {
	자원 반환 코드들
	...
}
try**(자원할당객체 선언)** {
	...
} catch(...) {
	...
} final {
	...
}

이때, try-with-resources문을 사용하기 위해 사용하는 자원할당객체는 AutoCloseable인터페이스를 구현해야 자동으로 close()가 호출되어 try-with-resources문을 사용할 수 있다.

  • 예시 코드
    package trywithresource.example;

    public class Member implements AutoCloseable{
        public void work() {
            System.out.println(this + " is working");
        }
        @Override
        public void close() throws Exception {
            System.out.println(this + "'s close() is called");
        }
    }
    package trywithresource.example;

    public class TestMain {
        public static void main(String[] args) {
            try (Member mb = new Member()) {
                mb.work();
            } catch (Exception e) {
                e.printStackTrace();
            }
    				// close()를 호출하는 final block이 없다.
        }
    }
    trywithresource.example.Member@3d24753a is working
    trywithresource.example.Member@3d24753a's close() is called

참고 : 코끼리를 냉장고에 넣는 방법 블로그, codechacha.com

자바 7부터 추가된 try-with-resources 문은 자바 9에서 더 업그레이드 되었다. 자바 7에서는 자원할당객체의 선언을 반드시 try 문 괄호 안에서 해야했지만, 자바 9부터는 괄호 외부에서도 선언이 가능해졌다.

자바 7

  1. 자바 7에서 try-with-resources 문을 사용하는 첫번째 방법
    package exception;

    import java.lang.Exception;

    public class ResourceException {

        public static void main(String[] args) {
            try (MyResource myResource = new MyResource()) {
                myResource.open();
                myResource.use();
            } catch (Exception e) {
                System.out.println("예외!");
            }
        }
    }
  1. 두번째 방법
    package exception;

    import java.lang.Exception;

    public class ResourceException {

        public static void main(String[] args) {
            MyResource myResource = new MyResource();
    				// 만약 외부에서 선언했다면 또다른 참조변수를 선언해서 대입해야 했었음
            try (MyResource myResource2 = myResource) {
                myResource2.open();
                myResource2.use();
            } catch (Exception e) {
                System.out.println("예외!");
            }
        }
    }

자바 9

    package exception;

    import java.lang.Exception;

    public class ResourceException {

        public static void main(String[] args) {
            MyResource myResource = new MyResource();
    				// 선언없이 바로 try 문에 대입이 가능해짐
            try (myResource) {
                myResource.open();
                myResource.use();
            } catch (Exception e) {
                System.out.println("예외!");
            }
        }
    }
    리소스가 open() 되었습니다.
    리소스를 사용하고 있습니다.
    리소스가 close() 되었습니다.
  • 의도적으로 예외 발생 시키기
    package exception;

    import java.lang.Exception;

    public class ResourceException {

        public static void main(String[] args) {
            MyResource myResource = new MyResource();
            try (myResource) {
                myResource.open();
                myResource.use();
                throw new Exception();
            } catch (Exception e) {
                System.out.println("예외!");
            }
        }
    }
    리소스가 open() 되었습니다.
    리소스를 사용하고 있습니다.
    리소스가 close() 되었습니다.
    예외!

다중 try-with-resource


3. 예외처리 미루기

3.1 throw

public void method(String arg1...) throws Exception1, Excepton2... {
	...
}

throw키워드는 메소드 선언부 끝에 작성되어, 메소드 내부에서 발생한 예외를 메소드를 호출한 곳으로 떠넘기는 역할을 한다. 따라서, throw키워드를 사용한 메소드는 try-catch문에서 호출되어야한다.

throw 를 사용하지 않은 경우

    package exception;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;

    public class Exception {
        public Class loadClass(String fileName, String className) {
            try {
                FileInputStream fis = new FileInputStream(fileName);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            Class c = null;
            try {
                c = Class.forName(className);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return c;
        }

        public static void main(String[] args) {
            Exception ex = new Exception();
            ex.loadClass("a.txt", "nothing");
        }
    }

즉, 예외처리를 미루지 않은 경우, 메서드의 사용여부와 상관 없이 메서드를 구현할 때, try-catch 문을 사용하여 예외처리를 해주어야한다. 하지만 throw 를 사용하면 메서드를 사용할 때 예외처리를 하도록 미룰 수 있다. try 문 안에 그 메서드를 감싸는 형식으로 구현된다.

throw 를 사용하여 예외처리 미루기

    package exception;

    import java.io.FileInputStream;
    import java.io.FileNotFoundException;

    public class ThrowException {
        public Class loadClass(String fileName, String className) throws FileNotFoundException,ClassNotFoundException {
            Class c = Class.forName(className);
            FileInputStream fis = new FileInputStream(fileName);
            return c;
        }

        public static void main(String[] args) {
            ThrowException ex = new ThrowException();

            try {
                ex.loadClass("a.txt", "nothing");
            } catch (ClassNotFoundException e) {
                System.out.println(e);
                e.printStackTrace();
            } catch (FileNotFoundException e) {
                System.out.println(e);
                e.printStackTrace();
            }
        }
    }

throws 사용 이유

try - catch 문으로 예외를 처리하기에 한계가 드러나기 때문!

throw 예외 발생시키기

public class ThrowException1 {

        public void sayNick(String nick) {
            if("fool".equals(nick)) {
                return;
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }

        public static void main(String[] args) {
            ThrowException1 test = new ThrowException1();
            test.sayNick("fool");
            test.sayNick("genious");
        }
    }
output=>
당신의 별명은 genious 입니다.

nick 매개변수에 들어온 fool은 equals 에 걸려서 return 되기 때문에 fool 을 제외한 genius만 출력이 된다.

RuntimeException

위의 코드를 변형시켜서 Output이 나오는 것이 아닌 예외를 출력시키도록 할 수 있다.

public class FoolExcep extends RuntimeException {
}
public class ThrowException1 {

        public void sayNick(String nick) {
            if("fool".equals(nick)) {
                throw new FoolExcep();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }

        public static void main(String[] args) {
            ThrowException1 test = new ThrowException1();
            test.sayNick("fool");
            test.sayNick("genious");
        }
    }
output=>
Exception in thread "main" FoolExcep
	at ThrowException1.sayNick(ThrowException1.java:5)
	at ThrowException1.main(ThrowException1.java:12)

위의 결과와 다르게 fool에서 발생한 예외로 인해 출력이 정상적으로 작동되지 않는다. FoolExcep에서 RuntimeException을 상속 받았기 때문인데 RuntimeException은 Exception과 다르게 실행시 발생하는 예외를 처리해주는 class이기 때문이다.

public class foolException extends Exception{

}
public class RunTImeExceptionTest {
    public void sayNick(String nick)
    {
        try {
            if("fool".equals(nick)) {
                throw new foolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(foolException e) {
            System.err.println("FoolException이 발생했습니다.");
        };
    }
    public static void main (String []args)
    {
        RunTImeExceptionTest test = new RunTImeExceptionTest();
        test.sayNick("fool");
        test.sayNick("geinus");
    }
}
output=>
FoolException이 발생했습니다.
당신의 별명은 geinus 입니다.

RuntimeException을 상속하던 것을 Exception을 상속하도록 변경했다. 이렇게 하면 RunTImeExceptionTest 클래스에서 컴파일 오류가 발생한다. 예측 가능한 Checked Exception이기 때문에 예외처리를 컴파일러가 강제하기 때문이다.

예외 던지기 (throws)


public class foolException extends Exception{
}

public class ThrowException2 {

    public void sayNick(String nick) throws foolException
    {
        if ("fool".equals(nick))
            throw new foolException();
        System.out.println("당신의 별명은" + nick + "입니다.");
    }
    public static void main(String []args)
    {
        ThrowException2 test = new ThrowException2();
        try {
            test.sayNick("genius");
            test.sayNick("fool");
            test.sayNick("geniuss");
        }
        catch (foolException e)
        {
            System.err.println("에러가 발생하였습니다.");
        }
        finally {
            //test.sayNick("geniusss");
            System.out.println("안녕하세요");
        }

    }
output=>
당신의 별명은genius입니다.
안녕하세요
에러가 발생하였습니다.
ThrowException2 test = new ThrowException2();
        try {
            test.sayNick("fool");
            test.sayNick("genius");
       
        }
        catch (foolException e)
        {
            System.err.println("에러가 발생하였습니다.");
        }
output=>
에러가 발생하였습니다.

try-catch 구문을 이용해서 test.sayNick("fool")과 test.sayNick("genius") 가 실행이 되지만 test.sayNick("fool")에서 에러가 발생하기 때문에 catch 문으로 빠져나가게 되어서 test.sayNick("genius")부분은 실행이 되지 않게된다.

System.err가 System.out보다 나중에 뭉쳐서 나오는 이유 : 스택오버플로우


4. 사용자 정의 예외

4.1 사용자 정의 예외 클래스 구현하기(Custom Exception)

사용자 정의 예외란?

프로그램을 개발하다보면 자바 표준 API가 제공하는 예외 클래스만으로 다양한 종류의 예외를 다 표현할 수 없습니다.

이때, 직접 정의하여 사용하는 예외를 사용자 정의 예외라고 합니다

  • 사용자 정의 예외는 반드시 java.lang.Exception 클래스를 상속받아 정의해야합니다.
  • 사용자 정의 예외는 JVM에서 예외를 발생시켜 주지 않으므로 직접 예외를 생성해야합니다.
  • 예외 생성 하기 : throw new 사용자정의예외()
public class ElementException extends Exception{
    public ElementException(String str)
    {
        super(str);
    }
}
import java.util.Scanner;

public class TestException {
    public static int sum() throws ElementException{
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        if (a < 0 || b < 0)
            throw new ElementException("음수값 입력 금지");
        return (a + b);
    }
    public static void main(String []args)
    {
        try{
            System.out.printf("합 : %d", sum());
            System.out.println("sum 함수 실행 완료");
        }catch (ElementException e)
        {
            System.out.println(e.getMessage());
        }
        System.out.println("try catch 문 실행 완료");
    }

ElementException : 사용자 정의 예외 처리를 설정해줍니다. 매개변수인 a는 e.getMessage()에서 출력됩니다.

예제 코드

    package exception;

    public class MyExceptionTest {
        private String userID;

        public String getUserID() {
            return userID;
        }

        public void setUserID(String userID) {
            if (userID == null) {
                try {
                    throw new MyException("null!");
                } catch (MyException e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                }
            } else if (userID.length() < 8 || userID.length() > 20) {
                try {
                    throw new MyException("아이디 생성 규칙에 어긋남!");
                } catch (MyException e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                }
            }
            this.userID = userID;
        }

        public static void main(String[] args) {
            MyExceptionTest test = new MyExceptionTest();
            String userID = "jwoo";

            test.setUserID(null);
            test.setUserID(userID);
        }
    }
    null!
    아이디 생성 규칙에 어긋남!
    exception.MyException: null!
    	at exception.MyExceptionTest.setUserID(MyExceptionTest.java:13)
    	at exception.MyExceptionTest.main(MyExceptionTest.java:33)
    exception.MyException: 아이디 생성 규칙에 어긋남!
    	at exception.MyExceptionTest.setUserID(MyExceptionTest.java:20)
    	at exception.MyExceptionTest.main(MyExceptionTest.java:34)

    Process finished with exit code 0

4.2 예외처리와 로그

Logging이란

프로그램 개발 중이나 완료 후 발생할 수 있는 오류에 대해 디버깅하거나 운영 중인 프로그램 상태를 모니터링 하기 위해 필요한 정보(로그)를 기록하는 것

애플리케이션 실행에 대한 추적을 기록하기 위해 어딘가에 메시지 (콘솔, 파일, 데이터베이스 등)를 작성하는 것
Logging을 어디에 이용할까?

  • 디버깅
  • 사용자 상호 작용 기록 (발생하는 이벤트 기록)

예외처리시 log 할때 Framework를 사용한다.

Java Logging Framework

  • SLF4J(Simple Logging Facade for Java
    Facade pattern

Web Logging

0개의 댓글