자바 예외는 크게 두 가지로 나뉜다. 어떤 건 예외 처리를 안 하면 빨간 줄이 뜨고(체크), 어떤 건 아무 말 없다가 실행 중에 터진다(언체크). 이 둘을 나누는 기준은 단순하다. "컴파일러가 간섭하느냐, 아니냐"다.
체크 예외(Checked Exception)는 RuntimeException을 상속받지 않은 예외들을 말한다. 이 예외들은 컴파일러가 "너 이거 예외 터지면 어떻게 할 거야?"라고 사사건건 간섭하기 때문에 반드시 try-catch로 잡거나 throws로 던져야 한다. 대책을 세우지 않으면 빌드조차 안 되는 강제성이 있다.
이렇게까지 하는 이유는 이 예외들이 주로 '외부 사고'에서 오기 때문이다. 파일이 없거나 DB 연결이 끊기는 건 내 코드 실수가 아니라 어쩔 수 없는 외부 요인이다. 자바는 이런 사고가 났을 때 프로그램이 그냥 죽게 내버려 두지 말고, 어떻게든 복구 대책을 세우라는 의도로 예외 처리를 강제한다. 즉, 코드 차원에서의 안전장치인 셈이다.
반대로 언체크 예외(Unchecked Exception)는 RuntimeException을 상속받은 예외들이다. 이름처럼 컴파일러가 예외 처리 여부를 확인하지 않아서 코드상으로는 조용하다가 실행(Runtime) 중에 펑 터진다.
이건 주로 '개발자의 실수'에서 온다. 0으로 나누거나 빈 객체를 참조하는 건 복구 대책을 세울 일이 아니라, 애초에 코드를 똑바로 짜서 고쳐야 할 문제다. 그래서 자바는 굳이 예외 처리를 강요하지 않는다. 예외 처리를 하기 전에 로직을 수정하라는 뜻이다.
이 철학적 차이는 스프링의 트랜잭션(@Transactional) 롤백 정책에 그대로 반영되어 있다.
체크 예외는 자바 설계상 '복구 가능한 사고'에 가깝다. 그래서 스프링도 이를 시스템이 멈춰야 할 장애가 아닌, 비즈니스 과정에서 발생한 예외적인 시나리오로 보고 일단 커밋을 진행한다. 반면에 언체크 예외는 '명백한 개발자의 실수'다. 스프링은 이를 데이터 정합성을 깨뜨릴 수 있는 치명적인 장애로 판단하고 즉시 롤백을 수행한다.
하지만 최근 실무 트렌드는 모든 예외를 언체크 예외로 던지는 쪽으로 기울고 있다. 어차피 DB가 꺼지는 사고는 코드 수준에서 복구하기 어렵기 때문이다. 굳이 모든 메서드에 throws를 달아 코드를 지저분하게 만들기보다, 체크 예외를 언체크 예외로 감싸서(Wrapping) 던지고 공통 에러 핸들러에서 한 번에 처리하는 방식이 훨씬 깔끔하다.
결국 체크 예외와 언체크 예외의 구분은 프로그램의 안정성(체크)과 코드의 생산성(언체크) 사이에서 균형을 잡기 위한 자바의 설계 장치다.
단순히 빨간 줄을 없애는 게 목적이 아니라, 지금 발생한 문제가 사용자가 다시 시도하게 할 '사고'인지, 아니면 당장 버그를 잡아야 하는 '실수'인지를 먼저 고민해 보는 것이 좋다.