본문 바로가기
Spring/Spring DB

자바 예외(Exception)

by YellowCow 2025. 7. 21.

자바에서는 예외에 대해 대응할 수 있도록 "예외 계층"을 제공한다

예외 계층

자바의 예외는 아래 그림과 같이 존재한다

먼저, 모든 예외는 클래스이기 때문에 Object를 상속받도록 되어있다.

자바에서 예외는 Exception과 Error로 구분되어 있다.

Error는 메모리 부족 등의 애플리케이션 레벨에서 복구 불가능한 예외를 의미한다. 애플리케이션 개발자는 이 예외를 잡으려고 하면 안 된다.

Exception은 애플리케이션 레벨에서 대응가능한 실질적인 최상위 예외이다.

Exception의 하위 예외들은 컴파일러에서 체크하는 예외이다. 이를 Checked Exception이라고 한다.

단, RuntimeException과 그 하위 예외들은 컴파일러에서 체크하지 않는 예외이다. 이를 Unchecked Exception이다.

 

이처럼, 컴파일러 체크여부에 따라 Checked ExceptionUnchecked Exception으로 분류된다.

자바에서는 Unchecked Exception 발생 시에만 트랜잭션 rollback을 하기 때문에 알아두어야 한다.

참고로, Error도 Unchecked Exception이다.

 

 

예외 기본 규칙

예외는 폭탄 돌리기와 같다.

예외는 반드시 잡아서 처리하거나 처리할 수 없으면 밖으로 던져야 한다.

 

예외 처리

위 그림을 보면 Repository에서 예외를 Service로 던진 후 Service에서 예외를 처리하는 모습을 볼 수 있다

예외 처리 이후에는 정상로직으로 동작한다

 

예외 던짐

위 그림은 Repository에서 예외가 발생하여 상위 계층으로 계속 예외를 던지는 모습이다.

예외를 처리하지 못하면 호출한 곳으로 예외를 계속 던지게 된다

 

정리하자면, 예외에는 아래와 같은 규칙이 존재한다

1. 예외는 잡아서 처리하거나 던져야 한다.

2. 예외를 잡거나 던질 지정한 예외뿐만 아니라 예외의 자식들도 함께 처리된다.

  - 예를 들어서 `Exception` `catch` 잡으면 하위 예외들도 모두 잡을 있다.

  - 예를 들어서 `Exception` `throws` 던지면 하위 예외들도 모두 던질 있다.

 

만약 예외를 호출한 지점에 도달할 때까지 예외를 처리하지 못하면 어떻게 될까?

- 자바 main() 쓰레드의 경우, 예외 로그를 출력하면서 시스템이 종료된다

- 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 시스템이 종료되면 안된다.

   WAS 해당 예외를 받아서 처리하는데, 주로 사용자에게 개발자가 지정한, 오류 페이지를 보여준다.

 

코드 예제 

Chekcked Exception

@Slf4j
public class CheckedTest {

    /**
     * Exception을 상속받은 예외는 Checked 예외가 된다
     */
    public static class MyCheckedException extends Exception {
        public MyCheckedException(String message) {
            super(message);
        }
    }

    /**
     * 예외는 잡아서 처리하거나, 던지거나 둘 중 하나를 필수로 선택해야 한다
     */
    public static class Service{
        Repository repository = new Repository();

        /**
         * 예외를 잡아서 처리하는 코드
         */
        public void callCatch(){
            try {
                repository.call();
            } catch (MyCheckedException e) {
                log.info("예외 처리, message={}", e.getMessage(), e);
            }
        }

        /**
         * Checked 예외를 밖으로 던지는 코드
         * Checked 예외는 예외를 잡지 않고 밖으로 던지려면 throws 예외를 메서드에 필수로 선언해야한다
         *
         * @throws MyCheckedException
         */
        public void callThrow() throws MyCheckedException {
            try {
                repository.call();
            } catch (MyCheckedException e) {
                throw new MyCheckedException(e.getMessage());
            }
        }
    }

    public static class Repository{
        public void call() throws MyCheckedException {
            throw new MyCheckedException("ex");
        }
    }

    @Test
    public void checked_catch(){
        Service service = new Service();

        service.callCatch();
    }

    @Test
    public void checked_throw(){
        Service service = new Service();

        assertThatThrownBy(service::callThrow)
                .isInstanceOf(MyCheckedException.class);
    }
}

 

Checked Exception은 컴파일러가 체크하는 예외이다.

물론, 예외가 발생하지 않는 상황이 제일 좋겠지만

예외가 발생하는 케이스 중 제일 최상의 경우는 컴파일러에서 발생되는 예외이다.

 

그래서 초기의 자바 라이브러리들은 되도록이면 컴파일러에서 예외를 체크할 수 있도록 Checked Exception을 주로 사용했다.

하지만 Checked Exception을 던지거나 처리해야하는 경우, 메서드 시그니처에 발생할 수 있는 예외를 명시해야 한다.

개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아준다는 장점이 있으나

개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다. 추가로 의존관계에 따른 단점도 있다.

 

아래 코드를 살펴보자

NetworkClient, Repository에서 발생하는 예외를 Service 뿐만 아니라, Controller에서도 명시해주어야 한다.

또한 Controller와 Service가 SQLException에 의존하고 있다.

SQLException은 JDBC에 의존하는 예외이다. JPA를 사용할 경우, SQLException가 아닌 다른 예외가 발생할 수 있다.

이 경우, Controller와 Service를 모두 수정해야 하므로, 유지보수에 어려움을 겪을 수 있다.

public class CheckedAppTest {

    @Test
    public void checked() throws Exception {
        Controller controller = new Controller();
        assertThatThrownBy(controller::request)
                .isInstanceOf(Exception.class);
    }

    static class Controller{
        Service service = new Service();

        public void request() throws SQLException, ConnectException {
            service.logic();
        }
    }

    static class Service{
        Repository repository =  new Repository();
        NetworkClient networkClient =  new NetworkClient();

        public void logic() throws SQLException, ConnectException {
            repository.call();
            networkClient.call();
        }
    }

    static class NetworkClient{
        public void call() throws ConnectException {
            throw new ConnectException();
        }
    }

    static class Repository{
        public void call() throws SQLException {
            throw new SQLException();
        }
    }
}

 

Unchekcked Exception

Unchecked Exception의 경우에는 메서드 시그니처에 예외를 생략할 수 있다는 장점이 있지만

개발자가 처리해야하는 예외를 누락할 수 있다는 단점이 존재한다

@Slf4j
public class UncheckedTest {

    /**
     * RuntimeException을 상속받은 예외는 Unchecked 예외가 된다
     */
    public static class MyUncheckedException extends RuntimeException {
        public MyUncheckedException(String message) {
            super(message);
        }
    }

    /**
     * Unchecked 예외는 잡아서 처리하거나, 던지지 않아도 된다
     * 예외를 잡지 않으면 자동으로 밖으로 던진다
     */
    public static class Service{
        Repository repository = new Repository();

        /**
         * 예외를 잡아서 처리하는 코드
         */
        public void callCatch(){
            try {
                repository. call();
            } catch (MyUncheckedException e) {
                log.info("예외 처리, message={}", e.getMessage(), e);
            }
        }

        /**
         * Unchecked 예외를 밖으로 던지는 코드
         * 예외를 잡지 않아도 된다. 예외가 발생할 경우 자연스럽게 상위로 넘어간다
         * Checked 예외와 다르게 throws 선언은 하지 않아도 된다
         *
         * @throws MyUncheckedException
         */
        public void callThrow() {
            try {
                repository.call();
            } catch (MyUncheckedException e) {
                throw new MyUncheckedException(e.getMessage());
            }
        }
    }

    public static class Repository{
        public void call() {
            throw new MyUncheckedException("ex");
        }
    }

    @Test
    public void unchecked_catch(){
        Service service = new Service();

        service.callCatch();
    }

    @Test
    public void unchecked_throw(){
        Service service = new Service();

//        service.callThrow();
        assertThatThrownBy(service::callThrow)
                .isInstanceOf(MyUncheckedException.class);
    }
}

 

 

그렇다면 언제 Unchecked Exception을 사용하면 좋을까?

  • 기본적으로 언체크(런타임) 예외를 사용하자.
  • 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.
    • 이 경우 해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야 한다. 예를 들어서 다음과 같은 경우가 있다.
      • 체크 예외 예)
        • 계좌 이체 실패 예외
        • 결제시 포인트 부족 예외
        • 로그인 ID, PW 불일치 예외
    • 물론 이 경우에도 100% 체크 예외로 만들어야 하는 것은 아니다. 다만 계좌 이체 실패처럼 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안된다고 판단할 수 있다. 이 경우 체크 예외로 만들어 두면 컴파일러를 통해 놓친 예외를 인지할 수 있다.

 

Checked Exception을 Unchecked Exception으로 변환하는 방법

아래 코드의 Repository부분을 보면 runSQL메서드 실행할 때 발생하는 SQLException을 Unchecked Exception인 RuntimeSQLException으로 변환하는 모습을 볼 수 있다. 이렇게 하면 Checked Exception을 Unchecked Exception으로 변환할 수 있다. 또한 Unchecked Exception의 생성자 인자 값으로 발생한 예외를 전달하면 발생했던 예외 로그까지 출력해준다.

@Slf4j
public class UncheckedAppTest {

    @Test
    public void unchecked() {
        Controller controller = new Controller();
        assertThatThrownBy(controller::request)
                .isInstanceOf(RuntimeException.class);
    }

    @Test
    public void printEx(){
        Controller controller = new Controller();
        try {
            controller.request();
        } catch (Exception e) {
            log.info("ex", e);
        }
    }

    static class Controller{
        Service service = new Service();

        public void request() {
            service.logic();
        }
    }

    static class Service{
        Repository repository =  new Repository();
        NetworkClient networkClient =  new NetworkClient();

        public void logic() {
            repository.call();
            networkClient.call();
        }
    }

    static class NetworkClient {
        public void call(){
            throw new RuntimeConnectException("연결 실패!!");
        }
    }

    static class Repository{
        public void call() {
            try {
                runSQL();
            } catch (SQLException e) {
            	// Unchecked Exception으로 변환
                throw new RuntimeSQLException(e);
            }
        }

		// Checked Exception 발생
        public void runSQL() throws SQLException {
            throw new SQLException("ex");
        }
    }

    static class RuntimeSQLException extends RuntimeException{
        public RuntimeSQLException(String message) {
            super(message);
        }

        public RuntimeSQLException(Throwable cause) {
            super(cause);
        }

        public RuntimeSQLException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    static class RuntimeConnectException extends RuntimeException{
        public RuntimeConnectException(String message) {
            super(message);
        }

        public RuntimeConnectException(Throwable cause) {
            super(cause);
        }

        public RuntimeConnectException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

 

예외 로그

13:10:45.626 [Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest -
ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException:
java.sql.SQLException: ex
at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.j
ava:61)
at
hello.jdbc.exception.basic.UncheckedAppTest$Service.logic(UncheckedAppTest.jav
a:45)
at
hello.jdbc.exception.basic.UncheckedAppTest$Controller.request(UncheckedAppTes
t.java:35)
athello.jdbc.exception.basic.UncheckedAppTest.printEx(UncheckedAppTest.java:24)
Caused by: java.sql.SQLException: ex
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.runSQL(UncheckedAppTest.java:66)
at hello.jdbc.exception.basic.UncheckedAppTest$Repository.runSQL(UncheckedAppTest.java:59)

'Spring > Spring DB' 카테고리의 다른 글

문제해결 - 트랜잭션 동기화 문제  (1) 2025.07.19
문제해결 - 트랜잭션 처리 코드 반복 문제  (0) 2025.07.19
DB Lock  (0) 2025.07.18
DB Connection  (0) 2025.07.18
트랜잭션  (0) 2025.07.18

댓글