Language/자바&코틀린

자바 예외의 종류와 처리방식

코딩공장공장장 2024. 2. 19. 22:37

Error와 Exception


자바에서 프로그램이 비정상적인 동작을 일으키는 경우 Error 또는 Exception이 발생하게 된다.

  Error Exception
Checked Exception UnChecked Exception
복구 여부 복구 불가 복구 가능
예외 종류 컴파일 타임 - 문법 오류
- 타입 체크
IOException  
런타임 - StackOverFlowError
- OutOfMemoryError
  Runtime Exception 하위
예외 처리 처리불가   컴파일러에 의해
예외 처리 강제
예외 처리 강제화 되지 않음

 

예외 상속관계

 

Error와 Exception은 모두 Throwable 클래스를 상속 받는다

Throwable 클래스는 message와 stackTrace를 담고 있다.
message는 
Error와 Exception 정보를 간단히 제공하는 메시지이고 

stackTrace는 StackTraceElement의 객체로 비정상 동작이 발생한 파일, 메서드, 라인수와 같은 구체적인 정보를 가지고 있다.

Error와 Exception 모두 Throwable을 상속하고 있으니 message와 stackTrace 정보에 접근할 수 있는 것이다.

 

Exception는 체크예외와 언체크예외로 다시 한번 나뉘게 되는데 언체크 예외는 예외처리가 강제화 되지 않는다. 

따라서 catch문으로 예외를 잡아서 반드시 처리해야하거나, 처리하지 않을시 thorw를 최상위 호출부까지 선언하는 코드를 작성하지 않아도 된다.

 

예외 처리 방식


1. 예외회피

예외 처리는 우리가 다 알고 있듯 try-catch문을 통해 작성한다.

예제를 통해 살펴보자.

public class NumberWriter {
    public void write() {
        String fileName = "numbers.txt";

        try(OutputStream outputStream = new FileOutputStream(fileName)) {
            for (int i = 1; i <= 10; i++) {
                String numberAsString = i + "\n";
                byte[] bytes = numberAsString.getBytes(StandardCharsets.UTF_8);
                outputStream.write(bytes);
            }
        } catch (IOException e) {
            System.out.println("exception throw : " + e.getMessage());
        }
    }
}

FileOutputStream은 FileNotFoundException 처리를 강제화하고 OutputStream의 write 함수는 IOException을 강제화 한다.

따라서 해당 블록을 try-catch로 감싸서 처리하였다.

 

만일 try-catch를 사용하지 않는다면 체크 예외는 호출부에서 throw 처리를 진행해야한다.

중간에 catch 하지 않는다면 예외는 결국 최상위 Main 클래스에까지 전달될 것이다.

public class NumberWriter {
    public void write() throws IOException {  // throw 선언
        String fileName = "numbers.txt";

        OutputStream outputStream = new FileOutputStream(fileName);
        for (int i = 1; i <= 10; i++) {
            String numberAsString = i + "\n";
            byte[] bytes = numberAsString.getBytes(StandardCharsets.UTF_8);
            outputStream.write(bytes);
        }
        outputStream.close();
    }
}
public class Main {
    public static void main(String[] args) throws IOException { // throw 선언
        new NumberWriter().write();
    }
}

위와 같이 예외를 어느쪽에서도 처리하지 않고 호출한 쪽으로 넘기는 방식예외 회피라고 한다.

throw코드를 통해 예외를 호출부로 지속적으로 던지는 것은 좋지 않다.

예를 들어 ‘컨트롤러-서비스-DB 영속화’와 같은 계층이 있다

영속화 관련 체크예외 발생시 throw를 비즈니스 계층과 웹(프레젠테이션) 계층까지 명시한다면 결합도가 높은 구조를 갖게 된다.
만일 위 구조에서 다른 영속화 프레임워크를 사용하게 되어 예외타입이 바뀌면 비즈니스 계층과 웹 계층 모두 수정을 필요로 한다.

위와 같이 throws 를 통해 예외가 지속적으로 전파되게 되면 코드 파악에 어려움을 만드는 것 뿐만 아니라, 예외로 인해 객체 간의 의존성을 높이는 구조를 유발한다.

 

2. 예외 전환

예외 회피로 인해 예외가 명시적으로 전파되는 것이 좋지 않다면 이를 어떻게 해결해야하나.

그 해결 방법이 예외전환이 될 수 있다.

예외전환이란 예외를 catch하여 다른 예외를 발생시키는 것이다.

특정 계층이나 라이브러리에 종속된 예외가 다른 계층에 전파되는 것을 막기 위해 다른 예외로 전환하여 전파하는 방식이다.

 

보통 예외전환을 직접 처리하는 경우는 의존도를 낮추기 위함이다. 

DB 예외가 웹 계층까지 전달되지 않거나, 라이브러리의 예외가 비즈니스 계층까지 전달되지 않도록 라이브러리에 종속되지 않는 예외를 발생시켜 의존도를 낮추는 것이다.

 

[예제]

아래와 같은 코드가 있다고 하자.
JpaRepositoryImp이 호출하는 saveDto 메서드 내부에서 JpaCheckException이 발생한다고 해보자.

public class JpaRepositoryImpl {
   public boolean save(BusinessDto businessDto) throws JpaCheckException {
       return saveDto(businessDto);
   }
}
public class BusinessServiceImpl {
   . . .

   public boolean save(BusinessDto businessDto) throws JpaCheckException {
       return jpaRepository.save(businessDto);
   }
}
public class WebController {
   . . .

   public Boolean save(BusinessDto businessDto) throws JpaCheckException {
       return businessService.save(businessDto);
   }
}

 

영속화 계층에서 throw한 체크 예외로 인해 서비스 계층과 컨트롤러 계층까지 영속화 예외에 대해 throw를 선언하였다.

만일 위 상황에서 JpaRepository가 MybatisRepository로 바뀌어 예외 타입이 MybatisCheckException으로 바뀌었다고 해보자.
WebController와 BusinessSerivce에 선언된 throws JpaCheckException을 모두 MybatisCheckException으로 변경해줘야한다.

체크예외의 경우 compiler가 잡아주기에 변경에 용이함은 있겠지만 다른 클래스에 영향을 미치는 것은 좋지 못한 구조이다.

애초에 이런 상황이 발생하더라도 영향도가 없게끔 아래와 같이 라이브러리에 종속되지 않은 런타임 예외로 전환하면  다른 계층에 예외가 전파되는 것을 막을 수 있다.

public class JpaRepository {
   public boolean save(BusinessDto businessDto)  {
       try{
           return saveDto(businessDto);
       } catch (JpaCheckException e) {
           throw new PersistenceSaveException("businessDto save 오류 id:" + businessDto.id );
       }
   }
}
public class BusinessServiceImpl {
   . . .
   public boolean save(BusinessDto businessDto){
       return jpaRepository.save(businessDto);
   }
}
public class WebController {
   . . .

   public Boolean save(BusinessDto businessDto){
       return businessService.save(businessDto);
   }
}

 

예외처리 고려사항


예외처리에 고려해야하는 사항들이 많이 있겠지만 개인적으로 중요하다고 생각하는 것을 정리하겠다.

  1. 예외 의존성 약화
    예외 또한 객체가 처리해야하는 책임이다. 예외가 다른 객체로 전파되는 것은 마냥 좋지 않다.
    특히 특정 라이브러리에 종속된 예외가 여기저기 전파된다면 라이브러리 변경시 전파된 모든 객체에서 수정을 필요로 한다.
    이런 상황에서 라이브러리에 종속되지 않은 커스텀 예외로 전환하게 되면 라이브러리 변경시 수정사항을 최소화 할 수 있다.

  2. 구체적인 예외로 전환
    만일 라이브러리 종속 예외를 커스텀 예외로 바꾸는 것을 고려한다면 예외 발생 상황을 구체적으로 알 수 있는 커스텀 예외를 만들자.
    이는 예외 상황에 대한 디버깅을 수월하게 할 수 있게 해준다.
    만일 상황에 따른 커스텀 예외를 만드는 것이 너무 많은 클래스를 생성하게 하여 오히려 유지보수에 복잡성을 일으킨다면 아래와 같이 예외 메시지에 구체적인 상황을 명시하는 것도 좋은 방법이 될 수 있다.
    throw new PersistenceSaveException("businessDto save 오류 id:" + businessDto.id );

  3. 체크 예외를 언체크 예외로 전환
    예외 처리를 별도로 진행할 수 있는 것이 없다면 체크예외를 통한 예외 처리를 강제화하는 것보다 언체크 예외로 전환시켜 예외 처리를 강제화하지 않게 할 수 있다.
    예외 처리 할 수 있는 방안이 딱히 없다면 이 또한 고려해볼만한 방법이다.
    이외에도 스프링 프레임워크에서만 해당되는 상황인데 체크예외의 경우 롤백을 지원하지 않는다.
    롤백을 사용하기 위해서 체크 예외를 언체크 예외로 전환할 수 있다.

  4. 예외 복구
    예외 발생 시 예외 이전 상황으로 복구가 가능한 처리를 구현하는 것을 예외 복구라고 한다.
    대표적인 예가 http 통신 상황에서 예외 발생시 몇번 더 시도를 해보는 것이 있다.
반응형