예외 처리 1에서는 반환값으로 예외를 처리했다.
➡️ Client 클래스의 필드에 오류검사 메서드의 결과를 필드에 boolean으로 담아두고 검사해 문자열을 반환한다. 또 문자열을 비교해서 예외 처리를 한다. (불필요한 검사 코드가 많음, 예외는 입력받은 문자열을 가지고 처리)
하지만 이 방법은 개발자가 다룰 수많은 코드들에서 정상로직과 예외로직을 쉽게 구분하기 어렵다. 잘보여야 할 정상로직이 제대로 안보이고 예외 로직은 실무에서 훨씬 많아져 유지보수가 어렵다.

모든 체크 예외는 Exception 의 자식 클래스다. 그 상위 클래스인 Throwable 에서 예외 메세지를 저장해둔다.
package exception.ex2;
public class NetworkClientExceptionV2 extends Exception { // 체크 예외
private String errorCode;
public NetworkClientExceptionV2(String errorCode, String message) {
super(message); // 오류 메세지를 Throwable 에 넘김
this.errorCode = errorCode; // 에러코드를 예외 객체에 저장
}
public String getErrorCode() {
return errorCode;
}
}
➡️ NetworkClient 클래스에서 생길 수 있는 체크 예외를 다루는 클래스다. 필드에는 에러코드를 가지게 하고, 오류 메세지는 생성자로 받아서 상위 클래스인 Throwable 에 넘긴다.
➡️ extends Exception 로 상속 관계를 알 수 있다.
package exception.ex2;
//import javax.management.RuntimeErrorException;
public class NetworkClientV2 {
private final String address;
public boolean connectError; // 서버 연결 실패
public boolean sendError; // 데이터 전송 실패
public NetworkClientV2(String address) {
this.address = address;
}
public ‼️void connect() throws NetworkClientExceptionV2 { // initError() 에서 확인한 결과를 가지고 연결 성공인지 예외가 발생하는지 체크
if (connectError) {
throw new NetworkClientExceptionV2("connectError", address + " 서버 연결 실패");
}
System.out.println(address+" 서버 연결 성공");
}
public ‼️void send(String data) throws NetworkClientExceptionV2 { // 각각 기능마다 발생할 수 있는 예외에 대한 객체가 있음
if (sendError) {
throw new NetworkClientExceptionV2("sendError", address + " 서버에 데이터 전송 실패 : " + data);
// 예상치 못한 전혀 다른 예외가 발생했을 경우
//throw new RuntimeException("ex");
}
System.out.println(address + " 서버에 데이터 전송 : " + data);
}
public ‼️void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
public void initError(String data) { // 예외가 발생할지 확인 후 Client 객체의 필드에 저장해둠
if (data.contains("error1")) {
connectError = true;
}
if (data.contains("error2")) {
sendError = true;
}
}
}
(💡 필드는 그냥 객체의 요소라고 생각하면 될듯 )
➡️ 주소와 서버연결실패, 데이터전송실패 여부를 요소로 가지고 있음
➡️ 실패는 initError(data) 메서드로 검사해서 boolean 형태의 필드에 참이면 true 를 대입하고 아니면 그냥 둔다. (기본값이 false)
오류가 발생했을 때 오류코드를 반환하는게 아니라 예외를 던진다‼️ 그래서 반환값 없이 void 로 설정
- Before : initError() -> 필드에 결과 저장 -> 필드를 가지고 정상 코드와 예외 코드 둘 중 하나를 실행 -> 오류코드 반환
➡️ 반환값으로 성공, 실패여부 확인
- Now : initError() -> 필드에 결과 저장 -> 필드로 정상 코드와 예외 생성 코드 둘중 하나를 실행
➡️ 예외 처리를 설정해 메서드가 정상 종료되면 성공이고, 예외가 던져지면 예외를 통해 실패를 확인
➡️ 서버 연결과 데이터 연결 메서드 모두 throws NetworkClientExceptionV2 를 선언했다. 즉 이 메서드들에서 예외가 발생하면 밖으로 던진다는 뜻이다. try-catch 로 잡아서 처리하지 않고.
package exception.ex2;
public class NetworkServiceV2_1 {
public void sendMessage(String data) throws NetworkClientExceptionV2 { // Client를 도구로 사용해 일을 처리하는 클래스
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
client.connect();
client.send(data);
client.disconnect();
}
}
➡️ 흐름을 제어하는 클래스 NetworkServiceV2. Client 의 기능을 사용해 외부 서버와 소통한다.
➡️ sendMessage() 메서드를 통해 주소를 적어 Client 객체를 생성하고 데이터를 검사하는 initError() 를 데이터를 넣어 호출한 후 서버 연결, 데이터 전송, 서버 해제를 수행한다.
➡️ sendMessage() 에서도 예외가 발생하면 throws NetworkClientExceptionV2 을 통해서 이 타입의 예외를 밖으로 던진다.
package exception.ex2;
import java.util.Scanner;
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 { // main() 까지 예외가 넘어올 수 있어 넘기라는 표시 필요
NetworkServiceV2_1 networkService = new NetworkServiceV2_1();
//NetworkServiceV2_2 networkService = new NetworkServiceV2_2();
//NetworkServiceV2_3 networkService = new NetworkServiceV2_3();
//NetworkServiceV2_4 networkService = new NetworkServiceV2_4();
//NetworkServiceV2_5 networkService = new NetworkServiceV2_5();
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("프로그램을 정상 종료합니다.");
}
}
➡️ exit 할 때까지 계속 반복해서 문자를 입력받고 메세지를 서버에 전송한다.
➡️ main 에서도 예외가 발생하면 밖으로 던지게 선언해뒀다. 여기서 던지면 예외 메세지와 예외를 추적할 수 있는 스택 트레이스가 콘솔에 뜨고 프로그램이 종료된다.
💥⚠️ 예외 처리만 했지 복구는 하지 못해서 프로그램이 종료된다.
💥⚠️ 외부 서버를 사용하고 반드시 disconnect() 로 해제해야 하는데 그러지 못했다.
예외를 잡아서 정상으로 복구하는 코드를 만들기
package exception.ex2;
public class NetworkServiceV2_2 {
public void sendMessage(String data) throws NetworkClientExceptionV2 { // 예외 복구는 생겨난 곳에서 하는게 아니라 Service 에서 처리한다.
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
try {
client.connect();
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : "+ e.getErrorCode()+", 메세지 : "+ e.getMessage());
return;
}
try {
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : "+e.getErrorCode()+", 메세지 : "+e.getMessage());
return;
}
client.disconnect();
}
}
➡️ 흐름을 제어하는 Service 클래스에 try-catch 로직으로 예외를 복구한다.
➡️ connect(), send() 두 메서드를 try 에 넣어두고 각각 처리하는 오류 코드와 메세지를 출력하게 한 뒤, 프로그램이 종료되지 않고 sendMessage()가 끝나고, 다음 줄의 코드를 계속 실행한다.
💥⚠️ 정상 흐름과 예외 흐름을 구분하기 어려움
💥⚠️ 리턴 하면 disconnect()를 실행할 수 없어서 리소스 해제가 되지 않는다.
정상 흐름을 하나의 try 안에 넣어서 처리한다.
package exception.ex2;
public class NetworkServiceV2_3 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
try {
client.connect();
client.send(data);
client.disconnect();
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : "+e.getErrorCode()+", 메세지 : "+e.getMessage());
}
}
}
➡️ 연결, 전송, 해지 모두 try 에 넣었다. 정상 흐름 중에 예외가 발생하면 밑에 있는 try 코드는 건너뛰고 바로 catch로 이동해서 예외를 복구한다.
try : 코드가 문제 없이 성공할 때 실행되어야 하는 모든 로직
💥⚠️ 정상 흐름과 예외 흐름을 분명하게 분리했지만, 예외가 발생하면 밑에 있는 disconnect() 가 실행되지 않아서 외부의 자원을 자동으로 실행하지 못한다.
리소스 : 무언가를 실행할 때 필요한 자원
package exception.ex2;
public class NetworkServiceV2_4 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
try {
client.connect();
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : "+e.getErrorCode()+", 메세지 : "+e.getMessage());
}
client.disconnect(); // sendMessage() 메서드 마지막에 넣어두면 무조건 코드를 끝내고 종료되야 하기 때문에 항상 실행된다.
}
}
➡️ Service 클래스에서 sendMessage() 로 작업을 시작하니까 이 메서드의 마지막에 disconnect() 를 넣으면 무조건 실행이 된다.
💥⚠️ 하지만 catch 에서 처리할 수 없는 예상치 못한 예외가 발생했을 때! sendMessage() 뒤에 붙은 throws 로 인해 바로 멈추고 main 으로 던져지게 되면서 그 뒤의 disconnect() 는 실행이 되지 않는 문제가 생긴다.
정상적인 상황, 예외 상황, 예상치 못한 상황 이 3가지를 모두 고려해야한다.
그래서 어떤 상황이더라도 반드시 호출하는 finally 를 사용한다.
package exception.ex2;
public class NetworkServiceV2_5 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
try {
client.connect();
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : "+e.getErrorCode()+", 메세지 : "+e.getMessage());
} finally {
client.disconnect();
}
}
}
➡️ 이러면 중간에 예기치 못한 예외가 발생해도 일단 finally는 실행하고 main 으로 던지게 된다.
➡️ 예외가 발생할 일이 없으면 try-finally 로도 사용해 강제할 수 있다.

앞에서는 NetworkClientExceptionV2 로만 예외를 다뤘다. 예외에도 계층이 존재한다. 모든 체크 예외는 Exception 을 상속받는다. 예외는 여러 메서드에서 발생할 수 있다. 그래서 오류 코드로만 분류하는 것이 아니라 메서드 별로 예외 객체를 만들어서 다룬다면 더 세세하게 예외를 복구하고 다룰 수 있다.
⏩ (부모) NetworkClient 클래스에서 생기는 모든 예외 : NetworkClientExceptionV2
⏩ (자식)연결 시 발생하는 예외 : ConnectExceptionV3
⏩ (자식) 전송 시 발생하는 예외 : SendExceptionV3
예외가 객체이기 때문에 부모를 잡으면 자식의 예외도 잡을 수 있다. 전체적인 예외를 한번에 다루고 싶으면 부모를, 세세하게 다루고 싶으면 자식을 잡으면 된다.
package exception.ex3.exception;
public class NetworkClientExceptionV3 extends Exception {
public NetworkClientExceptionV3(String message) { // NetworkClient 에서 발생하는 모든 예외는 이 예외를 부모로 한다.
super(message);
}
}
➡️ 이 예외 객체가 생성될 때, 예외 메세지도 받아온다. 이 메세지는 Exception의 상위 클래스인 Throwable 에 넘겨 저장한다.
package exception.ex3.exception;
public class ConnectExceptionV3 extends NetworkClientExceptionV3 { // 연결 실패시 발생하는 예외, 내부에 연결을 시도한 address 를 보관
private final String address;
public ConnectExceptionV3(String message, String address) { // 생성자를 입력해두지 않으니까 컴파일 에러가 뜨네?
// ☝️ 자바에서 자식이 부모를 상속받을 때 반드시 부모의 생성자를 먼저 호출해야하기 때문이다. super() 가 있어야한다!!
super(message);
this.address = address;
}
public String getAddress() {
return address;
}
}
‼️🥲 까먹은 부분...하위 클래스의 객체 생성자에서는 반드시 부모의 생성자를 호출해야한다. 객체를 생성할 때, 자식이 부모의 필드와 메서드를 “상속받는다”고 해도 그건 단순히 문법적인 연결일 뿐. 실제 메모리에서는 “부모 부분”과 “자식 부분”이 합쳐져서 하나의 객체가 만들어진다. 그래서 자식 객체를 생성할 때는 부모 부분이 필요하다.
➡️ 연결 시 필요한 정보를 생성자를 통해 가져온다. 예외 메세지와 서버 주소를 받아 메세지는 위로 보내고 주소는 예외 발생시 필요한 정보요소라서 본인에 저장해둔다.
(⚠️ 매개변수의 순서가 바뀌면 안된다. 호출하는 곳과 처리하는 곳의 매개변수 순서는 같아야 한다.)
package exception.ex3.exception;
public class SendExceptionV3 extends NetworkClientExceptionV3 {
private final String sendData; // 새로 sendData 라는 멤버변수를 넣었다.
public SendExceptionV3(String sendData, String message) {
super(message);
this.sendData = sendData;
}
public String getSendData() {
return sendData;
}
}
➡️ 내부에 전송을 시도한 데이터를 요소로 가짐
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkClientV3 {
private final String address;
public boolean connectError;
public boolean sendError;
// 생성자로 주소를 받아 객체가 생성된다.
public NetworkClientV3(String address) {
this.address = address;
}
public void connect() throws ConnectExceptionV3 {
if (connectError) {
throw new ConnectExceptionV3(address, address + " 서버 연결 실패");
}
System.out.println(address + " 서버 연결 성공");
}
public void send(String data) throws SendExceptionV3 {
if (sendError) {
throw new SendExceptionV3(data, address + " 서버에 데이터 전송 실패 : " + data); // 여기서 data 가 sendData 이다.
}
System.out.println(address + " 서버에 데이터 전송 : " + data);
}
public void disconnect() {
System.out.println(address+" 서버 연결 해제");
}
// 메인에서 호출 되어 데이터를 받아와 오류가 있는지 검사 후 기능이 있는 클래스(Client) 에 저장해둠
public void initError(String data) {
if (data.contains("error1")) {
connectError = true;
}
if (data.contains("error2")) {
sendError = true;
}
}
}
➡️ connect 면 ConnectExceptionV3 를, send 면 SendExceptionV3 를 생성해 던진다. 이를 통해 메서드 별로 발생하는 예외 그 자체를 바로 알아볼 수 있다.
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.NetworkClientExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkServiceV3_2 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV3 client = new NetworkClientV3(address);
client.initError(data);
try {
client.connect();
client.send(data);
} catch (ConnectExceptionV3 e) { // 연결 오류는 개별로 처리
System.out.println("[연결 오류] 주소: " + e.getAddress() + ", 메세지: " + e.getMessage());
} catch (NetworkClientExceptionV3 e) { // 나머지 예외들은 상위 예외로 처리
System.out.println("[네트워크 오류] 메세지: " + e.getMessage());
} catch (Exception e) { // 알 수 없는 예외 발생 시
System.out.println("[알 수 없는 오류] 메세지: "+e.getMessage());
} finally {
client.disconnect();
}
}
}
➡️ 중요해서 따로 다뤄야 하는 예외는 그 예외 타입으로 처리하고 나머지 예외는 그 상위 예외로 한 번에 처리한다. 그리고 예기치 않은 예외는 더 상위 예외인 Exception 으로 처리한다.
➡️ 이렇게 처리하면 예외별로 하나하나 처리하지 않고 효율적으로 처리할 수 있다.
⚠️
catch코드는 좁은 범위의 예외 -> 넓은 범위 예외 로 더 디테일한 예외를 먼저 입력해야 한다. 처음부터 넓은 범위를 잡으면 이 부분에서 아래의 세세한 예외까지 다 잡아 버릴 수 있기 때문이다.
우선 실무에서는 처리할 수 없는 예외가 처리할 수 있는 예외보다 훨씬 많다. 그래서 빨리 프로그램을 종료시키고 디버깅을 하는 경우가 대부분이다. 그리고 실무에서 예외를 처리할 때는 수많은 라이브러리를 사용하고 또 다양한 외부 시스템과 연동한다. 여기서 문제가 생기면 개발자가 처리할 수 없는‼️ 수많은 예외가 발생하게 된다.
Service 로 여러 예외들이 던져졌다고 하자. 그러면 이 모든 체크 예외를 잡는 코드도 던지는 코드도 모두
try{
} catch(ConnectException e){
} catch(SendException e){
} ...
or
class Service{
void sendMessage(String data) throws ConnectException, DataException, SendException ...){
}
이렇게 예외들을 하나하나 다 잡는 거든 던지는 거든 선언을 해줘야하는 번거로운 일이 생긴다.
(앞에서는 신경써야하는 예외만 따로 잡고 나머지는 상위 예외로 처리했었지만, 지금은 그보다 훨씬 많은 외부 자원과 라이브러리를 가지고 있어서 이걸로도 해결하기 어렵다.)
이 경우는 최악의 수라고 할 수 있다. 체크 예외의 장점을 사용할 수 없게 되기 때문이다. 최상위 체크 예외인 Exception은 모든 체크 예외를 다 던져버린다. 컴파일러가 어떤 예외가 발생할 수 있는지 세세하게 체크하지 않고 타입만 맞으면 다 통과 시켜버린다.
메서드 선언에도 void sendMessage(String data) throws Exception{...} 으로 적혀있어서 선언으로도 예외를 알아채기 어렵다.
코드가 실행되고 콘솔에 스택 트레이스가 출력되면 어디서 어떤 예외가 어떻게 발생했는지 알 수 있지만 개발자의 입장에서는 실행하지 않는 이상 바로 알아보기 어렵다.
잡아도 복구할 수 없는 예외

✔️ 런타임 예외의 철학 : 빨리 실패하게 만들어 개발자가 인지해 코드를 고치도록 유도하는 것 (코드 수정을 강제‼️ 단, 테스트 단계에서 사용해야하고 사용자에게서 나타나면 최악...💥)
1. 일일이 메서드에 throws 선언은 하지 않아도 된다. (컴파일러가 검사하지 않고 바로 던진다.)
2. 복구해야할 필요가 있는 예외에만 throws 를 선언해서 처리할 수 있다.
➡️ 언체크 예외는 여러곳에서 처리하지 않고 공통으로 처리할 한 곳을 정해 모아서 처리한다.
package exception.ex4;
import exception.ex2.NetworkClientExceptionV2;
import exception.ex4.exception.SendExceptionV4;
import java.util.Scanner;
public class MainV4 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV5 networkService = new NetworkServiceV5();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
try { // main() 에서 공통으로 예외를 다룬다
networkService.sendMessage(input);
} catch (Exception e) {
exceptionHandler(e);
}
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
// 예외 공통 처리 ✅
private static void exceptionHandler(Exception e) {
System.out.println("사용자 메세지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");
System.out.println("==개발자용 디버깅 메세지==");
e.printStackTrace(System.out);
if (e instanceof SendExceptionV4 sendEx) { // 전송 오류일 경우❗️
System.out.println("[전송 오류] 전송 데이터: "+sendEx.getSendData());
}
}
}
➡️ main 에서 catch 는 Exception 타입의 예외를 받아서 처리한다. (모든 예외)
➡️ 공통 예외를 처리하는 메서드 exceptionHandler() 를 호출해서 처리한다. 예외 메세지와 스택 트레이스 를 출력할 수 있다.
(실무에서는 로그 라이브러리로 콘솔과 특정파일에 결과 출력함)
➡️ 처리하는 예외 중 더 세세하게 처리하고 싶은 예외는 if / instanceof 를 사용해서 처리한다.
✔️ instanceof
➡️ 다운캐스팅 시 생길 수 있는 오류를 방지할 수 있도록 해주는 연산자. 객체가 특정 클래스(인터페이스)의 인스턴스거나 그 클래스의 하위타입인지 확인하며 boolean 타입을 반환한다. (다운캐스팅 오류가 생기면 ClassCastException 이 발생)
왼쪽 : 검사할 객체
오른쪽 : 비교할 타입
⚠️ 애플리케이션에서 외부 자원을 사용하는 경우 반드시 사용 후 외부 자원을 해제해야 한다.
앞에서 외부자원해제를 강제하기 위해 finally 를 사용했다. 이렇게 만드는 경우가 대부분이라 편의 기능을 추가했다.
먼저 AutoCloseable 인터페이스를 구현해야한다. 이 인터페이스를 구현하면 Try-with-resources 를 사용할 때 try 가 끝나는 시점에 close() 가 자동으로 호출된다.
package exception.ex4;
import exception.ex4.exception.ConnectExceptionV4;
import exception.ex4.exception.SendExceptionV4;
public class NetworkClientV5 implements ‼️AutoCloseable{
private final String address;
public boolean connectError;
public boolean sendError;
public NetworkClientV5(String address) {
this.address = address;
}
public void connect() {
if (connectError) {
throw new ConnectExceptionV4(address, address + " 서버 연결 실패");
}
System.out.println(address+" 서버 연결 성공");
}
public void send(String data) {
if (sendError) {
throw new SendExceptionV4(data,address+" 서버에 데이터 전송 실패: "+data);
}
System.out.println(address+" 서버에 데이터 전송: "+data);
}
public void disconnect() {
System.out.println(address+" 서버 연결 해제");
}
public void initError(String data) {
if (data.contains("error1")) {
connectError = true;
}
if (data.contains("error2")) {
sendError = true;
}
}
// ✅ AutoCloseable 인터페이스의 메서드 오버라이딩
@Override
public void close() { // try 가 끝나면 자동적으로 호출된다.
System.out.println("NetworkClientV5.close");
disconnect(); //그래서 종료 시점에 자원을 반납하는 방법을 여기에 정의하면 된다.
}
}
➡️ implements AutoCloseable 로 인터페이스 구현을 표시했다.
package exception.ex4;
public class NetworkServiceV5 {
public void sendMessage(String data) {
String address = "http://example.com";
/*NetworkClientV4 client = new NetworkClientV4(address);
client.initError(data);*/
try (NetworkClientV5 client = new NetworkClientV5(address)){ // 사용할 외부 자원을 try 에 명시
client.initError(data); // 💥이걸 빼먹어서 에러가 인식이 안됐음
client.connect();
client.send(data);
// catch 블록으로 가기 전에 혹은 예외가 전파되기 전에 해당 리소스의 close()가 먼저 자동으로 호출된다.
} catch (Exception e){
System.out.println("[예외 확인]: " + e.getMessage());
throw e;
}
}
}
➡️ try(NetworkClientV5 client = new NetworkClientV5(address)){...} 를 선언해서 try 가 성공이든 예외가 터져서든 끝나면 바로 close() 를 호출하고 catch 로 넘어간다.
📌 try-with-resources 장점
✔️ 리소스 누수 방지 (리소스가 제대로 닫히도록 보장)
✔️ 빠르고 자원이 사용되는 범위가 한정되서 유지보수가 쉬움