예외 처리

황희윤·2023년 11월 10일

예외

프로그램 실행 중 예기치 않은 문제가 발생한 상태(수습 가능)

예외 VS 오류

  • 실행 오류 : 시스템이 정상적인 기능을 수행할 수 없는 상태(수습 불가)
  • 컴파일 오류 : 문법(syntax)이 올바르지 않은 상태

예외 처리 구문

try / catch 문

  • try 블록에서 발생한 예외catch 블록에서 처리

  • try 블록에는 예외가 발생할 가능성이 있는 명령들을 넣는다. 그리고 어떤 명령문에서 예외가 발생하면 나머지 명령들은 실행되지 않고 곧장 catch 블록이 실행된다.

  • 실행 중일 때 예외가 발생하면 JVM은 즉각 실행을 중단하고 예외 객체(exception object)를 생성한다.

finally 블록

  • 예외의 발생 여부와 상관없이 무조건 실행된다.

  • 주로 try catch 문에 사용한자원을 해제할 때 사용한다.

try {
	fis = new FileInputStream(path);
    System.out.println("파일 열기 성공");
} catch(FileNotFoundException e) {
	System.out.println("파일 열기 실패");
    throw new MyException(e); // 예외 생성
}
fis.close(); // 명령문이 실행이 안될수도 있다.

finally가 없는 위의 코드에서 catch 블록은 예외를 throw하고 있다. 그럼 예외를 던지는 코드 때문에 자원이 반납이 이루어지지 않을수도 있다.

try {
	fis = new FileInputStream(path);
    System.out.println("파일 열기 성공");
} catch(FileNotFoundException e) {
	System.out.println("파일 열기 실패");
    throw new MyException(e); // 예외 생성
} finally {
	fis.close(); // 반드시 실행된다.
}

try-with-resources 문

위의 코드는 자바 1.7이전에 주로 사용되었던 방법이고 1.7이후에는 try 뒤에 바로 등장하는 괄호 안에 자원을 선언해서 관리한다.

private static void fileStreamControl(File source) {
	try(InputStream fis = new FileInputStream(source)){
    	byte[] buf = new byte[8192];
        
        int i;
        while((i = fis.read(buf)) != -1) {
        	fis.write(buf, 0, i);
        }
    } catch(Exception e) {
    	e.printStackTrae();
    }
}

예외 클래스

  • 자바에서는 실행 오류를 모두 클래스로 정의한다.
  • 모든 오류는 Throwable 클래스로부터 상속받아 Error 클래스와 Exception 클래스로 정의한다.

Error 클래스

  • Error 클래스 오류들은 자원 고갈이나 JVM 내부의 오류와 같이 프로그램에서 처리하지 못하는 치명적인 오류다.

  • 다행히 이런 오류가 발생할 확률은 낮다.

  • 이런 치명적 오류는 미리 대처할 수 없기에 예외 처리의 대상이 되지 않는다.

  • ex) OutOfMemoryError

Exception 클래스

  • 검증된 예외와 검증되지 않은 예외로 나눈다.

검증되지 않은 예외(unchecked)

  • 컴파일러가 검증하지 못하는 예외

  • 실행 예외(runtime exception), NullPointerException, IndexOutOfBoundsException 클래스 등

  • 개발자가 부주의해서 발생하는 경우에 대해 예외가 만들어진 것이기 때문에 굳이 예외 처리(catch나 throws)를 하지 않아도 된다.

  • null 관련 오류는 try catch로 하는 것이 아니라 if문으로 if( != null)과 같은 형식으로 처리한다.

  • ClassCastException : 형변환 오류

  • IllegalArgumentException : 메서드가 잘못되었거나 부적합한 인수가 전달되면 발생

검증된 예외(checked)

  • 검증되지 않은 예외를 제외한 나머지 예외

  • 사용자가 예외를 처리해야 컴파일이 되기 때문에 사용자 정의 예외라고 한다.

  • 컴파일러가 검증하는 예외


예외 처리 방법

예외 복구

  • try catch문을 활용해 예외 상황을 파악해서 예외를 잡고 정상 상태로 돌려놓는다

예외 처리 회피

  • 예외 처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져 버리는 방법

  • 다른 객체가 책임을 분명하게 지거나 자신을 호출하는(사용하는)쪽에서 예외를 다루는게 좋겠다고 판단할 때 사용한다.

  • 회피하는 방법은 throws문과 catch문이 있다.

throws

public void readFile() throws IOException {
	FileInputStream fis = new FileInputStream(path);
    ...
}

catch

catch 문으로 일단 예외를 잡고 로그를 남긴 후 다시 예외를 던진다.

public void readFile() throws IOException {
	try {
    	FileInputStream fis = new FileInputStream(path);
        ...
    } catch(IOException e) {
    	e.printStackTrace();
        throw e;
    }
}

throw vs throws

  • throw : 예외 객체를 생성해서 강제로 던진다.
String numStr = "11a";
try {
    int num = Integer.parseInt(numStr);
} catch (NumberFormatException e) {
    // catch exception
    throw new IllegalArgumentException("This string is not a number format");
}
  • throws : 메서드를 실행할 때 발생할 수 있는 잠재적인 Exception을 표시
public void changeToInt(int num) throws NumberFormatException {
    String numStr = "11a";
    Integer.parseInt(numStr);
}

올바른 예외 처리 예시(개발자 정의)

class MyException extends Exception {
	public MyException(String mg){
    	super(msg);
    }
};

class MyScore {
	private int score;
    public void setScore(int s) throws MyException {
    	if(s >= 0)
        	this.score = s;
        else
        	throw new MyException("음수가 될 수 없습니다"
    }
};

public class Ex12_14 {
	public static void main(String[] args){
    	MyScore obj = new MyScore();
        try {
        	obj.setScore(-10);
        } catch (MyException e) {
        	System.out.println(e.getMessage());
        }
    }
};
  • 단순히 null 리턴을 하는게 아니라 Exception에 에러에 대한 내용을 담아서 예외 객체를 반환한다.

  • 주석도 충분하지 않다.

  • 예시) user가 null인 이유를 IllegalStateException 예외 인스턴스에 상세하게 적었다.

public class UserRepository {
    public User findByName(String name) {
        User result = db.getUserBy(name);
        if (result == null) {
            throw new IllegalStateException(
                    "인사관리 시스템과 동기화 되지 않은 유저의 이름을 입력한 경우 이 메시지를 볼 수 있습니다.\n" +
                            "매주 월->화 넘어가는 자정에 인사 관리 시스템과의 데이터 동기화가 수행되므로, 새로운 사람이 월요일이 아닌 다른 날짜에 입사하지 않았는지 확인하십시오.\n" +
                            "다음 주 월요일까지 기다리거나, 수동 동기화를 실행하면 문제가 해결될 수 있습니다.\n" +
                            "인사 관리 시스템과의 데이터 동기화 로직은 UserRepositorySync 클래스를 참고하십시오.\n" +
                            "문제가 된 name=[" + name + "]"
            );
        }
        return result;
    }
}

잘못된 예외 처리 방법

1. null, -1, 빈 문자열 등 특수값을 예외로 사용

  • 특수값을 예외로 사용하면 호출자는 늘 항상 호출값을 확인해야 하고, 특수값이 의미하는 바가 무엇인지 알아야 한다.

  • 특수값 대신 예외를 사용하면 어떤 문제로 발생한 예외인지 알 수 있고, 해당 문제의 상세 메세지를 포함시킬 수 있고, 어떤 경로로 이 문제가 발생한 것인지 확인할 수 있는 Stack Trace를 알 수 있다.

2. 단순 문자열을 throw

  • 어떤 경로로 이 문제가 발생한 것인지 확인할 수 있는 Stack Trace를 알 수 없다.

  • 예외는 항상 예외 객체를 반환해야 한다.

3. Exception에 상세한 내용 적지 않기

// bad
throw new IllegalArgumentException("사용자의 입력이 잘못되었다.");


// good
throw new IllegalArgumentException("사용자 " + userId + "의 입력(" + inputData + ")가 잘못되었다.");

4. Exception들을 분류하지 않기

// bad
class DuplicatedException extends Error {}

class UserAlreadyRegisteredException extends Error {}

// good
class ValidationException extends Error {}

class DuplicatedException extends ValidationException {}

class UserAlreadyRegisteredException extends ValidationException {}

예외 계층 구조를 만들어서 알맞게 분류한다.

5. 외부 라이브러리에서 발생하는 예외를 분류하지 않고 한꺼번에 처리

// bad : 어디서 어떤 문제가 발생했고, 그에 따른 해결방법을 포함할 수 없다.
public static void order() {
	Pay pay = new Pay();
    Database db = new Database();
    
    try{
    	pay.billing(); // 외부 결제 라이브러리
        db.save(pay); // 우리가 관리하는 데이터베이스 관련 코드
    } catch (Exception e) {
    	Logger logger = Logger.getLogger(getClass().getName());
        logger.error("pay fail", e);
    }
}

// bad : 외부 라이브러리에서 발생하는 예외를 우리가 사용하는 코드에서 예외 처리하는 방식으로 똑같이 처리하면 안된다.
public static void order() {
    Pay pay = new Pay();
    Database database = new Database();
    
    try {
        pay.billing();
        database.save(pay);
    } catch (PayNetworkException e) {
        // PayNetworkException 처리
        // ...
    } catch (EmptyMoneyException e) {
        // EmptyMoneyException 처리
        // ...
    } catch (PayServerException e) {
        // PayServerException 처리
        // ...
    } catch (Exception e) {
        // 기타 예외 처리
        // ...
    }
}

// good : 외부 라이브러리 코드 예외 처리와 우리가 만든 코드의 예외 처리를 분리하기
public static void order() {
    Pay pay = new Pay();
    pay.billing();

    try {
        Database database = new Database();
        database.save(pay);
    } catch (Exception e) {
        pay.cancel();
    }
}

public static void billing() throws BillingException {
    try {
        pay.billing();
    } catch (PayNetworkException e) {
        // 처리할 내용
    } catch (EmptyMoneyException e) {
        // 처리할 내용
    } catch (PayServerException e) {
        // 처리할 내용
    } catch (Exception e) {
        // 다른 예외들을 처리하거나 예외를 다시 던질 수 있음
        throw new BillingException(e.getMessage(), e);
    }
    // 다른 작업 수행
}

6. 무분별한 예외 처리하기

  • 데이터가 없으면 예외(NoDataException)을 반환하지 않고 Null을 반환한다.

  • 그리고 호출한 코드에서 Null Check (if(data != null))를 한다.

profile
HeeYun's programming study

0개의 댓글