"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.
목차
1. 예외 처리 도입1 - 시작
반환 값을 이용한 예외 처리의 문제점
기존 프로그램에서는 반환 값을 사용하여 예외를 처리했다. 하지만 이 방식에는 다음과 같은 문제가 있다.
- 정상 흐름과 예외 흐름이 섞여 있어 코드 가독성이 떨어진다.
- 정상 흐름보다 예외 흐름의 코드 분량이 더 많아지기도 한다.
- 실무에서는 예외 처리 로직이 더 복잡하다.
이를 해결하기 위해 자바의 예외 처리 기능을 도입한다. 점진적으로 진행한다.
예외 클래스 정의
public class NetworkClientExceptionV2 extends Exception {
private String errorCode;
public NetworkClientExceptionV2(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
- 예외도 객체이므로 필요한 필드(errorCode)와 메서드를 가질 수 있다.
- 오류 코드(`errorCode`): 오류의 유형 구분을 위한 코드.
- 오류 메시지(`message`): 개발자가 이해할 수 있는 오류 설명. `Throwable`에서 제공하는 기능을 활용.
예외를 사용하는 클라이언트 클래스
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 {
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);
}
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;
}
}
- 오류 발생 시 `return`이 아닌 `throw`를 사용하여 예외 객체를 던진다.
- 오류가 발생하면 예외 객체를 만들고, 오류 코드와 오류 메시지를 담는다.
그리고 만든 예외 객체를 `throw`를 통해 던진다.
- 오류가 발생하면 예외 객체를 만들고, 오류 코드와 오류 메시지를 담는다.
- 성공/실패 여부를 반환값이 아닌 예외 발생 유무로 구분할 수 있게 된다.
예외를 던지기만 하는 서비스 클래스
public class NetworkServiceV2_1 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data);
client.connect();
client.send(data);
client.disconnect();
}
}
- 예외를 직접 처리하지 않고 `throws`를 통해 밖으로 던진다. (호출자에게 전달)
예외를 던지는 메인 클래스
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_1 networkService = new NetworkServiceV2_1();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
networkService.sendMessage(input);
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
}
- 예외를 잡지 않고 `main()` 메서드 밖으로 던지도록 설정했다.
실행 결과
정상 입력 시
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
예외 발생 시 (error1)
전송할 문자: error1
Exception in thread "main" exception.ex2.NetworkClientExceptionV2: http://example.com 서버 연결 실패
at exception.ex2.NetworkClientV2.connect(NetworkClientV2.java:15)
at exception.ex2.NetworkServiceV2_1.sendMessage(NetworkServiceV2_1.java:10)
at exception.ex2.MainV2.main(MainV2.java:20)
- 연결 실패가 발생한다.
- `main()` 밖으로 예외가 던져지면 예외 메시지와 예외를 추적할 수 있는 스택 트레이스를 출력하고 프로그램을 종료한다.
예외 발생 시 (error2)
전송할 문자: error2
http://example.com 서버 연결 성공
Exception in thread "main" exception.ex2.NetworkClientExceptionV2: http://example.com 서버에 데이터 전송 실패: error2
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:23)
at exception.ex2.NetworkServiceV2_1.sendMessage(NetworkServiceV2_1.java:11)
at exception.ex2.MainV2.main(MainV2.java:20)
- 데이터 전송 실패가 발생한다.
- `main()` 밖으로 예외가 던져지면 예외 메시지와 예외를 추적할 수 있는 스택 트레이스를 출력하고 프로그램을 종료한다.
남은 문제
- 예외 처리를 도입했지만, 아직 예외가 복구되지 않는다. 따라서 예외가 발생하면 발생하면 프로그램이 종료된다.
- 사용 후에는 반드시 `disconnect()` 를 호출해서 연결을 해제해야 한다.
2. 예외 처리 도입2 - 예외 복구
목표
앞선 예제에서는 예외를 던지기만 했기 때문에, 예외가 발생하면 프로그램이 즉시 종료되었다. 이번에는 예외를 잡아서 처리하고, 흐름을 정상적으로 복구하는 방법을 살펴본다.
예외 복구 흐름
public class NetworkServiceV2_2 {
public void sendMessage(String data) {
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();
}
}
- `connect()` 와 `send()` 메서드 각각에 대해 `try ~ catch` 블럭을 사용하여 `NetworkClientExceptionV2` 예외를 잡았다.
- 여기서는 예외를 잡으면 오류 코드와 예외 메시지를 출력한다.
- 예외를 잡아서 처리했기 때문에 이후에는 정상 흐름으로 복귀한다.
여기서는 리턴을 사용해서 `sendMessage()` 메서드를 정상적으로 빠져나간다.
Main 클래스 변경
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_2 networkService = new NetworkServiceV2_2();
...
}
}
- 기존과 달리 `throws`가 제거되었으며, 예외가 내부에서 처리되므로 프로그램이 중단되지 않는다.
실행 결과
정상 입력은 그대로
예외 발생 (error1)
전송할 문자: error1
[오류] 코드: connectError, 메시지: http://example.com 서버 연결 실패
예외 발생 (error2)
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: sendError, 메시지: http://example.com 서버에 데이터 전송 실패: error2
해결된 문제
- 예외를 잡아서 처리했다. 따라서 예외가 복구 되고, 프로그램도 계속 수행할 수 있다.
남은 문제
- `connect()`와 `send()` 각각에 대해 `try ~ catch`를 분리하여 작성했기 때문에 정상 흐름과 예외 흐름이 분리되지 않고 섞여 있다.
- 사용 후에는 반드시 disconnect() 를 호출해서 연결을 해제해야 한다.
3. 예외 처리 도입3 - 정상, 예외 흐름 분리
목표
앞선 예외 복구 예제에서는 `connect()`와 `send()` 각각을 따로 try ~ catch로 감싸다 보니 정상 흐름과 예외 흐름이 섞여 가독성이 떨어졌다. 이번에는 정상 흐름은 try 블럭에, 예외 흐름은 catch 블럭에 모아서 명확히 분리한다.
개선된 예외 처리 구조
public class NetworkServiceV2_3 {
public void sendMessage(String data) {
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` 블럭 안에 정상 흐름만 나열한다.
- 예외가 발생하면 `catch` 블럭에서 예외 메시지를 출력하고 흐름을 처리한다.
- 정상 흐름은 `try` 블럭에 들어가고, 예외 흐름은 `catch` 블럭으로 분리한다.
Main 클래스 변경
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_3 networkService = new NetworkServiceV2_3();
...
}
}
실행 결과
정상 입력은 그대로
예외 발생 (error1)
전송할 문자: error1
[오류] 코드: connectError, 메시지: http://example.com 서버 연결 실패
예외 발생 (error2)
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: sendError, 메시지: http://example.com 서버에 데이터 전송 실패: error2
해결된 문제
- try 블럭에는 정상 흐름만 포함되고, catch 블럭에는 예외 흐름만 들어가므로 코드 가독성이 높아졌다.
남은 문제
- 예외가 발생할 경우 disconnect()가 호출되지 않아 리소스가 반환되지 않을 수 있는 문제가 여전히 존재한다.
- 예외가 발생해도 반드시 리소스를 반납해야 하는 경우, `disconnect()`를 항상 호출하도록 보장할 수 있는 추가적인 처리가 필요하다.
4. 예외 처리 도입4 - 리소스 반환 문제
목표
이전 예제에서는 예외가 발생하면 `disconnect()`가 호출되지 않아 리소스 누수가 발생할 수 있었다.
이 문제를 해결하기 위해 예외 발생 여부와 관계없이 `disconnect()`가 반드시 호출되는 구조를 생각해야 한다.
시도 1: try ~ catch 이후 일반 흐름에 disconnect() 호출
public class NetworkServiceV2_4 {
public void sendMessage(String data) {
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());
}
// NetworkClientException이 아닌 다른 예외가 발생해서 예외가 밖으로 던져지면 무시
client.disconnect();
}
}
- 예외가 발생하든 말든 `disconnect()`는 항상 실행된다.
- 단, 이 코드는 `catch`로 잡지 못한 예외가 발생하면 `disconnect()`가 호출되지 않는다.
시도 2: 예외가 catch 대상이 아닌 경우
NetworkClientV2- 코드 변경 (실행 후 다시 변경한다)
public void send(String data) throws NetworkClientExceptionV2 {
if (sendError) {
// throw new NetworkClientExceptionV2("sendError", address + " 서버에 데이터 전송 실패: " + data);
// 중간에 다른 예외가 발생했다고 가정
throw new RuntimeException("ex"); // 언체크 예외
}
// 전송 성공
System.out.println(address + " 서버에 데이터 전송: " + data);
}
- `send()` 메서드에서 `RuntimeException`이 발생하면, `catch (NetworkClientExceptionV2)`에서는 이를 잡지 못한다.
- 따라서 예외는 밖으로 던져지고, `client.disconnect()`는 실행되지 않는다.
Main 클래스 변경
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_4 networkService = new NetworkServiceV2_4();
...
}
}
실행 결과
전송할 문자: error1
[오류] 코드: connectError, 메시지: http://example.com 서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
Exception in thread "main" java.lang.RuntimeException: ex
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:25)
at exception.ex2.NetworkServiceV2_4.sendMessage(NetworkServiceV2_4.java:12)
at exception.ex2.MainV2.main(MainV2.java:20)
- error1 의 경우 client.connect() 에서 NetworkClientExceptionV2 예외가 발생하기 때문에 바로 catch 로 이동해서 정상 흐름을 이어간다.
- error2 의 경우 client.send() 에서 RuntimeException 이 발생한다. 이 예외는 catch 의 대상이 아 니므로 잡지 않고 즉시 밖으로 던져진다.
핵심 문제 요약
try {
client.connect();
client.send(data); // 여기서 예외 발생
} catch (NetworkClientExceptionV2 e) {
...
}
client.disconnect(); // 호출되지 않음!
- catch 대상이 아닌 예외가 발생할 경우 disconnect()가 호출되지 않는다.
- 결국 리소스 정리는 실패하게 된다.
남은 문제
- 예외 처리 후 `disconnect()` 호출을 일반 흐름에 둘 경우, 예상하지 못한 예외가 발생하면 호출되지 않을 수 있다.
- 자원 해제는 반드시 보장되어야 한다.
- 다음 단계에서는 finally를 도입하여 모든 예외 상황에서도 자원 반환이 확실히 보장되도록 개선할 것이다.
5. 예외 처리 도입5 - finally
목표
예외가 발생해도 `disconnect()`가 항상 호출되도록 보장하는 것이 목적이다.
자바는 이를 위해 `try ~ catch ~ finally` 구조를 제공한다.
`finally`는 정상 흐름, 예외 발생 여부에 관계없이 반드시 실행되는 블럭이다.
try ~ catch ~ finally 구조
try {
// 정상 흐름
} catch (예외 타입 e) {
// 예외 흐름
} finally {
// 반드시 호출해야 하는 마무리 흐름
}
- `try` 시작만 하면, 어떤 경로를 거치든 `finally`는 반드시 실행된다.
- "정상 흐름" or "예외 catch" or "예외 던짐" => `finally`
- `finally` 코드 블럭이 끝나고 나서 이후에 예외가 밖으로 던져진다.
- `try` , `catch` 안에서 잡을 수 없는 예외가 발생해도 `finally` 는 반드시 호출된다.
- `try`에서 사용한 자원을 해제할 때 주로 사용한다.
finally 적용
public class NetworkServiceV2_5 {
public void sendMessage(String data) {
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();
}
}
}
- `disconnect()`는 어떤 예외가 발생하든 항상 호출된다.
- 예외가 발생하지 않으면 그대로 연결 종료.
- 예외가 발생하고 `catch`로 처리되든 안되든 상관없이 `disconnect()`는 실행된다.
Main 클래스 변경
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_5 networkService = new NetworkServiceV2_5();
...
}
}
실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: error1
[오류] 코드: connectError, 메시지: http://example.com 서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: sendError, 메시지: http://example.com 서버에 데이터 전송 실패: error2
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
- 각 경우 모두 `disconnect()`가 실행됨을 확인할 수 있다.
처리할 수 없는 예외(예: RuntimeException)와 finally
NetworkClientV2- 코드 변경 (실행 후 다시 변경한다)
public void send(String data) throws NetworkClientExceptionV2 {
if (sendError) {
// throw new NetworkClientExceptionV2("sendError", address + " 서버에 데이터 전송 실패: " + data);
// 중간에 다른 예외가 발생했다고 가정
throw new RuntimeException("ex"); // 언체크 예외
}
// 전송 성공
System.out.println(address + " 서버에 데이터 전송: " + data);
}
실행 결과
전송할 문자: error2
http://example.com 서버 연결 성공
http://example.com 서버 연결 해제
Exception in thread "main" java.lang.RuntimeException: ex
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:25)
at exception.ex2.NetworkServiceV2_5.sendMessage(NetworkServiceV2_5.java:12)
at exception.ex2.MainV2.main(MainV2.java:20)
- `RuntimeException`은 `catch`에서 잡히지 않지만, finally 블럭이 실행되어 `disconnect()`가 호출된다.
- => `catch`에서 잡을 수 없는 예외가 발생해서 예외를 밖으로 던지는 경우에도 `finally`를 먼저 호출하고 나서 예외를 밖으로 던진다.
try ~ finally만 사용하는 방법
예외를 따로 처리하지 않아도 된다면 다음과 같이 `catch` 없이 `finally`만 사용 가능하다.
try {
client.connect();
client.send(data);
} finally {
client.disconnect(); // 항상 호출
}
- 예외를 직접 처리하지 않고 외부로 던지면서 자원만 정리하고 싶은 경우 사용한다.
try ~ catch ~ finally 구조의 장점
- 정상 흐름과 예외 흐름을 분리해서, 코드를 읽기 쉽게 만든다.
- 사용한 자원을 항상 반환할 수 있도록 보장해준다.
6. 예외 계층1 - 시작
목표
기존에는 오류를 구분하기 위해 오류 코드를 사용했지만, 예외를 계층화하면 더 세밀하고 명확하게 예외를 처리할 수 있다.
예외를 계층 구조로 설계하면 다음과 같은 장점이 있다.
- 자바에서 예외는 객체이다. 부모 예외 하나로 모든 자식 예외를 처리할 수 있다.
- 필요한 경우 특정 예외만 따로 잡아서 처리할 수 있다.
- 예외에 필요한 추가 필드나 메서드를 정의할 수 있다.
예외 계층 설계
- `NetworkClientExceptionV3` : 상위 예외 (`NetworkClient`에서 발생하는 모든 예외는 이 예외의 자식)
- `ConnectExceptionV3` : 연결 실패 예외 (주소 정보 포함)
- `SendExceptionV3` : 전송 실패 예외 (데이터 정보 포함)
상위 예외 클래스
public class NetworkClientExceptionV3 extends Exception {
public NetworkClientExceptionV3(String message) {
super(message);
}
}
연결 실패 예외
public class ConnectExceptionV3 extends NetworkClientExceptionV3 {
private final String address;
public ConnectExceptionV3(String address, String message) {
super(message);
this.address = address;
}
public String getAddress() {
return address;
}
}
- 연결 실패시 발생하는 예외이다. 내부에 연결을 시도한 `address` 를 보관한다.
- `NetworkClientExceptionV3` 를 상속했다.
전송 실패 예외
public class SendExceptionV3 extends NetworkClientExceptionV3 {
private final String sendData;
public SendExceptionV3(String sendData, String message) {
super(message);
this.sendData = sendData;
}
public String getSendData() {
return sendData;
}
}
- 전송 실패시 발생하는 예외이다. 내부에 전송을 시도한 데이터인 `sendData` 를 보관한다.
- `NetworkClientExceptionV3` 를 상속했다.
예외 발생 위치에 따라 세분화된 예외 처리
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);
}
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;
}
}
}
- 예외 그 자체로 어떤 오류가 발생했는지 알 수 있다.
- 연결 관련 오류 발생하면 `ConnectExceptionV3` 를 던지고, 전송 관련 오류가 발생하면 `SendExceptionV3` 를 던진다.
서비스 클래스에서 세부 예외 처리
public class NetworkServiceV3_1 {
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 (SendExceptionV3 e) {
System.out.println("[전송 오류] 전송 데이터: " + e.getSendData() + ", 메시지: " + e.getMessage());
} finally {
client.disconnect();
}
}
}
- `catch (ConnectExceptionV3 e)` : 연결 예외를 잡고, 해당 예외가 제공하는 기능을 사용해서 정보를 출력한다.
- `catch (SendExceptionV3 e)` : 전송 예외를 잡고, 해당 예외가 제공하는 기능을 사용해서 정보를 출력한다.
Main 클래스
public class MainV3 {
public static void main(String[] args) {
NetworkServiceV3_1 networkService = new NetworkServiceV3_1();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
networkService.sendMessage(input);
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
}
실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: error1
[연결 오류] 주소: http://example.com, 메시지: http://example.com 서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[전송 오류] 전송 데이터: error2, 메시지: http://example.com 서버에 데이터 전송 실패: error2
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
- `ConnectExceptionV3` , `SendExceptionV3` 이 발생한 각각의 경우에 출력된 오류 메시지가 다른 것을 확인할 수 있다.
7. 예외 계층2 - 활용
목표
예외를 계층 구조로 설계하면, 세부 예외를 따로 처리할 수도 있고 상위 예외 하나로 여러 예외를 묶어서 처리할 수도 있다.
이번에는 예외 계층을 활용하여 상황에 따라 예외를 다르게 처리하는 방법을 알아본다.
예외 처리 전략
- `ConnectExceptionV3`처럼 중요한 예외는 별도로 처리
- 그 외 `NetworkClientExceptionV3`의 하위 예외는 하나로 묶어서 처리
- 예상치 못한 예외는 `Exception`으로 모든 예외를 최종 처리
예외 처리 코드
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();
}
}
}
- `ConnectExceptionV3`은 중요한 예외이므로 개별 처리
- `SendExceptionV3` 등 다른 예외는 상위 예외인 `NetworkClientExceptionV3`로 통합 처리
- `RuntimeException` 등 예상치 못한 예외는 Exception에서 처리
예외 처리 순서 예시
ConnectExceptionV3 발생
try {
// 1. ConnectExceptionV3 발생
} catch (ConnectExceptionV3 e) { // 1. 여기서 잡아서 처리
} catch (NetworkClientExceptionV3 e) {
} catch (Exception e) {
}
SendExceptionV3 발생
try {
// 1. SendExceptionV3 발생
} catch (ConnectExceptionV3 e) { // 2. 대상이 다름
} catch (NetworkClientExceptionV3 e) { // 3. 부모 예외로 잡힘 (NetworkClientExceptionV3은 부모)
} catch (Exception e) {
}
RuntimeException 발생
try {
// 1. RuntimeException 발생
} catch (ConnectExceptionV3 e) { // 2. 대상이 다름
} catch (NetworkClientExceptionV3 e) { // 3. 대상이 다름
} catch (Exception e) { // 3. 여기서 잡힘 (Exception은 RuntimeException의 부모)
}
예외는 자식 → 부모 → 최상위 순서로 처리되기 때문에, 더 구체적인 예외를 먼저 `catch`해야 한다.
Main 클래스 변경
public class MainV3 {
public static void main(String[] args) {
NetworkServiceV3_2 networkService = new NetworkServiceV3_2();
...
}
}
실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: error1
[연결 오류] 주소: http://example.com, 메시지: http://example.com 서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[네트워크 오류] 메시지: http://example.com 서버에 데이터 전송 실패: error2
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
여러 예외를 한 번에 처리하는 방법
`catch`에 `|` 연산자를 사용하면 여러 예외를 동시에 잡을 수 있다.
try {
client.connect();
client.send(data);
} catch (ConnectExceptionV3 | SendExceptionV3 e) {
System.out.println("[연결 또는 전송 오류] 메시지: " + e.getMessage());
} finally {
client.disconnect();
}
- 단, 이 경우 공통 부모 타입에 정의된 기능(메서드)만 사용할 수 있다.
- 즉, `e.getAddress()` 또는 `e.getSendData()`는 사용할 수 없다.
- 여기서는 `NetworkClientExceptionV3` 의 기능만 사용할 수 있다.
정리
- 예외 계층을 활용하면 중요한 예외는 구체적으로, 그 외는 상위 예외로 통합해서 처리할 수 있다.
- 예외 분류에 따라 다르게 처리함으로써 가독성과 안정성을 동시에 확보할 수 있다.
- `|` 연산자를 통해 여러 예외를 하나의 catch로 묶는 방식도 가능하지만, 예외별 세부 기능은 사용할 수 없다.
8. 실무 예외 처리 방안1 - 설명
목표
실무에서는 예외가 발생해도 복구가 불가능한 경우가 많다. 따라서 모든 예외를 체크 예외로 처리하기보다, 복구 가능한 예외와 불가능한 예외를 구분하고, 실무에 적합한 예외 처리 전략을 세우는 것이 중요하다.
처리할 수 없는 예외
예를 들어 다음과 같은 경우에는 예외를 잡아도 복구할 수 없다.
- 상대 네트워크 서버 장애
- 데이터베이스 접속 실패 등 시스템 문제
이러한 예외는 다시 시도하더라도 실패할 가능성이 높다. 따라서 다음과 같은 방식으로 대응한다.
- 사용자에게는 “현재 시스템에 문제가 있습니다.”라는 오류 메시지를 보여준다.
- 웹 애플리케이션의 경우 오류 페이지로 안내한다.
- 개발자를 위해 로그를 남긴다.
체크 예외의 부담
체크 예외는 컴파일러가 누락 없이 예외 처리를 강제할 수 있다는 점에서 오래전부터 많이 사용되었다.
하지만 복구할 수 없는 예외가 많아지고, 애플리케이션이 복잡해지면서 체크 예외는 오히려 부담이 되는 경우가 많다.
모든 체크 예외를 처리해야 하는 시나리오
try {
// 실행 코드
} catch (NetworkException e) { ... }
catch (DatabaseException e) { ... }
catch (XxxException e) { ... }
또는
class Service {
void sendMessage(String data) throws NetworkException, DatabaseException, XxxException {
...
}
}
- 많은 라이브러리나 외부 시스템에서 예외가 발생하면, 처리할 수 없는 예외임에도 `throws`로 모두 던져야 한다.
- 중간 계층의 클래스 등에서도 복구하지 못하므로, 결국 모든 계층에서 `throws` 선언으로 예외를 계속 밖으로 던지는 코드가 만들어진다.
결국 최악의 선택: throws Exception
개발자는 수 많은 체크 예외 지옥에 빠지고, 결국 다음과 같은 최악의 수를 두게 된다.
class Service {
void sendMessage(String data) throws Exception {
...
}
}
- `Exception`은 거의 모든 예외의 부모이다.
- 하나로 던지면 컴파일은 통과하지만, 컴파일러의 체크 기능이 무력화된다.
- 중간에 중요한 체크 예외가 발생해도 잡지 못하고 넘어가며, 구체적인 예외 정보를 놓칠 수 있다.
- 꼭 필요한 경우가 아니면 이렇게 `Exception` 자체를 밖으로 던지는 것을 좋지 않은 방법이다.
체크 예외의 문제 정리
- 처리할 수 없는 예외: 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다.
- 체크 예외의 부담: 처리할 수 없는 예외는 밖으로 던져야 한다. 체크 예외이므로 throws 에 던질 대상을 일일이 명시해야 한다.
언체크(런타임) 예외로 대체하는 전략
언체크(런타임) 에외는 `RuntimeException`을 상속받으며 `throws` 없이도 던질 수 있다.
1. 복구 불가능한 예외는 런타임 예외로 설계
class Service {
void sendMessage(String data) {
...
}
}
- `throws`를 선언하지 않아도 자동으로 밖으로 던진다.
- 사용 라이브러리가 늘어나서 언체크 예외가 늘어도 복구가 불가능하다면 신경 쓰지 않아도 된다.
- 상대 네트워크 서버가 내려가거나, 데이터베이스 서버에 문제가 발생한 경우
`Service`에서 예외를 잡아도 복구할 수 없다.
→ 처리할 수 없는 예외들은 밖으로 던지는 것이 더 나은 결정이다.
- 상대 네트워크 서버가 내려가거나, 데이터베이스 서버에 문제가 발생한 경우
2. 필요하면 일부 언체크 예외만 catch해서 처리 가능
try {
...
} catch (XxxException e) {
// 특별히 처리 가능하면 여기서 처리
}
- 일부 언체크 예외를 잡아서 처리할 수 있다면 잡아서 처리하면 된다.
예외 공통 처리
복구 불가능한 예외를 여러곳에서 처리하기 보다는 공통 예외 처리 영역에서 한 번에 처리하는 것이 낫다.
- 사용자에게는 “현재 시스템에 문제가 있습니다.”라는 오류 메시지 출력
- 개발자에게는 스택 트레이스 및 상세 로그 남기기
- 실무에서는 로깅 라이브러리(SLF4J, Logback 등)를 사용해 콘솔 출력이 아닌 로그 파일로 기록 (ex 파일, 이메일, 슬랙 등)
9. 실무 예외 처리 방안2 - 구현
목표
앞서 설명한 이론을 바탕으로 실제 실무 상황에 맞춰 복구할 수 없는 예외는 언체크(런타임) 예외로 정의하고,
이러한 예외들을 공통 처리 영역에서 한 번에 처리하는 방식으로 구현한다.
예외 계층 구성
- `NetworkClientExceptionV4`: 상위 런타임 예외 (`RuntimeException` 상속)
- `ConnectExceptionV4`: 연결 실패 예외 (address 포함)
- `SendExceptionV4`: 데이터 전송 실패 예외 (sendData 포함)
public class NetworkClientExceptionV4 extends RuntimeException {
public NetworkClientExceptionV4(String message) {
super(message);
}
}
public class ConnectExceptionV4 extends NetworkClientExceptionV4 {
private final String address;
public ConnectExceptionV4(String address, String message) {
super(message);
this.address = address;
}
public String getAddress() {
return address;
}
}
public class SendExceptionV4 extends NetworkClientExceptionV4 {
private final String sendData;
public SendExceptionV4(String sendData, String message) {
super(message);
this.sendData = sendData;
}
public String getSendData() {
return sendData;
}
}
클라이언트 클래스
`throws` 선언 없이 예외를 던진다. `RuntimeException`이므로 명시하지 않아도 된다.
public class NetworkClientV4 {
private final String address;
public boolean connectError;
public boolean sendError;
public NetworkClientV4(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;
}
}
}
서비스 클래스
복구할 수 없는 예외는 `catch`하지 않고, 자원 정리를 위해 `finally`만 사용한다.
public class NetworkServiceV4 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV4 client = new NetworkClientV4(address);
client.initError(data); // 추가
try {
client.connect();
client.send(data);
} finally {
client.disconnect();
}
}
}
- `NetworkServiceV4` 는 발생하는 예외인 `ConnectExceptionV4` , `SendExceptionV4` 를 잡아도 해당 오류들을 복구할 수 없다. 따라서 예외를 밖으로 던진다.
공통 예외 처리 - Main 클래스
모든 예외는 `Exception`을 상위에서 잡고, 공통 핸들러에서 처리한다.
public class MainV4 {
public static void main(String[] args) {
NetworkServiceV4 networkService = new NetworkServiceV4();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if (input.equals("exit")) {
break;
}
try {
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); // 스택 트레이스 출력
// e.printStackTrace(System.err);
// 필요하면 예외 별로 별도의 추가 처리 가능
if (e instanceof SendExceptionV4 sendEx) {
System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData());
}
}
}
공통 예외 처리
try {
networkService.sendMessage(input);
} catch (Exception e) { // 모든 예외를 잡아서 처리
exceptionHandler(e);
}
- Exception 을 잡아서 해결하지 못한 모든 예외를 여기서 공통으로 처리한다.
- 예외도 객체이므로 공통 처리 메서드인 exceptionHandler(e) 에 예외 객체를 전달한다.
exceptionHandler()
- 사용자 대응
- 사용자에게는 상세 원인을 알릴 필요 없이 "시스템 내에 알 수 없는 문제가 발생했습니다." 정도 메시지만 제공한다.
- 개발자 대응
- `e.printStackTrace()`로 예외 로그를 출력하여 문제를 찾는다.
- `instanceof` 를 활용해 예외 객체의 타입을 확인해서 별도 추가 처리를 할 수 있다.
e.printStackTrace()
- 예외 메시지와 스택 트레이스(예외 발생 위치)를 출력하는 데 사용된다.
- 예외가 어디서 발생했는지 역추적할 수 있다.
- 기본적으로 `System.err`로 출력하며, IDE에서는 빨간색으로 표시되어 오류를 쉽게 구분할 수 있다.
- 예제에서는 `System.out`을 사용해서 표준 출력으로 보냈다.
- System.out과 System.err는 별개의 출력 스트림이기 때문에, 두 개를 혼용하면 출력 순서가 꼬일 수 있다.
참고: 실무에서의 활용
`e.printStackTrace()`는 콘솔에만 출력되기 때문에, 서버 환경에서는 로그 확인이 어렵다.
실무에서는 보통 Slf4J, Logback 등의 로그 라이브러리를 사용하여 예외 정보를 콘솔 + 로그 파일 양쪽에 남긴다.
로그 파일에 예외를 기록하지 않으면, 나중에 문제를 추적하기 어렵고, 운영 환경에서 로그가 유실될 수 있다.
실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: error1
http://example.com 서버 연결 해제
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.ConnectExceptionV4: http://example.com 서버 연결 실패
at exception.ex4.NetworkClientV4.connect(NetworkClientV4.java:18)
at exception.ex4.NetworkServiceV4.sendMessage(NetworkServiceV4.java:11)
at exception.ex4.MainV4.main(MainV4.java:21)
전송할 문자: error2
http://example.com 서버 연결 성공
http://example.com 서버 연결 해제
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.SendExceptionV4: http://example.com 서버에 데이터 전송 실패: error2
at exception.ex4.NetworkClientV4.send(NetworkClientV4.java:26)
at exception.ex4.NetworkServiceV4.sendMessage(NetworkServiceV4.java:12)
at exception.ex4.MainV4.main(MainV4.java:21)
[전송 오류] 전송 데이터: error2
전송할 문자: exit
프로그램을 정상 종료합니다.
- error1: 연결 실패 → ConnectExceptionV4 발생 → 공통 처리
- error2: 전송 실패 → SendExceptionV4 발생 → 공통 처리 + 전송 데이터 출력
- exit: 정상 종료
참고: 과거에 자바에서 제공하는 라이브러리들은 체크 예외가 많았다. 하지만 최근 자바가 제공하는 라이브러리뿐만 아니라 최근 오픈소스들은 대부분 언체크(런타임) 예외를 사용한다.
10. try-with-resources
목표
외부 자원을 사용하는 경우, 사용 후 반드시 자원을 반납해야 한다. 이를 위해 자바는 Try with resources 라는 문법을 제공한다. (자바 7에서 도입)
기존에는 `finally` 블럭에서 명시적으로 자원 해제를 해야 했지만, Try with resources 를 사용하면 더 간결하고 안전하게 자원을 처리할 수 있다.
AutoCloseable 인터페이스
`try-with-resources`를 사용하려면 클래스가 `AutoCloseable` 인터페이스를 구현해야 한다.
public interface AutoCloseable {
void close() throws Exception;
}
- `close()` 메서드는 자원을 해제할 때 자동으로 호출된다. `try` 블럭이 끝나는 시점에 `close()`가 실행된다.
클라이언트 클래스
`NetworkClientV5`는 `AutoCloseable`을 구현하고, `close()` 메서드에서 연결 해제를 처리한다.
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;
}
}
@Override
public void close() {
System.out.println("NetworkClientV5.close");
disconnect();
}
}
서비스 클래스
`try` 블럭에서 자원을 생성하고, 블럭이 종료되면 `close()`가 자동으로 호출된다.
public class NetworkServiceV5 {
public void sendMessage(String data) {
String address = "http://example.com";
try (NetworkClientV5 client = new NetworkClientV5(address)) {
client.initError(data);
client.connect();
client.send(data);
} catch (Exception e) {
System.out.println("[예외 확인]: " + e.getMessage());
throw e;
}
}
}
- Try with resources 구문은 `try` 괄호 안에 사용할 자원을 명시한다.
- `try` 블럭이 끝나면 자동으로 `AutoCloseable.close()` 를 호출해서 자원을 해제한다.
- `catch` 블럭은 생략 가능하다.
- 여기서 `catch` 블럭은 단순히 발생한 예외를 잡아서 예외 메시지를 출력하고, 잡은 예외를 `throw` 를 사용해서 다시 밖으로 던진다.
Main 클래스 - 코드 변경
public class MainV4 {
public static void main(String[] args) {
// NetworkServiceV4 networkService = new NetworkServiceV4();
NetworkServiceV5 networkService = new NetworkServiceV5();
}
}
실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
NetworkClientV5.close
http://example.com 서버 연결 해제
전송할 문자: error1
NetworkClientV5.close
http://example.com 서버 연결 해제
[예외 확인]: http://example.com 서버 연결 실패
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.ConnectExceptionV4: http://example.com 서버 연결 실패
at exception.ex4.NetworkClientV5.connect(NetworkClientV5.java:18)
at exception.ex4.NetworkServiceV5.sendMessage(NetworkServiceV5.java:10)
at exception.ex4.MainV4.main(MainV4.java:21)
전송할 문자: error2
http://example.com 서버 연결 성공
NetworkClientV5.close
http://example.com 서버 연결 해제
[예외 확인]: http://example.com 서버에 데이터 전송 실패: error2
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.SendExceptionV4: http://example.com 서버에 데이터 전송 실패: error2
at exception.ex4.NetworkClientV5.send(NetworkClientV5.java:26)
at exception.ex4.NetworkServiceV5.sendMessage(NetworkServiceV5.java:11)
at exception.ex4.MainV4.main(MainV4.java:21)
[전송 오류] 전송 데이터: error2
전송할 문자: exit
프로그램을 정상 종료합니다.
Try with resources 의 장점
- 리소스 누수 방지: : 모든 리소스가 제대로 닫히도록 보장한다. `finally` 누락이나 지원 해제 코드 누락을 방지할 수 있다.
- 코드 간결성: 명시적인 `close()` 호출이 없어 코드가 더 간결해진다.
- 자원 스코프 제한: 자원 객체의 생명 주기가 `try` 블럭으로 한정되어 유지보수가 쉽다.
- 빠른 자원 해제: `try` 블럭이 끝나면 즉시 `close()`가 즉시 호출된다.
정리
자바 초기에는 체크 예외가 더 나은 방식이라고 여겨졌고, 실제로 많은 기본 라이브러리들이 체크 예외를 사용했다.
하지만 시간이 지나며 복구할 수 없는 예외가 많아지고, 다양한 라이브러리에서 발생하는 예외들을 처리하기 위해 `throws` 선언이 과도하게 늘어나는 문제가 생겼다.
결국 많은 개발자들이 `throws Exception` 같은 극단적인 방법을 선택하기도 했지만, 이는 어떤 예외를 처리하고 있는지 명확하지 않아 바람직하지 않다.
이러한 문제로 인해 최근의 라이브러리들(예: Spring, JPA)은 대부분 런타임 예외를 기본으로 제공한다.
런타임 예외는 필요할 때만 `catch`하고, 그렇지 않으면 자연스럽게 던져서 공통 처리 영역에서 처리하는 방식이 선호된다.
실무에서는 복구 가능한 예외만 체크 예외로 처리하고, 복구 불가능한 예외는 언체크 예외로 선언하여 공통 처리하는 방식이 더 효율적이다.
'Course > Java' 카테고리의 다른 글
[java-mid1] 9. 예외 처리1 - 이론 (0) | 2025.04.06 |
---|---|
[java-mid1] 8. 중첩 클래스, 내부 클래스2 (0) | 2025.03.28 |
[java-mid1] 7. 중첩 클래스, 내부 클래스1 (0) | 2025.03.24 |
[java-mid1] 5. 열거형 - ENUM (0) | 2025.03.15 |
[java-mid1] 4. 래퍼, Class 클래스 (0) | 2025.03.12 |
댓글