주제 : 김영한님의 자바 중급 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)를 반환 값으로 리턴해서 어떤 오류가 발생했는지 구분했다.- 여기서는 어떤 종류의 오류가 발생했는지 구분하기 위해 예외 안에 오류 코드를 보관한다.
오류 메시지
- 오류 메시지(
messsage)에는 어떤 오류가 발생했는지 개발자가 보고 이해할 수 있는 설명을 담아둔다.- 오류 메시지는 상위 클래스인
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; } } }
- 기존의 코드와 대부분 같지만, 오류가 발생했을 때 오류 코드를 반환하는 것이 아니라 예외를 던진다.
- 따라서, 반환 값을 사용하지 않아도 된다. 여기서는 반환 값을
void로 처리한다.- 이전에는 반환 값으로 성공, 실패 여부를 확인해야 했지만, 예외 처리 덕분에 메서드가 정상 종료되면 성공이고, 예외가 던져지면 예외를 통해 실패를 확인할 수 있다.
- 오류가 발생하면, 예외 객체를 만들고 거기에 오류 코드와 오류 메시지를 담아둔다. 그리고 만든 예외 객체를
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("프로그램을 정상 종료합니다."); } }
- 여기서도 예외를 처리하지 않고,
throws를 통해 밖으로 던진다.helloe이면 잘 작동한다.error1이면 연결 실패가 발생한다.- 모든 곳에서 발생한 예외를 잡지 않았기 때문에 결과적으로
main()밖으로 예외가 던져진다.main()밖으로 예외가 던져지면 예외 메시지와 예외를 추적할 수 있는 스택 트레이스를 출력하고 프로그램을 종료한다.
남은 문제
disconnect()를 호출해서 연결을 해제해야 한다.이번에는 예외를 잡아서 예외 흐름을 정상 흐름으로 복구해보자
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()메서드를 정상적으로 빠져나간다.
해결된 문제
남은 문제
disconnect() 를 호출해서 연결을 해제해야 한다.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블럭으로 명확하게 분리할 수 있다.
해결된 문제
try , catch 구조 덕분에 정상 흐름은 try 블럭에 모아서 처리하고, 예외 흐름은 catch 블럭에 별도로 모아서 처리할 수 있었다. 남은 문제
disconnect() 를 호출해서 연결을 해제해야 한다.현재 구조에서 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(); } }
- 이 코드를 보면 예외 처리가 끝난 다음에 정상 흐름의 마지막에
client.disconnect()를 호출했다.- 이렇게 하면 예외가 모두 처리되었기 때문에
client.disconnect()가 항상 호출될 것 같다.
하지만 지금과 같은 방식에는 큰 문제가 있다.
바로 catch 에서 잡을 수 없는 예외가 발생할 때이다.
-> 결국 새로운 대안이 필요하다.
자바는 어떤 경우라도 반드시 호출되는 finally기능을 제공한다.
try { //정상 흐름 } catch { //예외 흐름 } finally { //반드시 호출해야 하는 마무리 흐름 }
try ~ catch ~ finally구조는 정상 흐름, 예외 흐름, 마무리 흐름을 제공한다.- 여기서
try를 시작하기만 하면,finally코드 블럭은 반드시 호출된다.try,catch안에서 잡을 수 없는 예외가 발생해도finally는 반드시 호출된다.finally코드 블럭이 끝나고 나서 이후에 예외가 밖으로 던져짐
public class NetworkServiceV2_5 { public void sendMessage(String data){ String address = "https://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
try , catch 안에서 처리할 수 없는 예외가 발생해도 finally 는 반드시 호출된다.
정리
자바 예외 처리는 try ~ catch ~ finally 구조를 사용해서 처리할 수 있다. 덕분에 다음과 같은 이점이 있다.
예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.
NetworkClientExceptionV3:NetworkClient에서 발생하는 모든 예외는 이 예외의 자식이다.ConnectExceptionV3: 연결 실패시 발생하는 예외이다. 내부에 연결을 시도한address를 보관한다.SendExceptionV3: 전송 실패시 발생하는 예외이다. 내부에 전송을 시도한 데이터인sendData를 보관한다.
이렇게 예외를 계층화하면 다음과 같은 장점이 있다.
NetworkClientExceptionV3 예외를 잡으면 그 하위인 ConnectExceptionV3 ,SendExceptionV3 예외도 함께 잡을 수 있다.ConnectExceptionV3 , SendExceptionV3 와 같은 하위 예외를 잡아서 처리하면 된다.public class NetworkClientExceptionV3 extends Exception{ public NetworkClientExceptionV3(String message) { super(message); } }
NetworkClient에서 발생하는 모든 예외는 이 예외를 부모로 하도록 설계한다.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; } }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; } }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: 전송 실패시 발생하는 예외 따로 만들었다.- 연결 관련 오류 발생하면
ConnectExceptionV3를 던지고, 전송 관련 오류가 발생하면SendExceptionV3를 던진다.
public class NetworkServiceV3_1 { public void sendMessage(String data) { String address = "https://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(); } } }
- 예외 클래스를 각각의 예외 상황에 맞추어 만들면, 각 필요에 맞는 예외를 잡아서 처리할 수 있다.
- 예를 들면
e.getAddress(),e.getSendData()와 같이 각각의 예외 클래스가 가지는 고유의 기능을 활용할 수 있다.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("프로그램을 정상 종료합니다."); } }
- 실행 결과를 보면
ConnectExceptionV3,SendExceptionV3이 발생한 각각의 경우에 출력된 오류 메시지가 다른 것을 확인할 수 있다.
NetworkClientV3 에서 수 많은 예외를 발생한다고 가정해보자. 이런 경우 모든 예외를 하나하나 다 잡아서 처리하는 것은 상당히 번거로울 것이다. 그래서 다음과 같이 예외를 처리하도록 구성해보자.public class NetworkServiceV3_2 { public void sendMessage(String data) { String address = "https://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(); } } }
예외 공통 처리
처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 된다. 어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템에 문제가 있습니다라고 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다. 이런 부분은 공통 처리가 가능하다.
실습한 내용을 언체크 예외로 만들고,해결할 수 없는 예외들을 공통으로 처리해보자.

NetworkClientExceptionV4 는 언체크 예외인 RuntimeException 을 상속 받는다.NetworkClientExceptionV4 와 자식은 모두 언체크(런타임) 예외가 된다.public class NetworkClientExceptionV4 extends RuntimeException{ public NetworkClientExceptionV4(String message){ super(message); } }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; } }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 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; } } }public class NetworkServiceV4 { public void sendMessage(String data) { String address = "https://example.com"; NetworkClientV4 client = new NetworkClientV4(address); client.initError(data); try { client.connect(); client.send(data); } finally { client.disconnect(); } } }
NetworkServiceV4는 발생하는 예외인ConnectExceptionV4,SendExceptionV4를 잡아도 해당 오류들을 복구할 수 없다. 따라서 예외를 밖으로 던진다.- 언체크 예외이므로
throws를 사용하지 않는다.- 사실
NetworkServiceV4개발자 입장에서는 해당 예외들을 복구할 수 없다. 따라서 해당 예외들을 생각하지 않는 것이 더 나은 선택일 수 있다. 해결할 수 없는 예외들은 다른 곳에서 공통으로 처리된다.- 이런 방식 덕분에
NetworkServiceV4는 해결할 수 없는 예외 보다는 본인 스스로의 코드에 더 집중할 수 있다. 따라서 코드가 깔끔해진다.
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()); } } }공통 예외 처리
여기에 예외를 공통으로 처리하는 부분이 존재한다.
Exception을 잡아서 지금까지 해결하지 못한 모든 예외를 여기서 공통으로 처리한다.Exception을 잡으면필요한 모든 예외를 잡을 수 있다.- 예외도 객체이므로 공통 처리 메서드인
exceptionHandler(e)에 예외 객체를 전달한다.
exceptionHandler()
instanceof 와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.e.printStackTrace()
e.printStackTrace(System.out) 을 사용해서 표준 출력으로 보냈다.e.printStackTrace() 를 사용하면 System.err 이라는 표준 오류에 결과를 출력한다.System.err 로 출력하면 출력 결과를 빨간색으로 보여준다.애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다. try 에서 외부 자원을 사용하고, try 가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 Try with resources라는 편의 기능을 자바 7에서 도입했다. 이름 그대로 try 에서 자원을 함께 사용한다는 뜻이다. 여기서 자원은 try 가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.
이 기능을 사용하려면 먼저 AutoCloseable 인터페이스를 구현해야 한다.
public interface AutoCloseable { void close() throws Exception; }이 인터페이스를 구현하면 Try with resources를 사용할 때
try가 끝나는 시점에close()가 자동으로 호출된다.
그리고 다음과 같이 Try with resources 구문을 사용하면 된다.try (Resource resource = new Resource()) {
// 리소스를 사용하는 코드
}
이제 구현 코드를 만들어보자.
기존 코드를 Try with resources 구문을 사용하도록 변경해보자.
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(); } }
implements AutoCloseable 을 통해 AutoCloseable 을 구현한다.AutoCloseable 인터페이스가 제공하는 이 메서드는 try 가 끝나면 자동으로 호출된다. 종료 시점에 자원을 반납하는 방법을 여기에 정의하면 된다. 참고로 이 메서드에서 예외를 던지지는 않으므로 인터페이스의throws Exception 은 제거했다.public class NetworkServiceV5 { public void sendMessage(String data) { String address = "https://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블럭 없이try블럭만 있어도close()는 호출된다.- 여기서
catch블럭은 단순히 발생한 예외를 잡아서 예외 메시지를 출력하고, 잡은 예외를throw를 사용해서 다시 밖으로 던진다.
`