예외 계층
-
Object
: 모든 객체의 최상위 부모 -
Throwable
: 최상위 예외, 잡으면 안됨 (Error
까지 잡히므로)-
Error
- 애플리케이션에서 복구 불가능한 시스템 예외 (메모리 부족, 심각한 시스템 오류…)
- 애플리케이션 개발자는 이 예외를 잡지 않아야 함
- 언체크 예외
-
Exception
: 체크 예외 (런타임 예외 제외), 애플리케이션에서 개발자가 잡아야 할 실질적최상위 예외-
RuntimeException
: 언체크 예외 (=런타임 예외)
-
-
체크예외 VS 언체크 예외
- 핵심
- 언체크 예외는
throws
선언하지 않고 생략 가능 (자동 예외 던지기) - 나머지는 동일
- 언체크 예외는
- 체크 예외
- 컴파일러가 체크하는 예외
- 체크 예외의 장단점
- 예외를 누락하지 않도록 컴파일러가 안전 장치 역할 (누락 시 컴파일 오류)
- 크게 신경쓰지 않고 싶은 예외까지 모두 반드시 잡거나 던져야 함
- 언체크 예외
- 컴파일러가 체크하지 않는 예외
- 중요 예외의 경우
throws
를 선언해두면 IDE를 통해 개발자가 편리하게 인지 가능 (보통은 생략) - 언체크 예외의 장단점
- 신경쓰고 싶지 않은 언체크 예외 무시 가능
- 개발자가 실수로 예외 누락 가능
예외 처리 기본
-
기본 규칙
- 예외는 잡아서 처리하거나 던져야 한다
- 예외를 잡는 코드:
catch
- 예외를 던지는 코드:
throws
- 예외를 잡는 코드:
- 예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 예외의 자식들도 함께 처리된다
- 예외는 잡아서 처리하거나 던져야 한다
- 기본적으로 언체크(런타임) 예외를 사용하자
-
체크 예외들은 바깥으로 던져야 하는데 이 과정에서 의존 관계 문제 발생
- 실무에서 발생하는 대부분의 예외는 복구 불가능한 시스템 예외 (애플리케이션 단에서 처리 불가)
-
의존 관계 문제
- 컨트롤러, 서비스는 본인이 처리할 수 없어도
throws
를 선언해 예외를 던져야 함 -
컨트롤러, 서비스가 해당 체크 예외에 의존하게 되어 구현 기술 변경 시 OCP 위반
- 예를 들어, DB 접근 기술을 변경한다면 예외를 포함한 컨트롤러, 서비스 코드를 수정
- 수 많은 체크 예외를 일일이 명시해 던지는 것도 부담
- 컨트롤러, 서비스는 본인이 처리할 수 없어도
-
런타임 예외를 사용하면 처리할 수 없는 예외를 별도 선언 없이 그냥 두면 됨
- 의존성 발생 X -> 기술 변경이 있어도 컨트롤러, 서비스 코드 변경 X -> OCP 준수
- 대부분의 최근 라이브러리는 런타임 예외를 기본으로 제공 (스프링, JPA…)
- 런타임 예외는 놓칠 수 있기 때문에 문서화가 중요 (혹은 명시적으로 코드에
throws
남기기)
-
체크 예외들은 바깥으로 던져야 하는데 이 과정에서 의존 관계 문제 발생
- 처리할 수 없는 예외들은 한 곳에서 공통처리
- 서블릿 오류 페이지, 스프링 MVC
ControllerAdvice
예외 공통 처리- 고객: 오류 페이지
- 내부 개발자: 별도 로그, 슬랙, 문자, 메일을 통해 개발자가 빠르게 인지
- API는 상태코드 500 응답
- 예시 코드
public class Main { public static void main(String[] args) { NetworkService networkService = new NetworkService(); try { networkService.sendMessage(); } catch (Exception e) { // 모든 예외를 잡아서 처리 exceptionHandler(e); } } //공통 예외 처리 private static void exceptionHandler(Exception e) { //공통 처리 System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다."); System.out.println("==개발자용 디버깅 메시지=="); e.printStackTrace(System.out); // 스택 트레이스 출력 //e.printStackTrace(); // System.err에 스택 트레이스 출력 //실무에서는 보통 Slf4j 사용해 로그를 콘솔 출력 + 파일로 저장 //e.printStackTrace는 콘솔에만 출력하므로 사용 X //필요하면 예외 별로 별도의 추가 처리 가능 if (e instanceof SendException sendEx) { System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData()); } } }
- 서블릿 오류 페이지, 스프링 MVC
- 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용
- 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안된다고 판단할 수 있음
- 체크 예외 예시 (무조건 해야하는 건 아님)
- 계좌 이체 실패 예외
- 결제시 포인트 부족 예외
- 로그인 ID, PW 불일치 예외
-
Exception
을 던지지 말자- 코드가 깔끔해지는 것 같지만, 모든 체크 예외를 다 던져 버려서 중요한 체크 예외를 놓침
- 꼭 필요한 경우가 아니면
Exception
자체를 밖으로 던지는 것은 좋은 방법이 아님
-
스택 트레이스를 남기자
- 로그 남기기
log.info("예외 처리, message={}", e.getMessage(), e);
- 로그의 마지막 인수에 예외 객체 전달하면 로그에 스택 트레이스를 출력
- 예외를 전환할 때는 반드시 기존 예외를 포함하자
-
throw new RuntimeSQLException(e);
- 기존 예외e
포함 - 덕분에 기존 예외와 스택 트레이스까지 확인 가능
- 포함하지 않으면 실제 DB에서 발생한 근본적인 원인을 확인할 수 없는 심각한 문제 발생
-
- 로그 남기기
예외를 계속 던지면 벌어지는 상황
- 자바
main()
쓰레드의 경우 예외 로그를 출력하면서 시스템이 종료- 웹 애플리케이션의 경우 WAS가 예외를 받아 처리하고 개발자가 지정한 오류 페이지 보여줌
(예외 하나로 시스템이 종료되면 안됨)
사용자 예외 만들기
-
Exception
을 상속 받으면 체크 예외 -
RuntimeException
을 상속 받으면 언체크 예외 -
오류 메시지 보관하기
- 생성자를 통해 오류 메시지를 보관할 것 (예외가 제공하는 기본 기능)
-
super(message)
로 전달한 메시지는Throwable
의detailMessage
에 보관됨 -
getMessage()
로 조회 가능public class MyCheckedException extends Exception { public MyCheckedException(String message) { super(message); } }
-
적당한 예외 계층화하기 (너무 많아도 문제)
- 상속을 사용해 예외를 계층화하면 보다 세밀한 예외 처리 가능
- e.g.
-
NetworkClientException
(부모)-
ConnectException
(자식, 내부 연결 시도address
보관) -
SendException
(자식, 내부 전송 시도 데이터sendData
보관)
-
-
- e.g.
- 각각의 하위 예외에 고유 기능을 만들어 활용 가능
- e.g.
e.getAddress()
,e.getSendData()
- e.g.
- 부모 예외를 잡아 자식까지 한 번에 처리하거나 특정 하위 예외만 잡아 처리 가능
- 중요한 특정 하위 예외만 메시지를 명확히 남기고 나머지는 공통 처리 가능
- e.g.
-
[연결 오류] 주소: ...
(하위 예외) -
[네트워크 오류] 메시지: ...
(부모 예외) -
[알 수 없는 오류] 메시지: ...
(그 외 예외 공통 처리)
-
- e.g.
- 중요한 특정 하위 예외만 메시지를 명확히 남기고 나머지는 공통 처리 가능
- 상속을 사용해 예외를 계층화하면 보다 세밀한 예외 처리 가능
예외 처리 발전 과정 예시 (e.g. NetworkClient)
- 반환 값(문자열)으로 예외 처리
- 분기 처리 및
return
으로 네트워크 연결 및 해제, 데이터 전송 관리 등이 가능 - 가장 중요한 정상 흐름이 한눈에 들어오지 않음 (정상 흐름과 예외 흐름 분리 X, 가독성 감소)
- 분기 처리 및
- 예외 처리 메커니즘 사용 (
try ~ catch ~ finally
)- 성공 여부를 반환값이 아닌 메서드 정상 종료 여부로 판단
-
정상 흐름과 예외 흐름을 명확히 분리해 가독성 상승 (
try
블록,catch
블록) -
반드시 실행되어야 하는 코드를 안전하게 호출하도록 보장 (
finally
)-
finally
가 없으면catch
에서 잡지 못한 예외가 발생할 때 문제가 생김 - 외부 자원 해제 등에 편리
-
finally
finally
블록은 어떤 경우라도 반드시 호출된다. 주로try
에서 사용한 자원을 해제할 때 사용한다.
예외를 직접 잡을 일이 없다면,try ~ finally
만 사용하는 것도 가능하다.정상 흐름 (
try
) ->finally
예외 잡음 (catch
) ->finally
예외 던짐 ->finally
(finally
블록 끝난 이후 예외가 밖으로 던져짐)
자원 해제
외부 리소스는 사용 후 반드시 연결을 해제하고 자원을 반납해야 메모리 고갈을 피할 수 있다. (네트워크 연결 자원, DB 연결 자원…)
자바는 GC로 JVM 메모리 상 인스턴스들을 자동으로 해제하지만, 외부 연결 같은 자바 외부 자원은 자동으로 해제되지 않는다.
여러 예외 한 번에 잡는 Syntax
catch
블록에서|
키워드를 사용해 예외를 나열할 수 있다.
다만, 이 경우 각 예외들의 공통 부모 기능만 사용할 수 있다.
e.g.catch (ConnectException | SendException e) {...}
// 이 경우 공통 부모인 NetworkClientException의 기능만 사용 가능
try-with-resources 구문
-
finally
없이 편리한 외부 자원 해제 지원 - 사용 방법
- 외부 자원 클래스에
AutoCloseable
인터페이스를 구현 (implements AutoCloseable
)-
close()
메서드를 오버라이드해 자원 반납 방법 정의 - 메서드가 예외를 던지지 않으면 인터페이스의
throws Exception
은 생략
-
-
try-with-resources
구문 사용try (Resource resource = new Resource()) { // 리소스를 사용하는 코드 }
-
try
괄호 안에 사용할 자원을 명시- e.g.
try (NetworkClient client = new NetworkClient(address)) {...}
- e.g.
try
블록이 끝나면 자동으로AutoCloseable.close()
호출해 자원 해제-
try
블록만 단독으로도 사용 가능 (catch
,finally
없이 사용 가능)
-
- 외부 자원 클래스에
- 장점
-
리소스 누수 방지
- 실수로
finally
블록 혹은 그 내부에 자원 해제 코드를 누락하는 문제 예방
- 실수로
- 코드 간결성 및 가독성 향상
- 리소스 스코프 범위 한정
- 리소스 객체 변수의 스코프를
try
블록으로 한정해 코드 유지보수성 향상
- 리소스 객체 변수의 스코프를
-
조금 더 빠른 자원 해제
- 기존에는
catch
이후에 자원을 반납 (try
->catch
->finally
) -
try with resources
는try
블록이 끝나면 즉시close()
호출
- 기존에는
-
리소스 누수 방지