1. 예외 처리 도입
1-1. 시작
오류 코드
어떤 종류의 오류가 발생했는지 구분하기 위해 예외 안에 오류 코드를 보관한다.
오류 메세지
오류 메세지에 어떤 오류가 발생했는지 개발자가 보고 이해할 수 있는 설명을 담아둔다.
1-2. 예외 복구
1-3. 정상, 예외 흐름 분리
1-4. 리소스 반환 문제
1-5. finally
정리
자바 예외 처리는 try ~ catch ~ finally 구조를 사용해서 처리할 수 있다. 덕분에 다음과 같은 이점이 있다.
정상 흐름과 예외 흐름을 분리해서, 코드를 읽기 쉽게 만든다.
사용한 자원을 항상 반환할 수 있도록 보장해준다.
2. 예외 계층
2-1. 시작
예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.
이렇게 예외를 계층화 하면 다음과 같은 장점이 있다.
- 자바에서 예외는 객체이다. 따라서 부모 예외를 잡거나 던지면, 자식 예외도 함께 잡거나 던질 수 있다.
- 특정 예외를 잡아서 처리하고 싶으면 하위 예외를 잡아서 처리하면 된다.
2-2. 활용
여러 예외를 한번에 잡는 기능
다음과 같이 |를 사용해서 여러 예외를 한번에 잡을 수 있다.
try {
client.connect();
client.send(data);
} catch (ConnectExceptionV3 | SendExceptionV3 e) {
정리
예외를 계층화하고 다양하게 만들면 더 세밀한 동작들을 깔끔하게 처리할 수 있다. 그리고 특정 분류의 공통 예외들도 한번에 catch 로 잡아서 처리할 수 있다.
3. 실무 예외 처리 방안
3-1 설명
처리할 수 없는 예외
예를 들어서 상대 네트워크 서버에 문제가 발생해서 통신이 불가능하거나, 데이터베이스 서버에 문제가 발생해서 접속이 안되면, 애플리케이션 연결 오류, 데이터베이스 접속 실패와 같은 예외가 발생한다.
이렇게 시스템 오류때문에 발생한 예외들은 대부분 예외를 잡아도 해결할 수 있는 것이 없다. 예외를 잡아서 다시 호출을 시도해도 같은 오류가 반복될 뿐이다.
이런 경우 고객에게는 "현재 시스템에 문제가 있습니다"라는 오류 메세지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두어야 한다.
체크 예외의 부담
체크 예외는 개발자가 실수로 놓칠 수 있는 예외들을 컴파일러가 체크해주기 떄문에 오래전부터 많이 사용되었다. 그런데 앞서 설명한것 처럼 처리할 수 있는 예외가 많아지고, 또 프로그램이 점점 더 복잡해지면서 체크 예외를 사용하는 것이 점점 더 부담스러워졌다.
throws Exception의 문제
Exception은 최상위 타입이므로 모드 체크 예외를 다 밖으로 던지는 문제가 발생한다.
결과적으로 체크 예외의 최상위 타입인 Exception을 던지게 되면 다른 체크 예외를 체크할 수 있는 기능이 무효화 되고, 중요한 체크 예외를 다 놓치게 된다. 중간에 중요한 체크 예외가 발생해도 컴파일러는 Exception을 던지기 떄문에 문법에 맞다고 판단해서 컴파일 오류가 발생하지 않는다.
이렇게 하면 모든 예외를 다 던지기 떄문에 예외를 의도한 대로 사용하는 것이 아니다. 따라서 꼭 필요한 경우가 아니면 이렇게 Exception 자체를 밖으로 던지는 것은 좋지 않는 방법이다.
체크 예외의 문제 정리
- 처리할 수 없는 예외: 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다.
- 체크 예외의 부담: 처리할 수 없는 예외는 밖으로 던져야 하낟. 체크 예외이므로 throws에 던질 대상을 일일이 명시해야 한다.
예외 공통 처리
처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 된다. 어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템이 문제가 있습니다. 라고 오류 메세지를 보여주고, 만약 웹이라면 오류 메세지를 보여주면 된다. 그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다. 이런 부분은 공통 처리가 가능하다.
3-2. 구현
exceptionHandler()
- 해결할 수 없는 예외가 발생하면 사용자에게는 시스템내에 알수 없는 문제가 발생했다고 알리는 것이 좋다.
- 사용자가 디테일한 오류 코드나 오류 상황까지 모두 이해할 필요는 없다. 예를 들어서 사용자는 데이터베이스 연결이 안되서 오류가 발생한 것인지, 네트워크에 문제가 있어서 오류가 발생한 것인지 알 필요는 없다.
- 개발자는 빨리 문제를 찾고 디버깅 할 수 있도록 오류 메세지를 남겨두어야 하낟.
- 예외도 객체이므로 필요하면 instanceOf 와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.
e.printStatcTrace()
- 예외 메세지와 스택 트레이스를 출력할 수 있다.
- 이 기능을 사용하면 예외가 발생한 지점을 역으로 추적할 수 있다.
- 참고로 예제에서는 e.printStaceTrace(System.out)을 사용해서 표준 출력으로 보냈다.
- e.printStackTrae()를 사용하면 System.err 이라는 표준 오류에 결과를 출력한다.
- IDE에서는 Sytem.err로 출력하면 출력 결과를 빨간새으로 보여주낟.
- 일반적으로 이 방법을 사용한다.
참고: System.out과 System.err는 둘다 콘솔에 출력되지만, 서로 다른 흐름으로 출력하기 때문에 순서를 보장하지 않는다.
참고: 실무에서는 System.out이나 System.err을 통해 콘솔에 출력하기 보다는 주로 Slf4J, logback 같은 별도의 로그 라이브러리를 사용해ㅓㅅ 콘솔과 특정 파일에 함께 결과를 출력한다. 그런데 e.printStackTrace()를 직접 호출하면 결과가 콘솔에만 출력된다. 이렇게 되면 서버에 로그를 확인하기 어렵다. 서버에서는 파일로 로그를 확인해야 한다. 따라서 콘솔에 바로 결과를 출력하는 e.printStackTrace()는 잘 사용하지 않는다. 대신에 로그 이브러리를 통해서 예외 스택트레이스를 사용한다.
4. try-with-resource
애플리케이션에서 외부 자원을 사용하는 경우 반드시 외부 자원을 해제해야 한다.
따라서 finally 구문을 반드시 사용해야 한다.
try에서 외부 자원을 사용하고, try가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바에서는 Try with resources라는 편의 기능을 자바 7에서 조입했다. 이름 그대로 try에서 자원을 함께 사용한다는 뜻이다. 여기서 자원은 try가 끝나면 반드시 종료해서 반납해야 하는 외부 자원을 뜻한다.
package exception.ex4;
import exception.ex4.exception.ConnectExceptionV4;
import exception.ex4.exception.SendExceptionV4;
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 + " 서버 연결 실패", "connectError");
}
//연결 성공
System.out.println(address + " 서버 연결 성공");
}
public void send(String data) {
if (sendError) {
throw new SendExceptionV4( address + " 서버에 데이터 전송 실패: " + data, "sendError");
// 중간에 다른 예외가 발생해다고 가정
// throw new RuntimeException("ex");
}
//전송 성공
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();
}
}
이 기능을 사용하려면 먼저 AutoCloseable 인터페이스를 구현해야 한다.
이 인터페이스를 구현하면 Try with resources를 사용할 때 try가 끝나는 시점에 close()가 자동으로 생성된다.
package exception.ex4;
public class NetworkServiceV5 {
public void sendMessage(String data) {
String address = "http://example.com";
try (NetworkClientV5 client = new NetworkClientV5(address)){
client.connect();
client.send(data);
} catch (Exception e) {
System.out.println("[예외 확인]: " + e.getMessage());
throw e;
}
}
}
- Try with resouces 구문은 try 괄호 안에 사용할 자원을 명시한다.
- 이 자원은 try 블럭이 끝나면 자동으로 AutoCloseable.close()를 호출해서 자원을 해제한다.
- 참고로 여기서 catch 블럭 없이 try 블럭만 있어도 close()는 호출된다.
Try with resources 장점
- 리소스 누수 방지: 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally 블록을 적지 않거나, finally 블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다.
- 코드 간결성 및 가독성 향상: 명시적인 close() 호출이 필요 없어 코드가 더 간결하고 읽기 쉬워진다.
- 스코프 범위 한정: 예를 들어 리소스로 사용되는 client 변수의 스코프가 try 블럭 안으로 한정된다. 따라서 코드 유지보수가 더 쉬워진다.
- 조금 더 빠른 자원 해제: 기존에는 try → catch → finally로 catch 이후에 자원을 반납했다. Try with resources 구분은 try 블럭이 끝나면 즉시 close()를 호출한다.
정리
처음 자바를 설계할 당시 체크 예외가 더 나은 선택이라 생각했다. 그래서 자바가 기본으로 제공하는 기능들에는 체크 예외가 많다. 그런데 시간이 흐르면서 복구할 수 없는 예외가 너무 많아졌다. 특히 라이브러리를 점점 더 많이 사용하면서 처리해야 하는 예외도 더 늘어났다. 라이브러리들이 제공하는 체크 예외를 처리할 수 없을 때마다 throws에 예외를 덕지덕지 붙어야 했다. 그래서 개발자들은 throws Exception 이라는 극단적인 방법도 자주 사용하게 되었다. 물론 이 방법은 사용하면 안된다. 모든 예외를 던진다고 선언하는 것인데, 결과적으로 어떤 예외를 잡고 어떤 예외를 던지는지 알 수 없기 때문이다. 체크 예외를 사용한다면 잡을건 잡고 던질 예외는 명확하게 던지도록 선언해야 하낟.
체크 예외의 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다. 가장 유명한 스프링이나 JPA 같은 기술들도 대부분 언체크(런타임) 예외를 사용한다.
런타임 예외도 필요하면 잡을 수 있기 떄문에 필요한 경우에는 잡아서 처리하고, 그렇지 않으면 자연스럽게 던지도록 둔다. 그리고 처리할 수 없는 예외를 공통으로 처리하는 부분을 만들어서 해결하면 된다.
출처 - 김영한의 실전 자바 중급 1편
'Backend > Java' 카테고리의 다른 글
제네릭 2 (0) | 2024.08.18 |
---|---|
제네릭 1 (0) | 2024.08.14 |
예외 처리 1 (0) | 2024.08.13 |
중첩 클래스, 내부 클래스 - 2 (0) | 2024.08.13 |
중첩 클래스, 내부 클래스 (0) | 2024.08.11 |