Spring, Apache, Java

예외

cwchoiit 2023. 12. 1. 15:45
728x90
반응형
SMALL

자바에서 예외는 크게 두 가지(체크 예외, 언체크 예외)로 나뉘어진다. 

728x90
반응형
SMALL

체크 예외

컴파일러가 체크하는 예외이다. 체크 예외는 잡아서 처리하거나 또는 밖으로 던지도록 선언해야한다. 그렇지 않으면 컴파일 오류가 발생한다.

언체크 예외

컴파일러가 체크하지 않는 예외이며, 런타임 예외라고도 불린다. 언체크 예외와 체크 예외의 차이가 있다면 예외를 던지는 throws를 선언하지 않고 생략할 수 있다. 이 경우에 자동으로 예외를 던진다. 

 

예외 계층

 

예외 계층 구조를 그림으로 보자.

 

예외 역시 객체이므로 최상위 부모인 Object가 예외 객체의 부모가 된다. 

Throwable은 예외의 최상위 객체이다. 그리고 이 Throwable 객체의 하위에는 ExceptionError가 있다.

 

Error: 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구가 불가능한 시스템 예외이자 언체크 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다. 다시 말해 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데, 그 이유는 Error가 Throwable의 하위 예외이기 때문이다. 이러한 이유로 애플리케이션 로직은 Exception부터 필요한 예외로 생각하고 잡으면 된다.

 

Exception: 체크 예외로 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다. Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외지만 RuntimeException만은 아니다. RuntimeException은 언체크 예외이다. 

 

RuntimeException: 언체크 예외이며 런타임 예외라고도 자주 불린다. 컴파일러가 체크하지 않는 언체크 예외이고 이 자식 예외들은 모두 언체크 예외이다.

 

예외 기본 규칙

예외는 폭탄 돌리기와 같다. 1. 잡아서 처리하거나  2. 처리할 수 없으면 밖으로 던진다. 여기서 밖으로라는 말은 자신을 호출한 곳을 의미한다.

 

설명: 위 그림처럼 Repository에서 예외가 발생했고 그 예외를 Repository에서는 처리하지 못하여 자신을 호출한 Service로 예외를 던졌다. 예외를 받은 Service는 이 곳에서 Repository가 던질 수 있는 가능성이 있는 예외를 처리한 후 정상 흐름을 자신을 호출한 Controller에게 돌려준다.

 

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

그래서 Exception을 catch로 잡으면 그 하위 예외들도 모두 잡을 수 있다. 또는 Exception을 throws로 던지면 그 하위 예외들도 모두 던질 수 있다.

 

설명: 예외를 처리하지 못하면 호출한 곳으로 계속 예외를 던진다.

그래서? 예외를 처리하지 못하고 계속 던지면 어떻게 될까? 자바 main() 쓰레드의 경우 결국 어디서도 처리하지 못하고 예외가 최초 시작 지점인 main()까지 도달하면 예외 로그를 출력하면서 시스템이 종료된다. 그러나 웹 애플리케이션의 경우 시스템이 종료되는 현상은 일어나면 안되기 때문에 WAS까지 해당 예외가 올라오면 그 예외를 받아 처리하는데 주로 사용자에게 개발자가 지정한 오류 페이지를 보여준다.

 

 

체크 예외 예시 코드

체크 예외는 반드시 던지거나 처리하거나 둘 중 하나는 해야한다. 그렇지 않으면 컴파일러가 컴파일 오류를 뱉어내기 때문에. 이게 체크 예외의 기본 규칙이다.

package com.example.jdbc.exception.basic;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CheckedTest {

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

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

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

        /**
         * 체크 예외를 던지는 코드
         * @throws MyCheckedException
         * */
        public void callThrow() throws MyCheckedException {
            repository.call();
        }
    }

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

}

 

위 코드를 보면, MyCheckedException 이라는 체크 예외를 만들었다. Exception을 상속받으면 그 예외는 체크 예외가 된다. 그리고 Service에서 Repository를 사용하는데 Repository의 call() 메소드는 체크 예외(MyCheckedException)를 던진다. 이럴 때 이 call() 메소드를 호출한 서비스 쪽에서는 두가지 행위를 취할 수 있게 된다. 던지거나 잡거나. callCatch() 메소드는 catch에서 해당 에러를 잡는다. callThrow() 메소드는 해당 에러를 던진다. 이것이 체크 예외를 다루는 기본 방식이다. 그럼 이 서비스를 호출하는 쪽은 어떻게 될까?

 

package com.example.jdbc.exception.basic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;


class CheckedTestTest {

    @Test
    void checked_catch() {
        CheckedTest.Service service = new CheckedTest.Service();
        service.callCatch();
    }

    @Test
    void checked_throw() {
        CheckedTest.Service service = new CheckedTest.Service();
        assertThatThrownBy(service::callThrow)
                .isInstanceOf(CheckedTest.MyCheckedException.class);
    }
}

 

서비스를 호출하는 쪽 역시 마찬가지로 체크 예외이기 때문에 던져진 예외는 잡거나 던져야한다. 여기서 checked_catch() 메소드는 서비스의 callCatch() 메소드를 호출하고 이는 메소드 내에서 예외를 잡았기 때문에 어떠한 행위도 할 필요가 없다. 그러나, checked_throw() 메소드는 callThrow()를 호출하기 때문에 던져진 예외를 받는다. 그렇기 때문에 그 던져진 예외를 검증하는 테스트 코드가 들어가 있다.

 

체크 예외의 장단점

  • 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치가 된다.
  • 단점: 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다.

 

언체크 예외 예시 코드

언체크 예외는 역시 체크 예외와 같이 잡거나 던지거나하면 되는데, 잡지도 던지지도 않아도 컴파일러는 이에 대해 오류를 뱉어내지 않는다.

그리고 던질 땐 throws를 생략해도 무방하다. 자동으로 던져준다.

package com.example.jdbc.exception.advance;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UncheckedTest {

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

    /**
     * 언체크 예외는 예외를 잡거나 던지지 않아도 된다. 잡지 않으면 자동으로 밖으로 던진다.
     * */
    static class Service {
        private final Repository repository = new Repository();

        public void callCatch() {
            try {
                repository.call();
            } catch (MyUncheckedException e) {
                log.info("예외 처리 message = {}, ", e.getMessage(), e);
            }
        }

        public void callThrows() {
            repository.call();
        }
    }

    /**
     * 언체크 예외는 에러를 던질 때 throws가 생략 가능하다.
     * */
    static class Repository {
        public void call() {
            throw new MyUncheckedException("ex");
        }
    }
}

 

설명: MyUncheckedException은 RuntimeException을 상속받는다. 이 RuntimeException을 상속받으면 언체크 예외가 된다. 그리고 Repository와 Service를 만들고 Repository의 call() 메소드는 언체크 예외를 던진다. 이 call() 메소드를 호출하는 Service는 해당 예외를 잡을수도 던질수도 있으며 잡는 코드는 callCatch() 메소드이며 던지는 코드는 callThrows() 메소드이다. 마찬가지로 던질땐 throws를 생략할 수 있기 때문에 callThrows()에도 따로 throws를 작성하지 않아도 된다. 

 

이를 호출하는 테스트 코드를 작성해보자.

package com.example.jdbc.exception.advance;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

class UncheckedTestTest {
    @Test
    void unchecked_catch() {
        UncheckedTest.Service service = new UncheckedTest.Service();
        service.callCatch();
    }

    @Test
    void unchecked_throws() {
        UncheckedTest.Service service = new UncheckedTest.Service();


        assertThatThrownBy(service::callThrows)
                .isInstanceOf(UncheckedTest.MyUncheckedException.class);
    }
}

 

unchecked_catch() 메소드는 서비스가 가지는 callCatch() 메소드를 호출한다. 이 callCatch() 메소드는 내부에서 언체크 예외를 잡아서 정상 흐름을 반환하고 그대로 테스트는 통과한다. 그러나, unchecked_throws() 메소드는 서비스가 가지는 callThrows() 메소드를 호출한다. 이 callThrows() 메소드는 내부에서 언체크 예외를 잡지 않고 던지기 때문에 이를 호출한 unchecked_throws() 메소드 역시 잡거나 던지거나 둘 중 하나를 해야한다. 그래서 언체크예외가 던져지는 것을 확인하는 Assertions을 작성한다.

 

 

언체크 예외 장단점

  • 장점: 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws를 선언해야 하지만 언체크 예외는 이 부분을 생략할 수 있다. 
  • 단점: 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 그리고 실제 서비스가 실행되는 시점에 언체크 예외가 터지면 애플리케이션에서 오류를 뱉어낸다. 런타임 시에 발생하는 에러, 다시 말해 사용자가 수행한 행위가 에러로직을 발생할 때 일어나는 에러가 가장 최악의 에러인데 이 최악의 에러를 발생시킬 수 있다.

 

체크 예외와 언체크 예외를 언제 사용할까?

기본적으로 언체크 예외를 사용하자.

 

이것이 대원칙이다. 대원칙은 기본으로는 언체크 예외를 사용하고, 비즈니스 로직 상 반드시 처리해야 하는 문제일 때만 체크 예외를 사용하자. 예를 들면 다음과 같다

  • 계좌 이체 실패 예외
  • 결제시 포인트 부족 예외
  • 로그인 ID, PW 불일치 예외

그렇다고 저 예시들도 반드시 체크 예외를 사용해야 하는게 아니라, 정말 정말 처리를 반드시 해줘야하는 경우에 체크 예외를 사용하면 된다는 것이다. 그것은 비즈니스 요구사항과 프로젝트 별로 달라지기 때문에 프로젝트마다 적절히 판단하는 것이 중요하다.

 

그런데 얼핏 말만 들어보면 체크 예외는 컴파일 시 오류도 잡아주고 발생할 수 있는 예외를 미리 알려주기까지 하는데 왜 체크 예외를 기본으로 사용하는게 아니고 언체크 예외일까?

 

체크 예외의 문제점

 

위 그림을 보자. 서비스에서 Repository, NetworkClient를 호출하는데 각각 체크 예외 (SQLException, ConnectException)을 던진다고 해보자. 그럼 두 곳에서 올라오는 체크 예외를 서비스가 처리할 수 있을까? 만약, DB가 터져서 SQLException을 서비스가 받으면 서비스가 그 에러를 처리할 수 있을까? 못한다. 아무것도 할 수 있는게 없다. 또 다른 예로 만약 네트워크가 일시적 문제가 생겨서 ConnectException을 서비스가 받으면 그 체크 예외를 처리할 수 있을까? 못한다. 아무것도 역시 할 수 있는게 없다. 이럴 땐 서비스 역시 저 두개의 에러가 체크 예외이기 때문에 던져야한다. 던지면 컨트롤러가 받는다. 컨트롤러라고 뭘 할 수 있을까? 못한다. 할 수 있는게 없다.그럼 이 처리할 수 없는 체크 예외 때문에 컨트롤러가 100개면 100개 모두 throws 선언을 해야하는거고 서비스가 200개면 200개 모두 throws를 선언해야 하는 불필요한 공수가 들어간다. 또한 DB가 문제가 생겨서 아무것도 할 수 없는데 throws만 선언한다고 능사가 아니다. 

 

이런 네트워크 에러나 DB관련 문제는 보통 사용자에게 어떤 문제가 발생했는지 설명하기가 어렵다. 가령, 사용자에게 "DB가 터졌습니다. 😢" 이런 에러를 보여줄 수 있을까? 어떤 사이드 이펙트가 생길지 알고 저런 에러 메시지를 보낼까? 안된다. 그래서 이렇게 해결이 불가능한 공통 예외는 별도의 오류 로그를 남기고 개발자가 최대한 빠르게 인지할 수 있도록 하여야 하는 것이지 체크 예외라고 능사가 아니다.

 

체크 예외의 또다른 심각한 문제는 의존 관계 문제이다. 위처럼 복구가 불가능한 예외(SQLException, ConnectException)가 체크 예외가 된다면 서비스 입장에서는 잡거나 던져야하는데 처리할 수 없으니 던질 수 밖에 없다. 그럼 그 던져진 예외를 받는 컨트롤러역시 잡거나 던져야한다. 이 컨트롤러에서도 역시 처리할 수 없으니 던지게 되는데 이 때가 의존 관계 문제가 생기는 지점이다. 즉, 컨트롤러는 java.sql.SQLException을 의존하게 된다. 이게 왜 문제가 될까? 만약 향후에 JDBC 기술이 아닌 다른 기술로 변경한다면, 예를 들어 JDBC에서 JPA로 변경한다면 컨트롤러의 throws 코드를 모두 JPAException으로 변경해야 한다. 

 

어? 그럼 그냥 throws Exception으로 하면 되는거 아닌가요?

 

이 방법은 의존 관계에 상관없이 모든 체크 예외를 처리해줄 수 있기 때문에 위에서 말한 의존 관계 문제 자체는 해결해줄 수 있을지 모른다. 그러나, 이는 또 다른 심각한 문제를 야기한다. 이 Exception은 모든 체크 예외의 부모이기 때문에 정말 확실하게 체크하고 넘어가야 하는 체크 예외를 놓칠 수 있게 된다. 그래서 throws Exception은 절대 절대 사용하지 말자. 이건 매우 좋지 않은 안티 에러 패턴이다.

 

체크 예외를 다루는 예시 코드를 한번 보자.

체크 예외 예시 코드

package com.example.jdbc.exception.advance;

import java.net.ConnectException;
import java.sql.SQLException;

public class CheckedAppTest {

    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("ex");
        }
    }
}

 

Service는 Repository와 NetworkClient의 메소드를 모두 호출해야 한다. Repository는 call() 메소드가 있고 이 메소드는 SQLException을 throws로 선언했다. 즉, 체크 예외를 던진다는 의미이다. 그리고 NetworkClient는 call() 메소드가 있고 이 메소드는 ConnectException을 throws로 선언했다. 마찬가지로, 체크 예외를 던진다는 의미이다. 

 

이 때, Service에서 logic() 메소드는 Repository와 NetworkClient가 던지는 SQLException과 ConnectException을 처리할 능력이 없다. 사실상 처리할 수 있는 뚜렷한 방법도 없다. 그렇기 때문에 서비스 역시 이 에러들을 던진다. 그럼 Controller는 서비스를 호출하고 서비스가 던진 체크 예외를 받는다. 이 때 컨트롤러 역시 해당 에러들을 처리할 수 있는 능력은 없다. 그렇기에 또 던진다. 여기서 서비스와 컨트롤러에 의존 관계 문제가 생긴다. 즉, 컨트롤러와 서비스 각각이 java.sql.SQLException 의존 관계가 생기는 것. 그리고 모든 메소드마다 throws 선언을 해야하는 번거로움은 덤이다.

 

 

이런 체크 예외에 대한 문제를 해결하기 위해 대원칙으로 언체크 예외를 기본으로 사용하자고 했다. 언체크 예외를 사용하는 코드를 예시로 보자.

언체크 예외 예시 코드

package com.example.jdbc.exception.advance;

import java.net.ConnectException;
import java.sql.SQLException;

public class UnCheckedAppTest {

    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) {
                throw new RuntimeSQLException(e);
            }
        }

        public void runSQL() throws SQLException {
            throw new SQLException("ex");
        }
    }

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

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

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

 

설명: 이제 SQLException과 ConnectException을 언체크 예외로 변경하기 위해 RuntimeException을 상속받는 RuntimeSQLException, RuntimeConnectException 객체를 만들었다. 그리고 Repository는 runSQL() 메소드에서 SQLException을 던지는데 이 runSQL() 메소드를 호출하는 call() 메소드에서 이 던져진 SQLException을 RuntimeSQLException으로 치환하여 던진다. 이 때 RuntimeSQLException은 언체크 예외이기 때문에 throws를 선언하지 않아도 아무런 문제가 없다. (물론 선언하여 IDE의 도움을 받아 좀 더 명확하게 어떤 에러를 던질 수 있는지 확인하면 좋은 방법이 될 수 있다.) NetworkClient 객체가 가지고 있는 call() 메소드는 이제 RuntimeConnectException을 던진다. 이 또한 언체크 예외이기 때문에 throws를 선언하지 않아도 된다. Service에서는 이 두 객체의 각각의 call() 메소드를 호출한다. 저 둘이 던질 수 있는 언체크 예외가 있어도 따로 다루지 않아도 된다. 그렇기에 의존 관계 문제도 사라진다. 그렇기에 throws를 일일이 선언할 필요도 없다. 이 서비스를 호출하는 Controller 역시 마찬가지다. 

 

이제 런타임 에러로 변환하여 DB, 네트워크 에러처럼 발생하면 어떤 방법으로도 처리를 할 수 없는 경우에 적절히 에러 로그를 남긴 후 그냥 에러를 사용자에게 적절하게 알리고 개발자가 그 에러를 빠르게 대응하는 방식으로 운영할 수 있다. 그렇게 해야만 하고 그렇게밖에 할 수 없다. 그렇지만 의존 관계 문제라던가 발생 가능한 모든 예외를 처리해야 하는 체크 예외의 번거로움은 사라졌다.

 

그리고 이 언체크 예외를 공통으로 처리하는 부분이 따로 있으면 된다.

Database에서 생긴 에러라던가, Network 에러는 어차피 에러를 잡아도 복구할 수 없다. 이런것들을 체크 예외로 만들면 호출하는 쪽에서만 고생할 뿐이다. 체크 예외는 잡거나 던지지 않으면 컴파일 에러가 나니까 무조건 건드려줘야 하는 부분이라 그렇다. 그러니까 언체크 예외로 발생 가능성이 있는 에러를 정의하고 사용하는 쪽에서 에러가 나도 처리할 필요 없다. 처리할 수도 없을 뿐더러. 

 

그리고 그 에러를 공통으로 처리하는 부분이 있으면 되는것이다. 가령, 사용자에겐 "서버에 문제가 발생했다"는 안내 문구를 보여주고 개발자는 발생한 로그를 통해 빠르게 에러를 수정하는 방향으로 구현하는게 가장 좋은 방법이다.

 

그러나, 위 코드로 변경할 때 (체크 예외에서 언체크 예외로 예외 변경) 주의할 점이 있다.

주의할 점

기존 예외를 반드시 스택 트레이스에 추가를 해줘야한다. 그렇지 않으면 실제로 에러가 난 원인을 알 수가 없다.

 

아래 코드를 보자.

package com.example.jdbc.exception.advance;

import java.net.ConnectException;
import java.sql.SQLException;

public class UnCheckedAppTest {

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

        public void request() {
            try {
                service.logic();
            } catch (Exception e) {
                log.error("error: ", e);
            }
        }
    }

    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) {
                throw new RuntimeSQLException();
            }
        }

        public void runSQL() throws SQLException {
            throw new SQLException("ex");
        }
    }

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

    static class RuntimeSQLException extends RuntimeException {
        public RuntimeSQLException() {
        }

        public RuntimeSQLException(String message) {
            super(message);
        }

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

 

여기서 중요한 부분은 바로 이부분이다.

public void call() {
    try {
        runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException();
    }
}

 

이 부분이 체크 예외를 언체크 예외로 변경하는 부분이다. 실제 체크 예외인 SQLException을 RuntimeSQLException으로 언체크 예외로 변경한다. 변경 자체에는 아무런 문제가 없지만 변경하면서 실제 에러에 대한 내용을 언체크 예외에 포함하지 않아버렸다. 이 때 에러가 발생하면 실제 SQLException 예외가 어떤식으로 왜 발생했는지 알 턱이 없다. 실행해보자.

 

실행 코드

package com.example.jdbc.exception.advance;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;


class UnCheckedAppTestTest {

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

}

 

결과

 

결과에는 RuntimeSQLException에 대한 내용만 나오지 실제 에러의 원인인 SQLException에 대한 내용이 스택 트레이스에 빠져있다. 

이러면 이 에러가 발생해도 뭐때문에 에러가 났는지 알 수 없다. 그러니 반드시 반드시 스택 트레이스에 원래 에러를 포함시켜야한다.

 

포함하는 방법은 다음처럼 생성자에 에러를 추가해주면 된다.

public void call() {
    try {
        runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException(e);
    }
}

 

이 RuntimeSQLException의 생성자는 여러가지가 있지만 Throwable 객체를 받는 생성자가 있다. 그 생성자를 이용하면 스택 트레이스에 생성자로부터 받은 에러를 포함시킬 수 있다. 

static class RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException() {
    }

    public RuntimeSQLException(String message) {
        super(message);
    }

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

 

 

스택 트레이스에 실제 원인을 가지는 에러를 포함시킨 후 다시 실행해보자.

스택 트레이스에 에러를 포함한 결과

 

스택 트레이스에 SQLException의 에러 내용이 포함되어 있음을 확인할 수 있다. 이렇게 반드시 체크 예외를 언체크 예외로 변경 시에는 기존 에러를 스택 트레이스에 넣어줘야 한다.

참고로, 에러를 찍을때 e.printStackTrace() 이런거 쓰지말자. 이게 System.out으로 찍히는 것인데 그렇게 찍으면 안되고 로그로 찍어야한다. 운영상에 System.out은 절대 사용하지 않아야 한다.

 

728x90
반응형
LIST