[Java] 예외 처리

2024. 1. 28. 00:26공부 중/Java

1. 예외 처리

 

  • 심각도 : Error > Exception

 

  Error Exception
상황 프로그램을 잘못 작성
자바가 해결할 수 없는 심각한 오류
프로그램을 잘못 작성한 경우
프로그램의 작성 의도와 달리 사용되는 경우
자바가 처리할 수 있는 오류
대처 디버깅으로 코드 개선 디버깅으로 코드 개선
예외 처리 코드로 상황 수습
메모리 부족, stack overflow 등 null인 객체의 사용, 1/0, 읽으려는 파일이 없음.

 

  • 예외 처리
    • exception handing
    • 예외 발생 시 프로그램의 비정상 종료를 막고 정상적인 실행 상태로 복구하기.

 


가. 예외 클래스의 계층

 

출처 : https://www.shiksha.com/online-courses/articles/exception-handling-in-java/

 

Exception에는 Checked exception과 Unchecked exception이 존재한다.

  • Checked exception
    : 예외에 대한 대처 코드가 없으면 컴파일이 진행되지 않음.
    : Java source 뿐만 아니라 외부적 요소의 영향을 받는 예외.
  • Unchecked exception
    : RuntimeException의 하위 클래스. (코드를 개선해서 해결할 수 있는 경우)
    : 예외에 대한 대처 코드가 없더라도 컴파일은 됨.

 

// 컴파일은 문제없이 됨.
// 런타임 Exception가 발생함.
public class SimpleException {
    public static void main(String[] args) {
            //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
        int[] intArray = { 10 };
        System.out.println(intArray[2]);
        System.out.println("프로그램 종료합니다.");
    }
}

 


나. try-catch문

public class SimpleException {
    public static void main(String[] args) {
        int[] intArray = { 10 };
        try {
            System.out.println(intArray[2]);
        }catch(ArrayIndexOutOfBoundsException e) {
            System.out.println("예외 처리 완료");
        }
        System.out.println("프로그램 종료합니다.");
    }
}
/*
예외 처리 완료
프로그램 종료합니다.
*/

try문에서 예외가 발생하면 JVM이 해당 Exception 클래스의 객체를 생성한 후 throw함.

 

던져진 Exception을 처리할 수 있는 catch문에서 받아서 처리한다.

 

try-catch 문은 실행속도에 영향을 준다.

 

무책임하고 불필요한 try-catch문은 최소화하자.

 


다. Throwable

 

모든 ExceptionErrorThrowable의 자식이다.

 

 

주요 메서드 설명
public String getMessage() 발생된 예외에 대한 구체적인 메시지 반환.
public Throwable getCause() 에외의 원인이 되는 Throwable 객체 또는 null을 반환.
public void printStackTrace() 예외가 발생된 메서드가 호출되기까지의 메서드 호출 스택을 출력한다. 디버깅에 유용.

 

printStackTrace() 잘 활용하면 디버깅하기 편하다.

 

간단하게 System.out.print(e)도 사용할 수 있다.

 


라. Checked Exception 처리

public static void main(String[] args){
    Class<?> myClass = Class.forName("com.company.hello.ClassName");
    System.out.println("로딩된 클래스 : "+myClass.getSimpleName());
    System.out.println("프로그램 정상 종료");
}
/*
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
    Unhandled exception type ClassNotFoundException

    at com.company.SimpleException.main(SimpleException.java:12)
*/

Checked Exception은 처리하지 않으면 컴파일 자체가 안 된다.

 


마. Unchecked Exception 처리

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    String data = sc.next();

    int num = Integer.parseInt(data);
    System.out.println(num);
}

사용자가 숫자만 입력할 것이라 보장하지 않는다.

 

숫자가 아닌 문자열을 입력하면 아래와 같은 예외가 발생한다.

 

hello
Exception in thread "main" java.lang.NumberFormatException: For input string: "hello"
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:668)
    at java.base/java.lang.Integer.parseInt(Integer.java:786)
    at com.company.hello.Stest.main(Stest.java:25)
  • int num = Integer.parseInt(data);에서 NumberFormatExceptionthrows하고 있다.

 

Unchecked Exception가 개발자가 throwstry-catch를 강제하지 않는다고 그냥 넘어가진 말자.

 

 

쉽게 쉽게 사용해 온 Integer.parseInt() 같은 메서드도 NumberFormatException이라는 예외를 던질 수 있다.

 

사용할고 싶은 메서드가 어떠한 종류의 오류를 던지는 친구인지 확인하자.

 

오류를 던지는 경우 잊지 말고 어떻게 예외를 예방할지, 예외를 처리를 할지 고민해 보자.

 

예외를 처리하는 경우 try-catch로 내부에서 처리할 수도 있고 외부로 throw할 수도 있다.

 

내부에서 처리하기엔 너무 심각하거나, 상위 계층의 예외를 디버깅하기에 유용한 경우 throw하는 것도 고려할 수 있어야 한다.

 

예를 들어서 내가 만든 API를 누군가 사용한다고 가정하자.

 

사소한 예외는 내부에서 처리하도록 노력하고 중요한 예외는 다시 throw해야 한다.

 

그래야 사용하는 사람이 너무 귀찮지도 막막하지도 않을 것.

 


마. Multiple exception handing

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.sql.DriverManager;
import java.sql.SQLException;

public class MultiExceptionHandling {
    @SuppressWarnings("resource")
    public static void main(String[] args) {
        try {
            Class.forName("abc.Def"); // ClassNotFoundException
            new FileInputStream("Hello.java"); // FileNotFoundException
            DriverManager.getConnection("Hello"); // SQLException
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }catch(FileNotFoundException e){
            e.printStackTrace();
        }catch(SQLException e){
            e.printStackTrace();
        }catch(Exception e){
                e.printStackTrace();
            }
        System.out.println("프로그램 정상 종료");
    }
}

catch문 내부의 예외는 다형성이 적용된다.

 

그래서 주의할 점이 있다.

 

try {
    Class.forName("abc.Def"); // ClassNotFoundException
    new FileInputStream("Hello.java"); // FileNotFoundException
    DriverManager.getConnection("Hello"); // SQLException
}catch(Exception e){   // 이동
    e.printStackTrace();
}catch(ClassNotFoundException e){
    e.printStackTrace();
}catch(FileNotFoundException e){
    e.printStackTrace();
}catch(SQLException e){
    e.printStackTrace();
}
  • }catch(Exception e){e.printStackTrace(); : 여기서 모든 예외들이 처리된다.

 

하위의 catch문까지 도달하지 못한다.

 

또한 try-catch 문은 실행속도에 영향을 주는데 Exception과 같이 범위가 넓을수록 부하가 심하다.

 

상속 관계가 성립하는 경우에는 작은 범위(자식)에서 큰 범위(부모) 순으로 예외를 처리해야 하고 꼭 필요한 만큼만 확인하도록 최대한 하위 클래스의 예외를 사용하자.

 

}catch(ClassNotFoundException | FileNotFoundException e){
            e.printStackTrace();
}

|를 사용해서 묶어서 처리할 수 있다.

 

가급적 예외 상황 별로 분리해서 처리하는 것이 좋다.

 

예외가 너무 많아서 상황 별로 처리하는 것이 어려울 경우 또는 심각하지 않은 예외들의 경우는 …

  • 상속관계가 없는 여러 개의 exception인 경우 → |를 이용해 하나의 catch문으로 처리.
  • 상속관계의 여러 개의 exception인 경우 → 상위 타입의 예외로 모두 처리.

 


2. try-catch-finally

 

가. finally

 

finally는 예외 발생 여부와 상관없이 언제나 실행된다.

 

중간에 return을 만나는 경우에도 finally 블록을 먼저 수행 후 리턴을 실행한다.

 

public class MultiExceptionHandling {
    @SuppressWarnings("resource")
    public static void main(String[] args) {
        int num = new Random().nextInt(2);
        try {
            System.out.println("code 1, num: " + num);
            int i = 1 / num;
            System.out.println("code 2, 예외 없음");
            return;
        }catch(ArithmeticException e) {
            System.out.println("code 3, exception handing 완료");
        }finally {
            System.out.println("code 4, 항상 실행");
        }
        System.out.println("code 5");
    }
}
  • int num = new Random().nextInt(2); : num0, 1일 때 결과가 다르다.

 

code 1, num: 1
code 2, 예외 없음
code 4, 항상 실행

num이 1인 경우에 code 2 이후에 return 되지만 finally 블록이 실행되는 것을 볼 수 있다.

 

code 1, num: 0
code 3, exception handing 완료
code 4, 항상 실행
code 5

num이 0인 경우에 code 3 catch문 이후에 finally 블록이 실행되는 것을 볼 수 있다.

 

예외의 발생여부와 관계없이 반드시 실행되기 때문에 사용한 자원을 뒷정리하기에 적합하다.

 

try 블록에서 사용한 리소스를 안정적으로 반납할 수 있다.

(반납하지 않으면 resource leak 발생할 수 있다.)

 


나. try-with-resource

public void useStream() {
    FileInputStream fileInput = null;
    try {
        fileInput = new FileInputStream("abc.txt");
        fileInput.read();
    }catch (IOException e) {
        e.printStackTrace();
    }finally{
        if(fileInput != null){
            try{
                fileInput.close();
            }catch(IOException){
                e.printStackTrace();
            }
        }
    }
}

fileInput.close(); 자체가 IOException을 유발할 수 있어서 try-catch문을 추가해야 하고 fileInputnull인지 확인도 해야 한다.

 

finally 블록이 그 자체로 복잡하다.

 

finally 블록으로 리소스를 반환하는 것이 너무 복잡해서 JDK 1.7 이상부터 리소스를 자동으로 close() 할 수 있다.

 

// try-with-resource
public void useStream(){
    try(FileInputStream fileInput = new FileInputStream("abc.txt")) {
        fileInput.read();
    }catch (IOException e) {
        e.printStackTrace();
    }
}    

fileInput는 자동으로 close()된다.

 

단, AutoCloseable interface를 구현한 객체들만 해당하고, 이 객체들은 try 블록에서 다시 할당될 순 없다.

 


3. throws

 

method 내부에서 발생한 예외를 호출한 곳에서 처리하는 방법이다. (오류 전파)

 

이렇게 하는 이유는 메서드를 호출한 곳에서 에러를 대처하기 위해서다.

 

 

메서드 내부에서 예외 처리가 불가능한 경우나 메서드를 호출하는 개발자에게 에러에 대하여 인지시키기 위해서 메서드 외부로 throw한다.

 

void exception () throws Exception1, Exception2 {
    // 예외 발생
}

void methodCall(){
    try{
        exceptionMethod();
    }catch(Exception e){
        // 예외 처리
    }
}

 


가. 사용법

 

  • Checked exception은 반드시 try-catch문 또는 throws 필요. 적절한 순간 try-catch로 처리한다.
//Checked exception의 Exception Chaining
class Person {
    private int age;
    String name;

    public void setAge(int age) throws NegativeAgeException { // throws
        if(age < 0) {
            throw new NegativeAgeException(); // 예외 발생
        }
        this.age = age;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        Person person = new Person();
        try {
            person.setAge(-30);
        }catch (NegativeAgeException e){ //적절한 곳에서 예외 처리
            System.out.println(e);
        }
    }
}
  • Runtime exception은 throws를 명시적으로 추가하지 않아도 전달된다. 적절한 순간 try-catch로 처리한다.

 

//Unchecked exception의 Exception Chaining
public class ExceptionChaining {
    public static void main(String[] args) {
        OnlineShop shop = new OnlineShop();
        shop.order();
        System.out.println("상품 주문 사용 완료!");
    }
}

class OnlineShop {

    public void order() {
        try {
            packaging();
            delivery();
            System.out.println("상품이 정상적으로 배송 되었습니다.");
        }catch(RuntimeException e) {
            throw new IllegalStateException(e); // 예외 변환
        }
    }

    private void packaging() {
        System.out.println("상품을 포장합니다.");
    }

    private void delivery() {
        deliveryToWareHouse();
        deliveryToCustomer();
    }

    private void deliveryToWareHouse() {
        System.out.println("물류 창고로 배송합니다.");
    }

    private void deliveryToCustomer() {
        System.out.println("고객에게 배송합니다.");
        throw new RuntimeException("도로가 결빙입니다.");
    }

}

 


나. Override 시 주의점

 

throws를 활용하는 메서드를 재정의 할 때는 조상 클래스 메서드가 던지는 예외보다 상위의 예외를 던질 수 없다.

class Parent{
    void methodA() throws IOException{}
    void methodB() throws ClassNotFoundException{}
}

public class OverridingTest extends Parent {
    @Override
    void methodA() throws FileNotFoundException {
        // 가능
    }

    @Override
    void methodB() throws Exception {
        // 불가능
    }
}

 


다. 예외 변환

 

하위 계층에서 발생한 예외를 상위 계층이 던질 수 있는 예외로 바꿔서 던진다. (Spring에서 특히)

public class ExceptionChaining {
    public static void main(String[] args) {
        OnlineShop shop = new OnlineShop();
        shop.order();
        System.out.println("상품 주문 사용 완료!");
    }
}

class OnlineShop {

    public void order() {
        try {
            packaging();
            delivery();
            System.out.println("상품이 정상적으로 배송 되었습니다.");
        }catch(RuntimeException e) {
            throw new IllegalStateException(e); // 예외 변환
        }
    }

    private void packaging() {
        System.out.println("상품을 포장합니다.");
    }

    private void delivery() {
        deliveryToWareHouse();
        deliveryToCustomer();
    }

    private void deliveryToWareHouse() {
        System.out.println("물류 창고로 배송합니다.");
    }

    private void deliveryToCustomer() {
        System.out.println("고객에게 배송합니다.");
        throw new RuntimeException("도로가 결빙입니다.");
    }

}
  • throw new IllegalStateException(e); : RuntimeExceptionIllegalStateException으로 예외 변환.

 


4. 사용자 정의 예외

 

예외도 객체지향적으로 처리하자.

 

보통 Exception 또는 RuntimeException 클래스를 상속받아 작성한다.

 

참고로 Exception은 checked exception이다.

 

  • Exception 상속
    • Checked exception.
    • 명시적 예외 처리 또는 throws를 강제.
    • 코드는 복잡해지지만 처리 누락 등 오류 발생 가능성 감소.
  • RuntimeException 상속
    • Runtime exception.
    • 묵시적 예외 처리 가능.
    • 코드가 간결하지만 예외 처리 누락 가능성 발생.

 

목적에 맞게 어떤 것을 상속할지 잘 선택하자.

 


가. Bad

// bad case (절차식)
public class Person {
    private int age;
    String name;

    public void setAge(int age) {
        if(age < 0) {
            System.out.println("잘못된 입력 : " + age);
            return;
        }
        this.age = age;
    }
}
// bad case (절차식)
public class PersonTest {

    public static void main(String[] args) {
        Person person = new Person();
        person.setAge(-30);
    }
}

예외를 1도 사용하지 않은 코드다.

 

지금 예시는 간단해서 괜찮지만 프로그램이 복잡해질수록 예외를 관리하기 어려워진다.

 


나. Not bad but not good

 

// not bad case (객체지향)
public class NegativeAgeException extends Exception {}
// not bad case
public class Person {
    private int age;
    String name;

    public void setAge(int age) throws NegativeAgeException {
        if(age < 0) {
            //System.out.println("잘못된 입력 : " + age);
            throw new NegativeAgeException();
        }
        this.age = age;
    }
}
// not bad case
public class PersonTest {

    public static void main(String[] args) {
        Person person = new Person();
        try {
            person.setAge(-30);
        }catch (NegativeAgeException e){
            System.out.println(e);
        }
    }
}
/*
com.company.hello.java.NegativeAgeException
*/

필요한 예외는 적절하게 정의하여 사용하도록 하자.

 

발생한 예외에 대한 정보가 부족하다.

 

디버깅하기 쉽도록 예외에 대한 정보를 전달해 보자.

 


다. Better

 

 

Exception (Java Platform SE 8 )

protected Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) Constructs a new exception with the specified detail message, cause, suppression enabled or disabled, and writable stack trace enabled or disabl

docs.oracle.com

Exception 클래스의 두 번째 생성자를 사용하면 예외에 대한 정보를 메시지로 전달할 수 있다.

 

// good case
package com.company.hello.java;

public class NegativeAgeException extends Exception {
    public NegativeAgeException(String msg) {
        super(msg);
    }
}
  • super(msg); : Exception 클래스의 두번째 생성자 호출.

 

// good case
package com.company.hello.java;

public class Person {
    private int age;
    String name;

    public void setAge(int age) throws NegativeAgeException {
        if(age < 0) {
            //System.out.println("잘못된 입력 : " + age);
            throw new NegativeAgeException("잘못 입력 : " + age);
        }
        this.age = age;
    }
}
  • throw new NegativeAgeException("잘못 입력 : " + age);
    : 발생한 오류에 관한 정보를 전달한다.

 

// good case
package com.company.hello.java;

public class PersonTest {

    public static void main(String[] args) {
        Person person = new Person();
        try {
            person.setAge(-30);
        }catch (NegativeAgeException e){
            System.out.println(e);
            e.printStackTrace();
        }
    }
}
/*
com.company.hello.java.NegativeAgeException: 잘못 입력 : -30
com.company.hello.java.NegativeAgeException: 잘못 입력 : -30
    at com.company.hello.java.Person.setAge(Person.java:10)
    at com.company.hello.java.PersonTest.main(PersonTest.java:8)
*/
  • System.out.println(e);
    : Exception.toString()에서 메시지도 출력하도록 재정의 되어있음. 그래서 우리가 전달한 오류에 관한 정보도 출력됨.

 


다. Why? 굳이? 힘들게?

 

  • 예외에 대한 추가적인 정보를 전달할 수 있다. 이는 디버깅이나 로깅에 유용하다.
  • 동일한 상황에서 예외 객체를 재사용할 수 있다.
  • 예외 전파가 가능하다.
  • 예외의 종류를 명확하게 나타낼 수 있다. 나중에 코드를 읽고 어떤 예외가 발생할 수 있는지 이해하기 편하다.
  • 코드를 유지보수할 때 예외 처리를 더욱 효율적으로 할 수 있다.

 


5. 예외를 다루는 개발자의 마음가짐?

 

사용할 때도 만들 때도 모든 예외는 적절하게 처리한다는 마음을 가지자.

 

만들 때는 사소하면 내부에서 크며 외부에서 예외를 처리하도록 한다.

 

가져올 때는 어떤 예외가 발생할 수 있는지 확인하고 모든 예외를 고려해서 프로그래밍하자.

public void m1(String[] args, String name, int divisor) {
    try{
        System.out.println(args[0]);
        System.out.println(name.length());
        System.out.println(1/divisor);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

그렇다고 모든 예외를 try-catch하는 것은 무책임하다.

 

public void m2(String[] args, String name, int divisor) {
    if(args != null && args.length > 1){
        System.out.println(args[0]);
    }
    if(name != null){
        System.out.println(name.length());
    }
    if(divisor != 0){
        System.out.println(1/divisor);
    }
}

간단한 조건문으로 처리하는 것도 고려해 보자.

 

많은 경우 RuntimeException(Unchecked exception)try-catch를 통한 예외 처리 목적보다는 디버깅의 목적이 강하다.

 


'공부 중 > Java' 카테고리의 다른 글

[Java] 람다식  (0) 2024.01.28
[Java] Collection  (0) 2024.01.28
[Java] String  (0) 2024.01.22
[Java] 인터페이스  (0) 2024.01.22
[Java] 다형성  (0) 2024.01.22