프로그램 실행 중 예기치 않은 문제가 발생한 상태(수습 가능)
try 블록에서 발생한 예외를 catch 블록에서 처리
try 블록에는 예외가 발생할 가능성이 있는 명령들을 넣는다. 그리고 어떤 명령문에서 예외가 발생하면 나머지 명령들은 실행되지 않고 곧장 catch 블록이 실행된다.
실행 중일 때 예외가 발생하면 JVM은 즉각 실행을 중단하고 예외 객체(exception object)를 생성한다.
예외의 발생 여부와 상관없이 무조건 실행된다.
주로 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(); // 반드시 실행된다.
}
위의 코드는 자바 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();
}
}
Error 클래스 오류들은 자원 고갈이나 JVM 내부의 오류와 같이 프로그램에서 처리하지 못하는 치명적인 오류다.
다행히 이런 오류가 발생할 확률은 낮다.
이런 치명적 오류는 미리 대처할 수 없기에 예외 처리의 대상이 되지 않는다.
ex) OutOfMemoryError
컴파일러가 검증하지 못하는 예외
실행 예외(runtime exception), NullPointerException, IndexOutOfBoundsException 클래스 등
개발자가 부주의해서 발생하는 경우에 대해 예외가 만들어진 것이기 때문에 굳이 예외 처리(catch나 throws)를 하지 않아도 된다.
null 관련 오류는 try catch로 하는 것이 아니라 if문으로 if( != null)과 같은 형식으로 처리한다.
ClassCastException : 형변환 오류
IllegalArgumentException : 메서드가 잘못되었거나 부적합한 인수가 전달되면 발생
검증되지 않은 예외를 제외한 나머지 예외
사용자가 예외를 처리해야 컴파일이 되기 때문에 사용자 정의 예외라고 한다.
컴파일러가 검증하는 예외
예외 처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져 버리는 방법
다른 객체가 책임을 분명하게 지거나 자신을 호출하는(사용하는)쪽에서 예외를 다루는게 좋겠다고 판단할 때 사용한다.
회피하는 방법은 throws문과 catch문이 있다.
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream(path);
...
}
catch 문으로 일단 예외를 잡고 로그를 남긴 후 다시 예외를 던진다.
public void readFile() throws IOException {
try {
FileInputStream fis = new FileInputStream(path);
...
} catch(IOException e) {
e.printStackTrace();
throw e;
}
}
String numStr = "11a";
try {
int num = Integer.parseInt(numStr);
} catch (NumberFormatException e) {
// catch exception
throw new IllegalArgumentException("This string is not a number format");
}
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;
}
}
특수값을 예외로 사용하면 호출자는 늘 항상 호출값을 확인해야 하고, 특수값이 의미하는 바가 무엇인지 알아야 한다.
특수값 대신 예외를 사용하면 어떤 문제로 발생한 예외인지 알 수 있고, 해당 문제의 상세 메세지를 포함시킬 수 있고, 어떤 경로로 이 문제가 발생한 것인지 확인할 수 있는 Stack Trace를 알 수 있다.
어떤 경로로 이 문제가 발생한 것인지 확인할 수 있는 Stack Trace를 알 수 없다.
예외는 항상 예외 객체를 반환해야 한다.
// bad
throw new IllegalArgumentException("사용자의 입력이 잘못되었다.");
// good
throw new IllegalArgumentException("사용자 " + userId + "의 입력(" + inputData + ")가 잘못되었다.");
// bad
class DuplicatedException extends Error {}
class UserAlreadyRegisteredException extends Error {}
// good
class ValidationException extends Error {}
class DuplicatedException extends ValidationException {}
class UserAlreadyRegisteredException extends ValidationException {}
예외 계층 구조를 만들어서 알맞게 분류한다.
// 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);
}
// 다른 작업 수행
}
데이터가 없으면 예외(NoDataException)을 반환하지 않고 Null을 반환한다.
그리고 호출한 코드에서 Null Check (if(data != null))를 한다.