[김영한의 실전 자바 - 중급 1편] 10. 예외 처리2 - 실습

Turtle·2024년 7월 10일
0
post-thumbnail

🙄예외 처리 도입1 - 시작

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()를 호출해서 연결을 해제해야 한다.

🙄예외 처리 도입2 - 예외 복구

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()를 호출해서 연결을 해제해야 한다.

🙄예외 처리 도입3 - 정상, 예외 흐름 분리

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()를 호출해서 연결을 해제해야 한다. 외부 연결과 같은 자바 외부 자원은 자동으로 해제가 되지 않는다. 따라서 외부 자원을 사용한 후에는 반드시 연결을 해제해주어야만 한다.

🙄예외 처리 도입4 - 리소스 반환 문제

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문에서 잡을 수 없어 서버 연결 해제가 정상적으로 수행되지 않는다.

🙄예외 처리 도입5 - finally

try {
	// 정상 흐름
} catch {
	// 예외 흐름
} finally {
	// 반드시 호출해야 하는 마무리 흐름
}

finally 실행 시점

  • 정상 흐름 → finally
  • 예외 catch → 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를 사용할 수 있다.

🙄예외 계층1 - 시작

예외는 단순히 오류 코드로 분류하는 것이 아니라 예외를 계층화해서 다양하게 만들면 세밀하게 예외를 처리할 수 있다.

자바에서 예외는 객체이다. 부모 예외를 잡거나 던지면 그 하위 자식 예외도 함께 잡거나 던질 수 있는 것이다.

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();
		}
	}
}

🙄예외 계층2 - 활용

핵심
예외를 계층화하고 다양하게 만들어 세밀한 동작들을 할 수 있도록 처리할 수 있다. 특정 분류 공통 예외들도 한 번에 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();
		}
	}
}

🙄실무 예외 처리 방안1 - 설명

throws Exception
Exception은 애플리케이션에서 일반적으로 다루는 모든 예외의 부모이다. 이렇게 한 줄만 넣으면 모든 예외를 다 던질 수 있게 된다. 코드가 상당히 깔끔해지지만 이 방법은 치명적인 문제를 가지고 있다.

throws Exception 문제

  • 처리할 수 없는 예외 : 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다.
  • 체크 예외의 부담 : 처리할 수 없는 예외는 밖으로 던져야 한다. 체크 예외이므로 throws에 던질 대상을 일일이 다 명시해야 한다.(체크 예외 지옥)

예외 처리 지옥을 방지하기 위해 Service에서 처리하는 방법말고 하나의 공통 예외 처리를 따로 만들어서 한 번에 관리한다.

언체크 예외 사용 시나리오

🙄실무 예외 처리 방안2 - 구현

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());
		}
	}
}
  • ✔️exceptionHandler()
    • 해결할 수 없는 예외가 발생하면 사용자에게는 시스템 내에 알 수 없는 문제가 발생했다고 알리는 것이 좋다.
      • 사용자가 디테일한 오류 코드나 오류 상황을 이해할 필요는 없다. 사용자가 DB 연결이 안되서 오류가 발생한 것인지 아니면 네트워크에 문제가 있어 오류가 발생한 것인지 알 필요가 없다는 것이다.
    • 개발자가 빨리 문제를 찾고 디버깅할 수 있도록 오류 메시지는 남긴다.
    • 예외도 객체이므로 필요하면 instanceOf()와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.
  • ✔️e.printStackTrace()
    • 예외 메시지와 스택 트레이스를 출력할 수 있다.
    • 예외가 발생한 지점을 역추적 가능
    • 실무에서는 Slf4j같은 로그 라이브러리를 사용해서 콘솔과 특정 파일에 함께 결과를 출력한다.

🙄try-with-resources

애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다. 따라서 finally 구문을 반드시 사용해야 한다. 자바7에서 try with resources라는 편의 기능을 도입했는데 이 기능을 사용하려면 AutoCloseable 인터페이스를 구현해야 한다.

try에서 외부 자원을 사용하고 try가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 try with resources 기능을 자바7에서 도입했다. try가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.

해당 인터페이스를 구현하면 try가 끝나는 시점에 재정의한 close()가 자동으로 호출된다.
close()에 종료 시점에 자원을 반납하는 방법을 여기서 정의하면 된다.

Try with resources 장점

  • 리소스 누수 방지 : 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally 블록을 적지 않거나 finally 블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다.
  • 코드 간결성 및 가독성 향상 : 명시적인 close() 호출이 필요 없이 코드가 더 간결하고 쉬워진다.
  • 조금 더 빠른 자원 해제 : trycatchfinallycatch 이후에 자원을 반납했는데 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();
	}
}

0개의 댓글