본문 바로가기

Web Sever 개발과 CS 기초/스프링

자바 예외에 대한 이해

관련 내용

해당 프로젝트 깃허브

 

<JDBC, Spring - Transaction 처리에 대한 완벽한 이해>

  • 순수 JDBC만을 사용한 Transaction 사용 방법 알아보기

JDBC를 사용한 Trasaction 처리와 이해

  • 순수 JDBC-Transaction 문제 Spring TransactionManger로 해결하기 - 트랜잭션 템플릿, 트랜잭션 AOP 사용

JDBC Trasaction문제 Spring으로 해결하기

  • Spring Transaction AOP 동작 과정과 주의 사항 알아보기

스프링 Trasaction AOP 동작 과정과 주의 사항

  • Spring Transaction 전파 원리 이해하기

Spring Transaction 전파 이해하기

  • Spring을 사용한 JDBC 예외 누수 문제 해결하기

Spring을 사용한 JDBC 예외 누수 문제 해결하기

  • (현재 글)자바 예외에 대한 이해

자바 예외에 대한 이해

개요 목적

해당 글에서 체크 예외를 언 체크 예외로 바꿔, JDBC 레포지 토리 예외 누수 문제를 해결했다.

이번 글에서는 자바 예외의 이해를 통해, 왜 예외 누수 문제를 해결하기 위해 언 체크 예외를 사용했는 지 이해해보자.

예외 계층 종류와 개념

참고, 계층도에 있는 Error도 예외라고 부른다. 예외 계층 중 하나이다.

예외는 체크 예외와 언체크 예외로 나누어진다. 아래 설명을 통해 이 둘의 개념과 차이 활용에 대해서 알아보자.

예외 기본 규칙

예외에 대한 기본적인 2가지 규칙

  1. 예외는 반드시 잡아서 처리하거나 던져야 한다.
  2. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다.
    1. 예를 들어서 Exception 을 catch 로 잡으면 그 하위 예외들도 모두 잡을 수 있다.
    2. 예를 들어서 Exception 을 throws 로 던지면 그 하위 예외들도 모두 던질 수 있다.

예외를 처리하지 못하고 계속 던지면?

자바의 main() 쓰레드까지 예외가 올라오게 되면,(Controller에서도 예외를 throws한다면) 예외 로그를 출력하면서 시스템이 종료된다. 웹 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 시스템이 종료되면 안된다. WAS가 해당 예외를 받아서 처리하는데, 주로 사용자에게 개발자가 지정한, 오류 페이지를 보여준다.

체크 예외 이해하기

Exception 과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 RuntimeException은 언 체크 예외이다.

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

언 체크 예외와 다른 체크 예외의 가장 큰 특징은 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외 를 필수로 선언해야 한다.

//이런식으로 repository.call()에서 발생하는 MyCheckedException을 넘기려면
//반드시 throws MyCheckedException 명시해줘야 한다. - 컴파일러가 체크하고 있기 때문에
public void callThrow() throws MyCheckedException {
    repository.call();
}

체크 예외의 장 단점

예외를 밖으로 던지는 throws 예외 를 필수로 선언해야 한다는 특징 때문에 장 단점이 존재한다.

  • 장점

개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치이다.

  • 단점

하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 예외를 받아서 던지는 쪽에서, 해당 예외를 반드시 throws를 해야하기 때문에 의존성이 생기는 문제가 발생한다.

언 체크 예외 이해하기

RuntimeException 과 그 하위 예외는 언 체크 예외로 분류된다. 언 체크 예외는 컴파일러가 예외를 체크하지 않는다는 뜻이다. 언체크 예외는 체크 예외와 기본적으로 동일하다. 차이가 있다면 예외를 던지는 throws 를 선언하지 않고, 생략할 수 있다. 이 경우 자동으로 예외를 던진다.

다만 언 체크 예외도 main() 쓰레드 까지 예외가 전달된다면, 예외 로그를 출력하면서 시스템이 종료된다.(기본적 동작은 방식은 같다)

언 체크 예외의 장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외 를 생략할 수 있다. 이것에 대한 장 단점이 존재 한다.

  • 장점

신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws 예외 를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있다. 그렇기 때문에 예외에 관련된 라이브러리 의존성을 제거할 수 있다. 코드 작성없이 넘길 수 있기 때문이다.

  • 단점

언 체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을 잡아준다.

체크 예외와 언 체크 예외의 예외 던지기 코드 차이 보기

//jdbc/exception/basic/CheckedTest.java
static class Service {

        Repository repository = new Repository();
        /**
         * 체크 예외를 밖으로 던지는 코드 체크 예외는 예외를 잡지 않고 밖으로 던지려면 
	 * throws 예외를 메서드에 필수로 선언해야한다.
         */
        public void callThrow() throws MyCheckedException {
            repository.call();
        }
    }

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

//jdbc/exception/basic/UncheckedTest.java
static class Service {
        
	Repository repository = new Repository();
        /**
         * 예외를 잡지 않아도 된다. 자연스럽게 상위로 넘어간다.
         * 체크 예외와 다르게 throws 예외 선언을 하지 않아도 된다.
         */
        public void callThrow() {
            repository.call();
        }
    }
    static class Repository {
        public void call() {
            throw new MyUncheckedException("ex");
        }
    }

체크 예외와 언 체크 예외를 각각 언제 사용하면 될까?

예외를 사용하는 기본 원칙은 2가지

  • 기본적으로 언 체크(런타임) 예외를 사용하자.
  • 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.

이 경우 해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야 한다. 예를 들어서 계좌 이체 실패 예외 결제시 포인트 부족 예외 로그인 ID, PW 불일치 예외

이런 경우도 무조건 체크 예외를 만들 필요는 없지만, 반드시 개발자 실수로 놓치면 안된다는 예외는 체크 예외로 만들어 컴파일러를 통해 놓친 예외를 인지 할 수 있다.

체크 예외의 문제점 → 그래서 기본적으로 언 체크 예외를 사용함

체크 예외를 사용해도 얻는 이점이 크게 없으며, 오히려 문제점이 있다. 아래 그림을 따라가면서 문제점이 무엇인 지 알아보자

  • 복구 불가능한 예외

대부분 예외는 대부분 서비스, 컨트롤러에서 복구와 처리가 불가능하다. SQL 문제나 네트워크 문제는 각 계층에서 처리하는 것이 아니라, 오류 로그를 남기고 한 곳에서 일관성 있게 처리해서 개발자가 해당 오류를 빠르게 인지하도록 해야 한다. 그래서 굳이 throws 작성 같은 번거로운 작업을 각 계층에서 할 필요가 없는 것이다.

  • 의존 관계에 대한 문제

thorws SQLException을 붙여 줘야해서 서비스와 컨트롤러 계층에서 java.sql.SQLException 을 의존하게 된다. 그런데, JDBC 기술이 아닌 다른 기술로 변경한다면, 그래서 JPAException 으로 예외가 변경된다면 의존성이 있는 서비스 컨트롤러 계층의 코드들을 전부 바꿔줘야 하는 문제가 발생한다.

체크 예외의 문제점을 해결해주는 언 체크예외 활용하기

SQLException 을 런타임 예외인 RuntimeSQLException 으로 변환했다. ConnectException 대신에 RuntimeConnectException 을 사용하도록 바꾸었다.

런타임 예외이기 때문에 서비스, 컨트롤러는 해당 예외들을 처리하는 복잡한 절차(throws)도 사라졌고, 특정 라이브러리에 대한 의존성도 사라졌다.

이제 예외에 변경이 있을 때, 예외를 공통으로 처리하는 한 곳에서만 코드 변경을 하면 문제가 해결된다.

런타임 예외(언체크 예외)를 사용할 때 주의사항

  • 런타임 예외는 문서화해야 한다.

런타임 예외는 컴파일 체크가 없기 때문에, 아래 처럼 문서화를 통해서 해당 메소드가 어떤 예외를 가지고 있는 지 잘 명시해 주어야 한다.

/**
 * Issue a single SQL execute, typically a DDL statement.
 * @param sql static SQL to execute
 * @throws DataAccessException if there is any problem
 */
void execute(String sql) throws DataAccessException;
  • 예외 포함과 스택 트레이스

체크 예외를 언 체크 예외로 변환해서 넘겨 줄 때 반드시 기존 예외를 포함해 주어야 한다.

그렇지 않으면, 예외를 처리한 곳에서 근본적인 원인 체크 예외를 확인 할 수 없게 된다.

아래 코드 처럼, (Throwable cause) 생성자를 통해서, 기존 예외를 담아 주어야 한다.

static class RuntimeSQLException extends RuntimeException {

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