public class NetworkClientExceptionV2 extends Exception {
private String errorCode;
public NetworkClientExceptionV2(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
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 + "서버에 데이터 전송 성공 실패");
}
System.out.println(address + " 서버에 데이터 전송 성공: " + data);
}
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
// 사용자가 입력한 데이터를 기반으로 오류 체크
public void initError(String data) {
if (data.equals("error1")) {
connectError = true;
}
if (data.equals("error2")) {
sendError = true;
}
}
}
public class NetworkServiceV1_4 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 networkClientV2 = new NetworkClientV2(address);
networkClientV2.initError(data);
networkClientV2.connect();
networkClientV2.send(data);
networkClientV2.disconnect();
}
}
public class MainV2 {
// 메인에서도 예외를 해결하지 않고 밖으로 던지는 상황
public static void main(String[] args) throws NetworkClientExceptionV2 {
//NetworkServiceV1_1 networkServiceV1 = new NetworkServiceV1_1();
//NetworkServiceV1_2 networkServiceV1 = new NetworkServiceV1_2();
//NetworkServiceV1_3 networkServiceV1 = new NetworkServiceV1_3();
NetworkServiceV1_4 networkServiceV1 = new NetworkServiceV1_4();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String string = scanner.nextLine();
if (string.equals("exit")) {
break;
}
networkServiceV1.sendMessage(string);
System.out.println();
}
System.out.println("프로그램 정상 종료");
}
}
문제
예외 처리를 도입했지만, 아직 예외가 복구되지 않는다. 따라서 예외가 발생하면 프로그램이 종료된다.
사용 후에는 반드시 disconnect()
를 호출해서 연결을 해제해야 한다.
public class NetworkServiceV2_2 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 networkClientV2 = new NetworkClientV2(address);
networkClientV2.initError(data);
try {
networkClientV2.connect();
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : " + e.getErrorCode() + ", 메시지 : " + e.getMessage());
return;
}
try {
networkClientV2.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : " + e.getErrorCode() + ", 메시지 : " + e.getMessage());
return;
}
networkClientV2.disconnect();
}
}
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_2 networkServiceV1 = new NetworkServiceV2_2();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String string = scanner.nextLine();
if (string.equals("exit")) {
break;
}
networkServiceV1.sendMessage(string);
System.out.println();
}
System.out.println("프로그램 정상 종료");
}
}
문제
예외 처리를 했지만 정상 흐름과 예외 흐름이 섞여 있어 코드를 읽기 어렵다.
또한 사용 후에는 반드시 disconnect()
를 호출해서 연결을 해제해야 한다.
public class NetworkServiceV2_3 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 networkClientV2 = new NetworkClientV2(address);
networkClientV2.initError(data);
try {
networkClientV2.connect();
networkClientV2.send(data);
networkClientV2.disconnect();
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : " + e.getErrorCode() + ", 메시지 : " + e.getMessage());
}
}
}
해결된 문제
try ~ catch
블럭 구조 덕분에 정상 흐름은 try
블럭에 모아서 처리하고 예외 흐름은 catch
블럭에 별도로 모아 처리할 수 있다.남은 문제
disconnect()
를 호출해서 연결을 해제해야 한다. 외부 연결과 같은 자바 외부 자원은 자동으로 해제가 되지 않는다. 따라서 외부 자원을 사용한 후에는 반드시 연결을 해제해주어야만 한다.public class NetworkServiceV2_4 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 networkClientV2 = new NetworkClientV2(address);
networkClientV2.initError(data);
try {
networkClientV2.connect();
networkClientV2.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : " + e.getErrorCode() + ", 메시지 : " + e.getMessage());
}
networkClientV2.disconnect();
}
}
문제
예외가 해결이 되고 연결을 해제하는 것처럼 보이지만 만약 NetworkClientExceptionV2
예외가 아닌 경우 catch
문에서 잡을 수 없어 서버 연결 해제가 정상적으로 수행되지 않는다.
try {
// 정상 흐름
} catch {
// 예외 흐름
} finally {
// 반드시 호출해야 하는 마무리 흐름
}
finally 실행 시점
finally
finally
finally
public class NetworkServiceV2_5 {
public void sendMessage(String data) throws NetworkClientExceptionV2 {
String address = "http://example.com";
NetworkClientV2 networkClientV2 = new NetworkClientV2(address);
networkClientV2.initError(data);
try {
networkClientV2.connect();
networkClientV2.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드 : " + e.getErrorCode() + ", 메시지 : " + e.getMessage());
} finally {
networkClientV2.disconnect();
}
}
}
try ~ finally
예외를 직접 잡아서 처리할 일이 없다면 try ~ finally
를 사용할 수 있다.
예외는 단순히 오류 코드로 분류하는 것이 아니라 예외를 계층화해서 다양하게 만들면 세밀하게 예외를 처리할 수 있다.
자바에서 예외는 객체이다. 부모 예외를 잡거나 던지면 그 하위 자식 예외도 함께 잡거나 던질 수 있는 것이다.
package me.jangwoojin.exception;
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(address + " 서버에 데이터 전송 실패", data);
}
System.out.println(address + " 서버에 데이터 전송 성공: " + data);
}
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
// 사용자가 입력한 데이터를 기반으로 오류 체크
public void initError(String data) {
if (data.equals("error1")) {
connectError = true;
}
if (data.equals("error2")) {
sendError = true;
}
}
}
개선된 부분
어떤 예외가 터지는지를 명확히 알 수 있다는 점(예외 그 자체로 어떤 오류가 발생했는가?)
예외 계층 세분화
public class NetworkClientExceptionV3 extends Exception {
public NetworkClientExceptionV3(String message) {
super(message);
}
}
public class ConnectExceptionV3 extends NetworkClientExceptionV3 {
private final String address;
public ConnectExceptionV3(String message, String address) {
super(message);
this.address = address;
}
public String getAddress() {
return address;
}
}
public class SendExceptionV3 extends NetworkClientExceptionV3 {
private final String data;
public SendExceptionV3(String message, String data) {
super(message);
this.data = data;
}
public String getData() {
return data;
}
}
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(address + " 서버에 데이터 전송 실패", data);
}
System.out.println(address + " 서버에 데이터 전송 성공: " + data);
}
////////////////////////////////////////////////////////////////
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
// 사용자가 입력한 데이터를 기반으로 오류 체크
public void initError(String data) {
if (data.equals("error1")) {
connectError = true;
}
if (data.equals("error2")) {
sendError = true;
}
}
}
public class NetworkServiceV3_1 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV3 networkClientV3 = new NetworkClientV3(address);
networkClientV3.initError(data);
try {
networkClientV3.connect();
networkClientV3.send(data);
} catch (ConnectExceptionV3 e) {
System.out.println("[연결 오류] 주소 : " + e.getAddress() + ", 메시지 : " + e.getMessage());
} catch (SendExceptionV3 e2) {
System.out.println("[전송 오류] 전송 데이터 : " + e2.getData() + ", 메시지 : " + e2.getMessage());
} finally {
networkClientV3.disconnect();
}
}
}
핵심
예외를 계층화하고 다양하게 만들어 세밀한 동작들을 할 수 있도록 처리할 수 있다. 특정 분류 공통 예외들도 한 번에 catch
로 잡아서 처리할 수 있다.
public class NetworkServiceV3_2 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV3 networkClientV3 = new NetworkClientV3(address);
networkClientV3.initError(data);
try {
networkClientV3.connect();
networkClientV3.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 {
networkClientV3.disconnect();
}
}
}
throws Exception
Exception
은 애플리케이션에서 일반적으로 다루는 모든 예외의 부모이다. 이렇게 한 줄만 넣으면 모든 예외를 다 던질 수 있게 된다. 코드가 상당히 깔끔해지지만 이 방법은 치명적인 문제를 가지고 있다.
throws Exception 문제
throws
에 던질 대상을 일일이 다 명시해야 한다.(체크 예외 지옥)예외 처리 지옥을 방지하기 위해 Service에서 처리하는 방법말고 하나의 공통 예외 처리를 따로 만들어서 한 번에 관리한다.
언체크 예외 사용 시나리오
package me.jangwoojin.exception;
import java.util.Scanner;
public class MainV4 {
public static void main(String[] args) {
NetworkServiceV4 networkServiceV4 = new NetworkServiceV4();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("전송할 문자: ");
String string = scanner.nextLine();
if (string.equals("exit")) {
break;
}
try {
networkServiceV4.sendMessage(string);
} 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 sendExceptionV4) {
System.out.println("[전송 오류] 전송 데이터 : " + sendExceptionV4.getSendData());
}
}
}
instanceOf()
와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.Slf4j
같은 로그 라이브러리를 사용해서 콘솔과 특정 파일에 함께 결과를 출력한다.애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다. 따라서 finally 구문을 반드시 사용해야 한다. 자바7에서 try with resources
라는 편의 기능을 도입했는데 이 기능을 사용하려면 AutoCloseable
인터페이스를 구현해야 한다.
try
에서 외부 자원을 사용하고 try
가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 try with resources
기능을 자바7에서 도입했다. try
가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.
해당 인터페이스를 구현하면 try
가 끝나는 시점에 재정의한 close()
가 자동으로 호출된다.
close()
에 종료 시점에 자원을 반납하는 방법을 여기서 정의하면 된다.
Try with resources 장점
finally
블록을 적지 않거나 finally
블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다. close()
호출이 필요 없이 코드가 더 간결하고 쉬워진다.try
→ catch
→ finally
로 catch
이후에 자원을 반납했는데 Try with resources 구문은 try
이 블럭이 끝나면 즉시 close()
를 호출한다.client
변수의 스코프가 try
블럭 안으로 한정된다.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(address + " 서버에 데이터 전송 실패", data);
}
System.out.println(address + " 서버에 데이터 전송 성공: " + data);
}
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
// 사용자가 입력한 데이터를 기반으로 오류 체크
public void initError(String data) {
if (data.equals("error1")) {
connectError = true;
}
if (data.equals("error2")) {
sendError = true;
}
}
@Override
public void close() {
System.out.println("NetworkClientV5.close");
disconnect();
}
}