예외 처리 (1)
예외처리가 필요한 이유
예제 프로그램 내용 : 사용자의 입력을 받고, 입력 받은 문자를 외부 서버에 전송하는 프로그램
- 프로그램의 구성
(Main) -- sendMessage(data) --> (NetworkService) -- connect,send,disconnect --> (NetworkClient)
package exception.ex1;
public class NetworkServiceV1_3 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV1 client = new NetworkClientV1(address);
client.initError(data); // 추가
String connectResult = client.connect();
if (isError(connectResult)) {
System.out.println("[네트워크 오류 발생] error code = " + connectResult);
} else {
String sendResult = client.send(data);
if (isError(sendResult)) {
System.out.println("[네트워크 오류 발생] error code = " + sendResult);
}
}
client.disconnect();
}
private static boolean isError(String connectResult) {
return !connectResult.equals("success");
}
}
(이전에 여러 단계가 있지만 생략했다. 최종 요구사항은 에러 발생 시 연결 해제까지 완료하는 로직을 구현하는 것)
위 코드를 보면, 정상흐름 / 예외흐름이 섞여있다.
package exception.ex1;
public class NetworkServiceV1_1 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV1 client = new NetworkClientV1(address);
client.initError(data); // 추가
client.connect();
client.send(data);
client.disconnect();
}
}
(정상 흐름 코드)
두 코드를 비교해보면, 정상 흐름 코드 -> 예외 흐름 코드 순으로 섞여있기 때문에 코드의 흐름이 읽혀지지 않는다.
그리고 정상 흐름 코드보다 예외흐름 코드가 더 많다.
쉽게 말해서 코드가 한눈에 들어오지 않는다.
이런 문제를 해결하는 예외처리 메커니즘이 존재한다.
자바의 예외처리는 다음과 같은 키워드를 사용한다.
try, catch, finally, throw, throws
Object: 모든 객체의 최상위 부모, 예외의 최상위 부모도 Object이다.
Throwable: 최상위 예외
Error: 메모리 부족이나 심각한 시스템 오류와 같은 복구가 불가능한 시스템 예외
Exception: 체크예외, 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외. (RuntimeException은 제외)
RuntimeException: 런타임 예외, 컴파일러가 체크하지 않는 언체크 예외
예외 기본 규칙
예외의 2가지 기본 규칙
1. 예외는 잡아서 처리하거나 밖으로 던져야 한다.
2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리할 수 있다.
(Exception을 catch로 잡으면 그 하위 예외들도 모두 잡을 수 있다. throws로 던져도 하위 예외를 모두 던질 수 있다.)
체크예외
package exception.basic.checked;
// Exception 을 상속받은 예외는 체크예외가 된다.
public class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
예외 클래스를 생성하려면 Exception을 상속받으면 된다. Exception을 상속받은 예외는 체크예외가 된다.
package exception.basic.checked;
public class Client {
public void call() throws MyCheckedException{
//문제 상황 발생
throw new MyCheckedException("ex");
}
}
클라이언트에서 예외를 발생시켰다. throw 키워드로 예외를 발생시켰는데, 예외도 객체이기 때문에 new로 생성하고 발생시켜야 한다.
throw
- 메서드 내부에서 예외를 직접 발생시킬 때 사용하는 키워드
- 메서드 실행 중 필요한 시점에 사용
- throw 뒤에 예외 객체를 지정 (new로 생성)
- try-catch로 처리 가능
throws
- 메서드가 특정 예외를 던질 때 사용하는 키워드
- 메서드 선언부에 사용됨, 체크 예외를 호출자에게 전달하기 위해 사용
- throws 뒤에 예외 클래스 이름을 지정
- 호출자가 예외를 처리해야함
package exception.basic.checked;
public class Service {
Client client = new Client();
//예외를 잡아서 처리하는 코드
public void callCatch() {
try {
client.call();
} catch (MyCheckedException e) {
//예외 처리 로직
System.out.println("예외 처리, message = " + e.getMessage());
}
System.out.println("정상 흐름");
}
//체크 예외 던지는 코드, 던지려면 throws 메서드 필수 사용
public void callThrow() throws MyCheckedException{
client.call();
}
}
package exception.basic.checked;
public class CheckedCatchMain {
public static void main(String[] args) {
Service service = new Service();
service.callCatch();
System.out.println("정상 종료");
}
}
main -> service.callCatch() -> client.call() [예외 발생, 던졌기 때문에 호출자가 처리해야함]
-> client.call() [try-catch로 예외처리 완료] -> service.callCatch() [정상 흐름]
위 코드는 체크 예외를 잡아서 처리하는 내용이다.
package exception.basic.checked;
public class CheckedThrowMain {
public static void main(String[] args) throws MyCheckedException {
Service service = new Service();
service.callThrow();
System.out.println("정상 종료");
}
}
예외를 던지는 코드
main -> service.callThrow() -> client.call [예외발생, 던졌기 때문에 호출자가 처리해야함]
-> service.callThrow() [예외 던짐] -> main [예외 던짐, 프로그램 종료]
체크 예외의 장점은 실수로 예외를 처리하지 않아도 컴파일러가 문제를 잡아주기 때문에 디버깅이 쉽다.
단점은 모든 체크예외를 잡아야 하기 때문에 번거롭다.
언체크 예외
RuntimeException과 그 하위 예외는 언체크 예외로 분류된다.
언체크 예외는 체크 예외와 기본적으로 동일하다. 차이가 있따면 예외를 던지는 throws를 생략하냐 마냐 차이다.
package exception.basic.unchecked;
// RuntimeException 을 상속받은 예외는 언체크 예외
public class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) {
super(message);
}
}
RuntimeException을 상속받은 예외는 언체크 예외가 된다.
package exception.basic.unchecked;
public class Client {
// 예외 발생
public void call() {
throw new MyUncheckedException("ex");
}
}
체크 예외와 다른점은 throws 키워드를 생략할 수 있다. (체크 예외는 throws Exception 키워드를 사용함)
package exception.basic.unchecked;
// UnChecked 예외는 예외를 잡거나 던지지 않아도 된다. 예외를 잡지 않으면 자동으로 밖으로 던진다.
public class Service {
Client client = new Client();
public void callCatch() {
try {
client.call();
} catch (MyUncheckedException e) {
System.out.println("예외 처리, message = " + e.getMessage());
}
System.out.println("정상 로직");
}
// 예외를 잡지 않아도 자연스럽게 상위로 넘어간다.
public void callThrow() {
client.call();
}
}
필요한 경우 try-catch로 잡아서 처리하면 된다.
callThrow() 메서드엔 체크 예외와 다르게 throws 키워드를 사용하지 않았다. 자동으로 생략되기 때문이다.
언체크 예외는 컴파일러가 체크하지 않기 때문에 언체크 예외인 것이다.
throws를 키워드를 생략하냐 명시적으로 표현을 해주냐 차이다.
정말 중요한 예외라면 명시적으로 throws 키워드를 사용해도 된다. 하지만 생략을 해도 문제가 없다. (선택의 문제)
언체크 예외는 throws를 생략하므로써 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 그러나 이로인해 실수로 예외를
누락시킬 수도 있다. 컴파일러가 체크를 하지 않기 때문이다.
체크 예외와 언체크 예외를 두가지로 나눠서 생각하지 말고, 같지만 예외를 밖으로 던지는 부분에서 throws를 선언하는가 생략하는가의
차이라고 생각하면 된다.