- 예외 : 단순히 오류를 표시하는 것.
- 예외 처리 : 개발자가 프로그램을 다시 정상적으로 돌리기 위해 대응 로직을 작성하는 것이다.

NetworkClient : 외부 서버와 연결하고, 데이터를 전송하고, 연결을 종료하는 기능이 있다.
NetworkService : NetworkClient 를 다뤄서 데이터를 전송하며 흐름을 제어한다.
package exception.ex1;
public class NetworkClientV1 {
private final String address;
public boolean connectError; // 서버 연결 실패
public boolean sendError; // 데이터 전송 실패
public NetworkClientV1(String address) {
this.address = address;
}
public String connect() {
if (connectError) {
System.out.println(address + " 서버 연결 실패");
return "connectError";
}
System.out.println(address + " 서버 연결 성공");
return "success";
}
public String send(String data) { // 문자열로 오류 필드에 참/거짓을 저장하고 반환한다.
if (sendError) {
System.out.println(address + " 서버에 데이터 전송 실패");
return "sendError";
}
System.out.println(address + " 서버에 데이터 전송 : " + data);
return "success";
}
public void disconnect() {
System.out.println(address+" 서버 연결 해제");
}
public void initError(String data) {
if (data.contains("error1")) {
connectError = true; // boolean 타입 멤버변수 값은 return을 하는게 아니라 멤버변수에 T/F 을 넣는 거구나.
// 이 필드를 메인에서 출력하면 있는지 없는지 알 수 있네
}
if (data.contains("error2")) {
sendError = true;
}
}
}
➡️ Client 로 서버 연결, 데이터 전송, 서버 연결 해제의 기능을 가지고 있어야 한다.
initError(String data) : main() 으로부터 받은 data 에 에러의 흔적이 있는지 판별한다. (boolean 형태)➡️ 연결에러(connectError), 데이터 전송에러(sendError) 를 필드로 가지고 있다. 이 필드에 boolean 값을 넣는 방식으로 에러가 맞는지/아닌지 를 표시한다.
< 흐름 제어 역할 클래스 >
package exception.ex1;
public class NetworkServiceV1_3 {
public void sendMessage(String data) {
NetworkClientV1 client = new NetworkClientV1("http://example.com");
client.initError(data);
String connectResult = client.connect(); // 반환값을 받아서
if (isError(connectResult)) { // 메서드 결과가 true 면 밑의 코드 실행 아니면 종료
System.out.println("[네트워크 오류 발생] 오류 코드 : " + connectResult);
} else {
String sendResult = client.send(data);
if (isError(sendResult)) {
System.out.println("[네트워크 오류 발생] 오류 코드 : " + sendResult);
}
}
client.disconnect(); // return 을 제거해 앞의 결과에 상관 없이 무조건 호출된다.
}
private static boolean isError(String resultCode) {
return !resultCode.equals("success"); // 성공이 아닐 때 -> 오류일때! true 가 된다
}
}
initError() 에 전달한다. isError() 메서드로 initError() 로 채워둔 오류유무를 확인해 오류면 오류코드를 출력하고, 성공이면 send() 메서드로 데이터를 전송한다.<Main 클래스>
package exception.ex1;
import java.util.Scanner;
public class MainV1 {
public static void main(String[] args) {
NetworkServiceV1_3 networkService = new NetworkServiceV1_3();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("전송할 문자 : ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
networkService.sendMessage(input); // 입력받은 데이터를 흐름제어 객체로 전송
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
}
반환값으로 처리하는 예외는 단점이 존재한다.
자바는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황, 즉 예외(Exception)를 처리하기 위한 메커니즘을 제공한다. 그리고 예외를 다루기 위한 예외 처리용 객체들을 제공한다.

그림에서 보이듯이 예외 계층에도 최상위 부모는 Object이다. 예외 중에서는 Throwable 이 최상위 예외이다.
예외는 체크예외와 언체크예외 그리고 에러가 있다. 여기서 에러는 시스템 상에서 발생해 개발자가 해결할 수 없기 때문에 처리하려고 하면 안된다. 체크예외는 컴파일러가 확인하는 예외를 말한다. Exception을 상속받으면 다 체크예외이다. 언체크예외 혹은 런타임예외는 컴파일러가 확인하지 않는 예외를 말한다.
📌 체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
📌 예외 처리에도 상속 관계처럼 부모와 자식이 연결된다. 상위 예외를 잡으면 그 하위 예외까지 잡게된다.
(Throwable 예외는 잡으면 안됨. 건들면 안되는 Error도 잡아버리기 때문)
package exception.basic.checked; // **예외 클래스‼️**
public class MyCheckedException extends Exception { //Exception 을 상속 받은 예외는 체크예외가 된다.
public MyCheckedException(String message) {
super(message); // -> 이건 생성자 호출이란다*^^*
}
}
➡️ 부모 클래스인 Exception의 생성자를 호출해 받은 메세지를 예외 객체 내부에 저장한다.
➡️ super(message) 로 전달한 메세지는 Throwable 에 있는 detailMessage 에 보관된다.
💡 이런 예외 타입 클래스는 항상 개발자가 정의해야 하는 것은 아니다.
1. 기본적인 예외 (파일 없음, 메모리 부족 등)
2. 사용자 정의 체크 예외 (개발자 입력이 필요한 특수한 문제)
package exception.basic.checked; // 예외가 발생하게 만들어둔 클래스
public class Client {
public void call() throws MyCheckedException {
throw new MyCheckedException("ex"); // 예외도 객체이기 때문에 new 로 생성을 하고 예외를 발생시켜야 한다.
}
}
➡️ throw new MyCheckedException("ex") 으로 바로 위의 예외 클래스를 기반으로 예외가 생성되고 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("정상 흐름");
}
}
➡️ try-catch : try에 예외가 발생할 수도 있는 코드를 넣어둔다. 그리고 이 예외에 대한 대응 코드를 catch 블럭 안에 넣어둔다.
🚨 try-catch 와 throws는 같이 쓰는 건가?
두 경우가 있다. 보통 잡으면 마무리로 처리되고 다시 정상코드를 실행한다. 하지만, catch 블록 안에 무조건 예외를 다 처리하는 코드가 들어있지 않을 수도 있다. 이때는 일부를 처리하고 밖으로 예외를 또 던지는데 이때는 뒤에 throw 객체, throws, try-catch 가 다 있어야 한다.
package exception.basic.checked;
public class CheckedCatchMain {
public static void main(String[] args) {
Service service = new Service();
service.callCatch(); // 예외를 처리했기 때문에 main() 메서드까지 예외가 올라오지 않는다.
// 그래서 main() 에서 출력하는 "정상 종료" 문구나 출력된 것을 확인할 수 있다.
System.out.println("정상 종료");
}
}

package exception.basic.checked;
public class Service {
Client client = new Client();
// 체크 예외를 밖으로 던지는 코드
public void callThrow() throws MyCheckedException {
client.call();
}
}
➡️ call() 메서드에서 예외가 발생해서 던져지면 callThrow() 에서 바로 밖으로 던진다.
(여기는 붙잡아서 처리하는 코드가 없다.)
package exception.basic.checked;
public class CheckThrowMain {
public static void main(String[] args) throws MyCheckedException {
Service service = new Service();
service.callThrow(); // main() 에서도 예외를 처리 못하면 콘솔에 스택 트레이스(빨간 글씨)가 출력되면서 프로그램이 종료된다.
System.out.println("정상 종료");
}
}
컴파일 시점 확인: 이 예외를 던지는(throw) 메서드는 반드시 메서드 시그니처에 throws MyCheckedException과 같이 예외를 선언해야 한다.
의무적 처리: 이 예외를 호출하는 코드(메서드)는 반드시 try-catch 블록을 사용하여 예외를 처리하거나, 다시 throws로 던지도록 선언해야 한다. 이를 지키지 않으면 컴파일 오류가 발생한다.
⚠️ try-catch로 잡았는데 컴파일 오류가 나는 경우
말 그대로 컴파일러가 예외를 체크하지 않는다. 기본적으로 체크 예외와 동일하나 예외를 던지는 throws를 선언하지 않고, 생략할 수 있다. 생략한 경우 자동으로 예외를 던진다.
예외를 잡아서 처리 안해도 throws 를 생략할 수 있다.
package exception.basic.unchecked; // 언체크 예외 클래스
public class MyUncheckedException extends RuntimeException {
public MyUncheckedException(String message) { // 생성자
super(message);
}
}
➡️ 마찬가지로 예외가 발생하면 예외 메세지가 Throwable 에 저장된다.
package exception.basic.unchecked;
public class Client { // 언체크 예외 발생
public void call() {
throw new MyUncheckedException("ex");
}
}
package exception.basic.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();
}
}
➡️ 발생한 예외를 catch로 잡고 정상로직으로 돌아간다.
예외를 잡아도 되고, 예외를 잡지 않아도 된다‼️ (체크 예외, 언체크 예외 공통)
하지만❗️
예외를 잡지 않는 경우는 반드시 던져야 한다. 이때 표시인 throw를 넣어야하는 체크 예외와 달리 언체크 예외는 던지라는 표시인 throws 를 입력해두지 않아도 자동으로 던져진다.
언체크 예외는 throws 를 생략해도 되지만 대부분 표시해서 개발자들이 쉽게 식별할 수 있게 한다.
package exception.basic.unchecked;
public class UncheckedCatchMain {
public static void main(String[] args) {
Service service = new Service();
service.callCatch();
System.out.println("정상 종료");
}
}
📌 현대 개발에서는 사실 체크 예외를 거의 사용하지 않는다.
장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다.
➡️ 잡아서 복구하기보다 프로그램이 비정상적으로 종료되게 두고 나중에 디버깅을 할 수 있다는 선택지가 주어진다. (복구 가능한 것만 처리)
단점 : 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다.