일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 mvc2 - 로그인 처리
- 게시글 목록 api
- jpa 활용2 - api 개발 고급
- 코드로 시작하는 자바 첫걸음
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch8
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch13
- @Aspect
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch4
- 스프링 고급 - 스프링 aop
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch6
- 스프링 db2 - 데이터 접근 기술
- 스프링 mvc2 - 타임리프
- 스프링 db1 - 스프링과 문제 해결
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch1
- 스프링 입문(무료)
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch11
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch2
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch9
- Today
- Total
나구리의 개발공부기록
예외 처리 - 실습, 예외 처리 도입(시작, 예외 복구, 정상, 예외 흐름 분리, 리소스 반환 문제, finally), 예외 계층(시작, 활용), 실무 예외 처리 방안(설명, 구현), try-with-resources 본문
예외 처리 - 실습, 예외 처리 도입(시작, 예외 복구, 정상, 예외 흐름 분리, 리소스 반환 문제, finally), 예외 계층(시작, 활용), 실무 예외 처리 방안(설명, 구현), try-with-resources
소소한나구리 2025. 1. 24. 18:40출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1.예외 처리 도입
1) 시작
(1) NetworkClientExceptionV2
- 앞서 만든 프로그램은 반환 값을 사용하여 예외를 처리했는데, 정상 흐름과 예외 흐름이 섞여있어 코드를 한눈에 이해하기 어려웠음
- 오히려 예외 흐름이 더 많은 코드 분량을 차지하여 가장 중요한 정상 흐름이 한눈에 들어오지 않았던 문제를 자바 예외 처리를 도입하여 문제를 해결
- 예외도 객체이기 때문에 필요한 필드와 메서드를 가질 수 있음
- 오류 코드: 어떤 종류의 오류가 발생했는지 구분하기 위해 예외 안에 필드로 오류 코드를 보관
- 오류 메시지: 상위 클래스인 Throwable에서 기본으로 제공하는 message 기능을 사용하여 어떤 오류가 발생했는지 개발자가 이해할 수 있는 설명을 담아줌
package exception.ex2;
public class NetworkClientExceptionV2 extends Exception {
private String errorCode;
public NetworkClientExceptionV2(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
(2) NetworkClientV2
- 기존의 NetworkClientV2와 대부분 코드가 비슷하지만 connect()와 send()에서 오류가 발생했을 때 오류 코드를 return 하는 것이 아니라 예외를 던짐
- 반환값을 사용하지 않아도 되기 때문에 메서드들을 모두 반환값이 없는 void로 처리함
- 이전에는 반환 값으로 성공, 실패 여부를 확인해야 했지만 예외 처리 덕분에 메서드가 정상 종료되면 성공이고 예외가 던져지면 예외를 통해 실패를 확인할 수 있게됨
- 오류가 발생하면 예외 객체를 만들고 거기에 오류 코드와 오류 메시지를 담아둔 후 예외를 throw로 던짐
package exception.ex2;
public class NetworkClientV2 {
// ... 기존 코드 동일 생략
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);
}
// ... 기존 코드 동일 생략
}
(3) NetworkServiceV2_1
- 여기서는 예외를 별도로 처리하지 않고 throws를 통해 밖으로 던짐
package exception.ex2;
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();
}
}
(4) MainV2
- NetworkServiceV2_1을 사용하는 것만 다르고 기존 코드와 동일함
- Service에서 예외를 처리하지않고 던졌으므로 여기까지 예외가 올라오며, 여기서도 밖으로 예외를 던짐
- 프로그램을 실행 후 error1이나 error2를 입력하면 프로그램이 예외가 발생하고 예외정보, 예외메시지, 스택 트레이스를 출력하며 프로그램이 종료가 됨
package exception.ex2;
import java.util.Scanner;
public class MainV2 {
public static void main(String[] args) throws NetworkClientExceptionV2 {
NetworkServiceV2_1 networkService = new NetworkServiceV2_1();
// ... 기존 코드와 동일 생략
}
}
/* 정상 출력
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
/* 예외 발생 1
전송할 문자: error1
Exception in thread "main" exception.ex2.NetworkClientExceptionV2: connectError
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:16)
*/
/* 예외 발생 2
전송할 문자: error2
http://example.com 서버 연결 성공
Exception in thread "main" exception.ex2.NetworkClientExceptionV2: sendError
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:16)
*/
(5) 남은 문제
- 예외 처리를 도입했지만 예외 복구를 하지않았으므로 예외가 발생하면 프로그램이 종료됨
- 사용 후에는 반드시 disconnect()를 호출해서 연결을 해제해야 함
2) 예외 복구
(1) NetworkserviceV2_2
- 예외를 잡아서 예외 흐름을 정상 흐름으로 복구
- connect(), send()에서 예외가 발생할 수 있는 곳을 try - catch를 사용하여 NetworkClientEceptionV2 예외를 잡고 예외를 잡으면 오류코드와 예외 메시지를 출력
- 예외를 잡아서 처리했기 때문에 이후에는 정상흐름으로 복귀하며 여기서는 return을 사용하여 sendMessage()메서드를 빠져나감
package exception.ex2;
public class NetworkServiceV2_2 {
public void sendMessage(String 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();
}
}
(2) MainV2 - 수정
- NetWorkServiceV2_2로 사용하도록 변경한 뒤 error1과 error2를 입력해보면 catch문의 코드를 입력하고 프로그램이 정상 흐름으로 복구되어 정상적으로 종료되는 것을 확인할 수 있음
/* 실행 결과
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: error1
[오류] 코드: http://example.com서버 연결 실패, 메시지: connectError
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: http://example.com서버 데이터 전송 실패: error2, 메시지: sendError
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(3) 해결된 문제와 남은 문제
- 해결된 문제: 예외를 잡아서 처리했으므로 예외가 복구되고 프로그램도 계속 수행할 수 있음
- 남은 문제: 예외를 처리했지만 Service의 코드를 보면 정상 흐름과 예외흐름이 섞여있어 코드를 읽기 어렵고, 사용 후 반드시 disconnect()를 호출해서 연결을 해제해야하는 요구사항을 만족하지 못했음
3) 정상, 예외 흐름 분리
(1) NetworkServiceV2_3
- try-catch 기능을 제대로 사용하여 정상 흐름과 예외 흐름이 섞여있는 문제를 해결
- 하나의 try안에 프로그램의 정상 흐름의 코드만 담겨있고 예외처리는 catch 블럭에서 해결하도록 하면 정상 흐름은 try 블럭에, 예외흐름은 catch 블럭으로 명확하게 분리가 됨
package exception.ex2;
public class NetworkServiceV2_3 {
public void sendMessage(String data) {
// ... 기존 코드 동일 생략
// 하나의 try 안에 정상 흐름을 모두 담음
try {
client.connect();
client.send(data);
client.disconnect();
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
}
}
}
(2) MainV2 - 수정
- NetWorkServiceV2_3으로 사용하도록 변경 후 실행해보면 NetworkServiceV2_2를 사용했던 결과와 동일하게 출력되고 있음
(3) 해결된 문제와 남은 문제
- 해결된 문제: 자바의 예외 처리 메커니즘과 try, catch구조 덕분에 정상 흐름은 try블럭에 모아서 처리하고 예외 흐름은 catch 블럭에 모아서 처리하여 정상 흐름과 예외 흐름을 명확하게 분리하여 코드를 더 쉽게 읽을 수 있게 되었음
- 남은 문제: 아직 사용 후 반드시 disconnect()를 호출해서 연결을 해제해야하는 요구사항을 만족시키지 못했음
4) 리소스 반환 문제
(1) NetworkServiceV2_4
- 정상 흐름의 마지막에 client.disconnect()를 호출하도록 변경하여, 정상흐름은 물론 예외가 발생해도 정상흐름으로 복구되기 때문에 disconnect()가 호출 될 수 있음
package exception.ex2;
public class NetworkServiceV2_4 {
public void sendMessage(String data) {
// ... 기존 코드 동일 생략
try {
client.connect();
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
}
client.disconnect();
}
}
(2) MainV2 - 수정
- NetworkServiceV2_4를 사용하도록 변경 후 프로그램을 실행해보면 요구사항대로 예외가 발생하여도, 프로그램이 정상적으로 실행되어도 모두 서버가 연결이 해제되었다는 문구가 출력되는 것을 확인 할 수 있음
- 그러나 지금과 같은 방식은 catch에서 잡을 수 없는 예외가 발생했을 때 문제가 발생함
/* 실행 결과
전송할 문자: error1
[오류] 코드: http://example.com서버 연결 실패, 메시지: connectError
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: http://example.com서버 데이터 전송 실패: error2, 메시지: sendError
http://example.com 서버 연결 해제
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: hello
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(3) NetworkClientV2 - 수정 및 MainV2 실행 결과
- NetworkClientV2의 send()메서드에서 자바가 기본으로 제공하는 RuntimeException으로 변경하여 중간에 원인 모를 예외가 발생했다고 가정하도록 코드를 변경
- 프로그램을 실행해보면 catch에서 NetworkClientExceptionV2는 잡을 수 있지만 새로 등장한 RuntimeException은 잡을 수 없기 때문에 error1를 입력했을 때는 오류를 잡지만 error2를 입력하면 예외를 잡지못하고 main()밖으로 예외가 던져지게됨
- 결국 error2일 때는 disconnect()는 호출되지 않음
package exception.ex2;
public class NetworkClientV2 {
// ... 기존 코드 동일 생략
public void send(String data) throws NetworkClientExceptionV2 {
if (sendError) {
// throw new NetworkClientExceptionV2("sendError", address + "서버 데이터 전송 실패: " + data);
throw new RuntimeException("runEx"); // 중간에 전혀 예상 못한 다른 예외가 발생했다고 가정
}
System.out.println(address + " 서버에 데이터 전송: " + data);
}
// ... 기존 코드 동일 생략
}
/* MainV2 실행 결과
전송할 문자: error1
[오류] 코드: http://example.com서버 연결 실패, 메시지: connectError
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
Exception in thread "main" java.lang.RuntimeException: runEx
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:24)
at exception.ex2.NetworkServiceV2_4.sendMessage(NetworkServiceV2_4.java:13)
at exception.ex2.MainV2.main(MainV2.java:19)
*/
(4) 정리
- 사용 후에 반드시 disconnect()를 호출하여 연결 해제를 보장하는 것은 정상적인 상황과 예외 상황 그리고 어디선가 모르는 예외를 밖으로 던지는 상황을 모두 고려해야하기 때문에 쉽지 않음
- 지금과 같은 구조로는 항상 코드를 disconnect() 코드를 호출하는 것도 어렵지만 꼼꼼히 개발을 한다고해도 실수로 놓칠 가능성이 높으므로 새로운 대안이 필요함
** 참고
- 다음 예제를 위해 NetworkClientV2에서 RuntimeException을 발생시키도록 변경한 부분을 기존의 NetworkClientExceptionV2를 발생시키도록 원상 복구 후 진행
5) finally
(1) finally
- 자바는 어떤 경우라도 반드시 호출되는 finally 기능을 제공함
- try - catch - finally 구조는 정상 흐름, 예외 흐름, 마무리 흐름을 제공하며 try를 시작하기만하면 무조건 finally 코드 블럭은 반드시 호출됨
- try - catch 블럭에서 잡을 수 없는 예외가 발생해도 finally는 반드시 호출되기 때문에 주로 try에서 사용한 자원을 해제할 때 사용함
- 흐름 정리
- 1. 정상 흐름 -> finally
- 2. 예외 catch -> finally
- 3. 예외 던짐 -> finally, finally 코드 블럭이 끝난 이후에 예외가 밖으로 던져짐
try {
// 정상 흐름
} catch {
// 예외 흐름
} finally {
// 반드시 호출해야 하는 마무리 흐름
}
(2) NetworkServiceV2_5
- try - catch - finally를 적용하여 finally에 disconnect()를 호출하도록 수정
package exception.ex2;
public class NetworkServiceV2_5 {
public void sendMessage(String data) {
// ... 기존 코드 동일 생략
try {
client.connect();
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
} finally {
client.disconnect();
}
}
}
(3) MainV2 - 수정
- NetworkServiceV2_5를 사용하도록 변경 후 프로그램을 실행해보면 요구사항대로 예외를 처리 후 서버 연결이 해제되는 출력문을 확인할 수 있음
/* 실행 결과
전송할 문자: error1
[오류] 코드: http://example.com서버 연결 실패, 메시지: connectError
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[오류] 코드: http://example.com서버 데이터 전송 실패: error2, 메시지: sendError
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(4) NetworkClientV2 - 수정 및 MainV2 재실행
- NetworkClientV2의 send()메서드에서 NetworkClientExceptionV2가 아닌 RuntimeException을 던지도록 수정
- 다시 MainV2를 실행하고 error2를 입력해보면 NetworkServieV2_4에서는 연결이 해제되지 않고 바로 오류를 던졌지만, 여기에서는 서버 연결이 해제 되고난 후 오류가 밖으로 던져지는 것을 확인할 수 있음
/* 실행 결과
전송할 문자: error2
http://example.com 서버 연결 성공
http://example.com 서버 연결 해제
Exception in thread "main" java.lang.RuntimeException: runEx
at exception.ex2.NetworkClientV2.send(NetworkClientV2.java:24)
at exception.ex2.NetworkServiceV2_5.sendMessage(NetworkServiceV2_5.java:12)
at exception.ex2.MainV2.main(MainV2.java:20)
*/
(5) try - finally
- 만약 예외를 직접 잡아서 처리할 일이 없다면 catch문을 생략하고 try { .... } finally { ... } 처럼 작성해도되며 이 경우에도 예외가 밖으로 던져일 경우 finally호출이 보장됨
(6) 정리
- 자바 예외 처리는 try - catch - finally 구조를 사용하여 처리할 수 있음
- 이 덕분에 정상 흐름과 예외 흐름을 분리해서, 코드를 읽기 쉽게 만들며 사용한 자원을 항상 반환할 수 있도록 보장해주는 장점이 있음
2. 예외 계층
1) 시작
(1) 예외를 계층화
- 예외를 단순히 오류 코드로 분류하는 것이 아니라 예외를 계층화해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있음
- NetworkClientExceptionV3: NetworkClient에서 발행하는 모든 예외는 이 예외의 자식으로 설정
- ConnectExceptionV3: 연결 실패시 발생하는 예외이며 내부 연결을 시도한 address를 보관
- SendExceptionV3: 전송 실패 발생하는 예외로 내부에 전송을 시도한 데이터인 sendData를 보관
- 이렇게 예외를 계층화 하면 부모 예외를 잡거나 던질 때 자식 예외도 함께 잡거나 던질 수 있게되고, 특정 예외를 잡아서 처리하고 싶을 때는 하위 예외를 잡아서 처리하는 등으로 다양한 예외처리를 할 수 있음
(2) exception 패키지
- 예외를 구분하기 쉽도록 exception 패키지에 모아서 작성
- Exception 예외를 상속받는 NetworkClientExceptionV3예외와, NetworkClientExceptionV3예외를 상속받는 ConnectExceptionV3와 SendExceptionV3가 있음
- 예외 메시지 기능은 Throwable에 있는 기능을 사용하고, ConnectExceptionV3에는 address, SendExceptionV3에는 sendData를 보관하는 기능이 있음
- NetworkClien에서 발행하는 모든 예외는 이 예외를 부모로 하도록 설계하였고, 그 하위에 연결 실패시 발생하는 ConnectExceptionV3와 전송 실패 시 발생하는 SendExceptionV3가 있도록 예외 계층이 설계됨
package exception.ex3.exception;
public class NetworkClientExceptionV3 extends Exception {
public NetworkClientExceptionV3(String message) {
super(message);
}
}
package exception.ex3.exception;
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;
}
}
package exception.ex3.exception;
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;
}
}
(3) NetworkClientV3
- 예외는 별도의 패키지에 정의되어있으므로 import가 필요함
- connect()와 send()에서 발생하는 오류를 하위 예외인 ConnectExceptionV3와 SendExceptionV3로 각각 던지도록 설정
- 이렇게 하면 오류 코드로 어떤 문제가 발생했는지 이해하는 것이 아니라 예외 그 자체로 어떤 오류가 발생했는지 알 수 있게 되고 연결 오류가 발생하면 ConnectExceptionV3를, 전송 관련 오류가 발생하면 SendExceptionV3를 던지게 됨
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkClientV3 {
// ... 기존 코드 동일 생략
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);
}
// ... 기존 코드 동일 생략
}
(4) NetworkServieV3_1
- NetworkClientV3의 connect()와 send()메서드를 호출하면 각각의 예외가 던져지고, 예외를 처리할 때 필요에 맞는 예외를 잡아서 처리할 수 있음
- catch (ConnectExceptionV3 e): 연결 예외를 잡고, 해당 예외가 제공하는 기능인 getAddress()를 사용하여 정보를 출력
- catch (SendExceptionV3 e): 데이터 전송 예외를 잡고, 해당 예외가 제공하는 기능인 getSendData()를 사용하여 정보를 출력
package exception.ex3;
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();
}
}
}
(5) MainV3
- MainV2를 복사하여 NetworkServiceV3_1를 사용하도록 변경하고 프로그램을 실행해보면 error1을 입력했을 때는[연결 오류]라고 라고 출력되고, error2를 입력 하면 [전송 오류]로 출력되며 각각 상황에 맞게 출력되는 데이터가 다른것을 확인할 수 있음
/* 실행 결과
전송할 문자: error1
[연결 오류] 주소: http://example.com, 메시지: http://example.com서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[전송 오류] 전송 데이터: error2, 메시지: http://example.com서버 데이터 전송 실패: error2
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
2) 활용
(1) 예외 처리 활용
- NetworkClientV3에서 수 많은 예외를 발생한다고 가정했을 때 모든 예외를 하나하나 다 잡아서 처리하는 것은 매우 번거로운 일이므로 아래처럼 예외를 처리하도록 구성
- ConnectExceptionV3: 연결시 발생하는 오류는 매우 중요한 오류라고 가정하여 ConnectExceptionV3가 발생하면 명확하게 '[연결 오류] ...' 처럼 메시지를 명확하게 남기도록 설정
- NetworkClientV3의 나머지 예외: 그 외에 NetworkClientV3를 사용하면서 발생하는 나머지 예외는 단순히'[네트워크 오류] ... '처럼 메시지가 나오도록 설정
- 그 외의 예외가 발생하면 '[알 수 없는 오류] ...' 처럼 나오도록 설정
(2) NetworkServiceV3_2
- 제일 먼저 디테일한 오류인 ConnectExceptionV3를 catch로 예외처리
- 두번째로 NetworkClientExceptionV3를 예외처리하여 해당 예외와 그 자식 예외가 발생하면 해당 catch블럭에서 예외가 처리됨
- 세번째로 Exception 예외를 처리하여 Exception과 그 하위예외가 모두 처리됨
- 주의할 점은 예외가 발생했을 때 catch를 순서대로 실행하기 때문에 더 디테일한 자식을 먼저 잡아서 처리되도록해야 계층적인 예외를 정상적으로 처리할 수 있음
- 만약 Exception 예외가 가장 먼저 처리된다면 이미 모든 예외를 먼저 처리해버리기 때문에 상황에 따라 예외처리를 할 수 없음
package exception.ex3;
public class NetworkServiceV3_2 {
public void sendMessage(String 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();
}
}
}
(3) MainV3 - 수정 및 실행
- NetworkServiceV3_2를 사용하도록 변경하고 실행해보면 예외 계층을 순차적으로 예외처리하여 error1은 연결 오류, error2는 [네트워크 오류]로 메시지가 출력되는 것을 확인할 수 있음
- 여기에서 추가적으로 throw new RuntimeException을 발생시키면 [알 수 없는 오류]라는 메시지가 출력되는 것을 확인할 수 있음
/* 실행 결과
전송할 문자: error1
[연결 오류] 주소: http://example.com, 메시지: http://example.com서버 연결 실패
http://example.com 서버 연결 해제
전송할 문자: error2
http://example.com 서버 연결 성공
[네트워크 오류] 메시지: http://example.com서버 데이터 전송 실패: error2
http://example.com 서버 연결 해제
전송할 문자: run
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송: run
[알수 없는 오류] 메시지: run
http://example.com 서버 연결 해제
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(4) 여러 예외를 한번에 잡는 기능
- catch 문에서 예외를 잡을 때 | 를 사용하여 여러 예외를 한번에 잡을 수 있음
- 그러나 이 경우에는 각 예외들의 공통 부모의 기능만 사용할 수 있으므로 ConnectExceptionV3에만 있는 get.Address()와 SendExceptionV3에만 있는 getSendData()는 사용할 수 없고 조상의 기능인 getMessage()만 사용할 수 있음
catch (ConnectExceptionV3 | SendExceptionV3 e) {
System.out.println("[연결 또는 전송 오류] 메시지: " + e.getMessage());
}
(5) 정리
- 예외를 계층화하고 다양하게 만들면 더 세밀한 동작들을 깔끔하게 처리할 수 있으며 특정 분류의 공통 예외들도 한번에 catch로 잡아서 처리할 수 있음
- 예외를 계층화 하는 작업은 절충이 필요한데, 예외가 너무 많아지면 프로그래밍 시 너무 복잡해질 수 있기 때문에 적절히 계층화 하고 애매한 부분은 공통으로 묶어서 두개를 섞어서 작업하는 것이 좋음
3. 실무 예외 처리 방안
1) 설명
(1) 처리할 수 없는 예외
- 상대 네트워크 서버에 문제가 발생하여 통신이 불가능하거나 데이터 베이스 서버에 문제가 발생하여 접속이 안되면 애플리케이션에서 연결 오류, 데이터베이스 접속 실패와 같은 예외가 발생함
- 이렇게 시스템 오류 때문에 발생한 예외들은 대부분 예외를 잡아도 해결할 수 있는 것이 없고 예외를 잡아서 다시 호출을 시도해도 같은 오류가 반복 될 뿐이며 서버가 복구되길 기다릴 수밖에 없음
- 이런 경우 고객에게 "현재 시스템에 문제가 있습니다"라는 오류 메시지나 오류 페이지를 보여주고 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록 오류에 대한 로그를 남겨두어야 함
(2) 체크 예외의 부담
- 체크 예외는 개발자가 실수로 놓칠 수 있는 예외들을 컴파일러가 체크해주기 때문에 오래전부터 많이 사용되었음
- 그러나 처리할 수 없는 예외가 많아지고 프로그램이 점점 복잡해지면서 체크 예외를 사용하는 것이 부담스러워짐
(3-1) 체크 예외 사용 시나리오
- 실무에서는 수 많은 라이브러리를 사용하고 다양한 외부 시스템과 연동하는데 사용하는 각각의 클래스들이 자신만의 예외를 모두 체크 예외로 만들어서 전달한다고 가정
- 이런 경우 Service는 호출하는 곳에서 던지는 체크 예외들을 처리해야 하거나 처리할 수 없다면 밖으로 던져야 함
- 그러나 앞서 설명했듯이 상대 네트워크 서버나, 데이터 베이스 서버가 문제가 발생한 경우 Service에서 예외를 잡아도 복구할 수 없으므로 Service에서도 밖으로 던지는 것이 더 나은 결정임
- 처리못하는 체크 예외를 모두 밖으로 던져야하는데, 라이브러리 늘어날 수록 던져야할 예외도 매우 많아지므로 개발자 입장에서는 매우 번거로운일임
- 만약 중간에 Facade라는 클래스가 있다고 하면 여기에서도 Service에서 넘어온 예외들을 복구 할 수 없으므로 Facade 클래스도 예외를 밖으로 던져야하는데, 이렇게 중간의 모든 클래스에 throws로 예외를 밖으로 던지는 지저분한 코드가 만들어짐
- 개발자가 본인이 다룰 수 없는 수 많은 체크 예외 지옥에 빠지게되어 결국 throws Exception로 모든 예외의 부모인 Exception을 던져서 본인과 그 하위의 Exceptin을 던지게 됨
- 이렇게 되면 이후에 예외가 추가되더라도 throws Exception 한줄로 해결되는 것 같지만 이방법에는 다른 체크 예외를 체크할 수 있는 기능이 무효화되는 치명적인 문제가 있음
- 즉, 중간에 중요한 체크 예외가 발생해도 컴파일러는 Exception을 던지기 때문에 문법에 맞다고 판단하여 컴파일 오류가 발생하지 않아 문제를 확인할 수가 없게는데, 이것은 체크 예외를 의도한 대로 사용하는 방법이 아님
- 꼭 필요한 경우가 아니면 Exception 자체는 밖으로 던지는 것은 좋지 않은 방법임
(3-2) 체크 예외 시나리오 문제 정리
- 처리할 수 없는 예외: 예외를 잡아서 복구할 수 있는 예외보다 복구 할 수 없는 예외가 더 많음
- 체크 예외의 부담: 처리할 수 없는 예외는 밖으로 던져야 하는데 체크 예외이기 때문에 throws에 던질 대상을 일일히 다 명시해야 함
- Service를 개발하는 개발자 입장에서 수 많은 라이브러리에서 쏟아지는 모든 예외를 다 다루기는 어려움
- 특히 본인이 해결할 수 없는 예외들을 다 다루고 싶지 않기 때문에 본인이 해결할 수 있는 예외만 잡아서 처리하고 본인이 해결할 수 없는 예외는 신경쓰지 않는것이 더 나은 선택일 수 있음
(4) 언체크(런타임) 예외 사용 시나리오
- 이번에는 Service에서 호출하는 클래스들이 언체크(런타임) 예외를 전달한다고 가정하면 네트워크 예외나 데이터베이스 예외는 잡아도 복구할 수 없으므로 밖으로 던져야하는데, 언체크 예외이므로 무시해도 자동으로 밖으로 던져짐
- 만약 사용하는 라이브러리가 늘어나서 언체크 예외가 늘어나도 본인이 처리할 수 있는 예외만 잡고 나머지는 자동으로 밖으로 던져지므로 throws로 예외선언을 늘리지 않아도 됨
(5) 예외 공통 처리
- 이렇게 처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 됨
- 어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템에 문제가 있다는 오류 메시지나 오류 페이지를 보여주고 문제 상황을 인지할 수 있도록 오류에 대한 로그를 남기도록하는 부분은 공통 처리가 가능함
2) 구현
(1) 예제 구조
- NetworkClientExceptionV4는 RuntimeException을 상속받으므로 NetworkClientExceptionV4과 자식은 모두 언체크(런타임) 예외가 됨
(2) exception 패키지
- 런타임 예외인 NetworkClientExceptionV4와 그 자식 예외인 ConnectExceptionV4, SendExceptionV4가 있음
package exception.ex4.exception;
public class NetworkClientExceptionV4 extends RuntimeException {
public NetworkClientExceptionV4(String message) {
super(message);
}
}
package exception.ex4.exception;
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;
}
}
package exception.ex4.exception;
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;
}
}
(3) NetworkClientV4
- NetworkClientV3를 복사하여 NetworkClientV4를 사용하도록 변경하면 ConnectExceptionV4와 SendExceptionV4를 발생 시켜도 런타임 예외이기 때문에 throws를 하지 않아도 됨
package exception.ex4;
public class NetworkClientV4 {
// ... 기존 코드 동일 생략
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);
}
// ... 기존 코드 동일 생략
}
(4) NetworkServiceV4
- NetworkServiceV4는 발생하는 예외인 ConnectExceptionV4나 SendExceptionV4를 잡아도 해당 오류를 복구할 수 없으므로 예외를 밖으로 던지는데, 언체크 예외이므로 여기서도 throws를 사용하지 않아도 됨
- 이렇게 해결할 수 없는 예외들은 다른곳에서 공통으로 처리되며 이런 방식 덕분에 NetworkServiceV4는 해결할 수 없는 예외보다는 본인 스스로의 코드에 더 집중할 수 있어 코드가 깔끔해짐
package exception.ex4;
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();
}
}
}
(5) MainV4
- networkService.sendMessage()를 호출하여 발생하는 모든 예외를 잡아서 공통으로 처리하도록 try - catch를 이용하여 Exception을 잡아서 예외처리를 진행
- 예외를 공통으로 처리하는 메서드인 exceptionHandler(e)를 만들어서 예외 객체를 전달하여 처리되도록 하면 모든 예외가 공통으로 처리되며 정상적으로 프로그램이 동작하는 것을 확인할 수 있음
- error2를 입력하면 [전송 오류] 라는 메시지가 출력되도록 동작하는 로직도 정상작으로 동작하고 있음
package exception.ex4;
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());
}
}
}
/* 실행 결과
전송할 문자: 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:20)
전송할 문자: 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:20)
[전송 오류] 전송 데이터: error2 발생했다
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(5) 공통 예외 처리 메서드 - exceptionHandler() 설명
- 해결할 수 없는 예외가 발생하면 사용자는 디테일한 오류 코드나 오류 상황까지 모두 이해할 필요가 없으므로 사용자에게는 시스템 내에 알 수 없는 문제가 발생했다고 간단하게 알리는 것이 좋음
- 개발자는 빨리 문제를 찾고 디버깅할 수 있도록 오류 메시지와 스택 트레이스를 남겨두어야 함
- 예외도 객체이므로 필요하면 instanceof와 같이 예외 객체의 타입을 확인하여 넘어오는 예외 객체마다 다르게 처리되도록 할 수 있는데 여기서는 SendExceptionV4이 인스턴스로 넘어오면 [전송 오류]와 함께 추가 메시지를 남기도록 설정하였음
(6) e.printStackTrace()
- 예외 메시지와 스택 트레이스를 출력할 수 있어 이기능을 사용하면 예외가 발생한 지점을 역으로 추적할 수 있음
- 예제에서는 e.printStackTrace(System.out)을 사용하여 표준 출력으로 보냈는데 e.printStackTrace()를 사용하면 System.err라는 표준 오류에 결과를 출력할 수 있음
- IDE에서는 System.err로 출력하면 출력 결과를 빨간색으로 보여주며 일반적으로는 이 방법을 사용함
** 참고
- System.out, System.err 둘다 콘솔에 출력되지만 서로 다른 흐름을 통해서 출력되기 때문에 둘을 함께 사용하면 출력 순서를 보장하지 않으므로 출력 순서가 꼬여서 보일 수 있음
- 실무에서는 System.out이나 System.err를 통해 콘솔에 무언가 출력하기 보다는 Slf4J, logback 같은 별도의 로그 라이브러리를 사용하여 콘솔과 특정 파일에 함게 결과를 출력함
- e.printStackTrace()를 직접 호출하면 결과가 콘솔에만 출력되는데 서버에서 로그를 확인하기가 어렵기 때문에 콘솔에 결과가 출력되는 e.printStackTrace()는 잘 사용하지 않음
- 서버에서는 보통 파일로 로그를 확인하며 로그 라이브러리를 통해서 예외 스택 트레이스를 출력함
- 지금은 로그 라이브러리가 있다는 정도만 알아두고 자바를 학습하는 단계에서는 e.printStackTrace()를 적는 사용해도 괜찮음
- 과거에 제공했던 라이브러리들은 체크 예외를 많이 다루었지만 최근에 제공하는 라이브러리들은 대부분 언체크 예외를 많이 사용하고 있음
4.try-with-resources
1) 예외 처리 편의 기능
(1) 설명
- 애플리케이션에서 외부 자원을 사용하는 경우에는 반드시 외부 자원을 해제해야 하기 때문에 finally 구문을 반드시 사용해야 했음
- try에서 외부 자원을 사용하고 try가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 try with resources라는 편의 기능을 자바 7에서 도입했음
- 이름 그대로 try에서 자원을 함께 사용한다는 뜻임
- 해당 기능을 사용하려면 AutoCloseable 인터페이스를 구현해야 하며 try with resources를 사용할 때 try가 끝나는 시점에 close()가 자동으로 호출됨
package java.lang;
public interface AutoCloseable {
void close() throws Exception;
}
try (Resource resource = new Resource()) {
// 리소스를 사용하는 코드
}
(2) NetworkClientV5
- try with resources 구문을 사용하는 코드
- close(): AutoCloseable 인터페이스를 구현하여 close()메서드를 오버라이딩하면 try가 끝날 때 이 메서드를 자동으로 호출되어 종료 시점에 자원을 반납하는 방법을 여기에 정의하면됨
- 참고로 오버라이딩을 하면 자동으로 throws Exception이 붙는데 이 메서드에서 예외를 던지지는 않으므로 인터페이스의 메서드에 있는 throws Exception은 제거하였음
package exception.ex4;
public class NetworkClientV5 implements AutoCloseable {
// ... 기존 코드 동일 생략
@Override
public void close() {
System.out.println("NetworkClientV5.close");
disconnect();
}
}
(2) NetworkServiceV5
- try with resources구문은 try 괄호 안에 사용할 자원을 명시하고 이 자원은 try 블러이 끝나면 자동으로 AutoCloseable.close()를 호출해서 자원을 해제함
- catch블럭 없이 try 블럭만 있어도 close()는 호출되며 여기서 catch 블럭은 예외를 출력만하고 정상흐름을 반환하지 않고 다시 위로 던짐
package exception.ex4;
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;
}
}
}
(3) MainV4 - 수정 및 실행
- NetworkServiceV5를 사용하도록 하여 프로그램을 실행
- 정상 흐름: 정상적으로 서버 연결 해제가 출력됨
- 예외 발생: error1과 error2를 입력하여 예외가 발생하여 출력된 문구를 보면 출력순서가 catch부분이 먼저 실행되는 것이 아니라 try가 종료되는 즉시 close()메소드가 실행되는 것을 확인할 수 있음
- try with resources 구문은 자원을 try에서만 사용하기 때문에 try가 끝나자마자 자원을 반환하기 때문에 catch부분으로 넘어가게되면 이미 자원은 반납되고 없음
/* 실행 결과
전송할 문자: 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:9)
at exception.ex4.MainV4.main(MainV4.java:19)
전송할 문자: 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:10)
at exception.ex4.MainV4.main(MainV4.java:19)
[전송 오류] 전송 데이터: error2
전송할 문자: exit
프로그램을 정상 종료합니다.
*/
(4) try with resources 장점
- 리소스 누수 방지: 모든 리소스가 제대로 닫히도록 보장하므로 실수로 finally 블록을 적지 않고나 finally 블럭안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있음
- 코드 간결성 및 가독성 향상: 명시적인 close() 호출이 필요 없어 코드가 더 간결하고 읽기 쉬워짐
- 스코프 범위 한정: 리소스로 사용되는 client 변수의 스코프가 try 블럭안으로 한정되어서 코드의 유지보수가 더 쉬워짐
- 조금 더 빠른 자원 해제: 기존에는 try-catch-finally로 catch 이후에 자원을 반납하지만 try with resources는 try 블럭이 끝나면 즉시 close()를 호출하여 자원을 반납함
(5) 예외 총 정리
- 처음 자바를 설계할 당시에는 체크 예외가 더 나은 선택이라 생각했기 때문에 자바가 기본으로 제공하는 기능들에는 체크 예외가 많음
- 그런데 시간이 흐르면서 복구 할 수 없는 예외가 너무 많아졌고 특히 라이브러리를 점점 더 많이 사용하면서 처리해야 하는 예외 자체가 늘어나게 되어 체크 예외를 처리할 수 없을 때마다 throws에 예외를 덕지덕지 붙여야 했음
- 그래서 과거 개발자들은 throws Exception이라는 극단적인 방법도 자주 사용했었음
- 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공하며 가장 유명한 스프링이나 JPA같은 기술들도 대부분 언체크 예외를 사용함
- 런타임 예외도 필요하면 잡을 수 있기 때문에 필요한 경우에는 잡아서 처리하고 그렇지 않으면 자연스럽게 던지도록 두고 처리할 수 없는 예외는 공통으로 처리하는 부분을 만들어서 해결하면 됨