ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java의 Error와 Exception 그리고 예외처리 전략
    Language/Java 2020. 9. 17. 07:27

    우아한테크코스(이하 우테코) 레벨 1 복습을 위한 글입니다.

     

     

    자바는 오류가 발생하거나 발생할 여지가 있는 여러 상황에 대한 예외를 만들어두었습니다. 우리에게 익숙한 IllegalArgumentException을 비롯해 원인을 찾기 힘든 NullPointerException도 있습니다. 또한 파일 처리를 하다 보면 발생하기도 하는 IOException도 있구요. 이번 글에서는 이런 예외에 대해 알아보도록 하겠습니다.

     


     

    오류와 예외란

    예외를 알아보기 위해서는 우선 오류와 예외를 구분할 필요가 있습니다.
    오류(Error)는 시스템이 종료되어야 할 수준의 상황과 같이 수습할 수 없는 심각한 문제를 의미합니다. 개발자가 미리 예측하여 방지할 수 없습니다.
    반면 예외(Exception)는 개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해 발생합니다. 오류와 달리 개발자가 미리 예측하여 방지할 수 있기에 상황에 맞는 예외처리(Exception Handle)를 해야합니다.

    이렇게 오류와 예외에 대해 간단하게 알아보았습니다. 이제는 구성에 대해 알아보도록 하겠습니다.

     

    오류와 예외의 상속 관계

     


     

    Throwable이 왜 여기서 나와?

    오류와 예외 모두 자바의 최상위 클래스인 Object를 상속받습니다. 그리고 그 사이에는 Throwable이라는 클래스와 상속관계가 있네요. 이 클래스에 대한 공식문서를 읽어보면 이 클래스의 객체에 오류나 예외에 대한 메시지를 담는다는 이야기가 나옵니다. 그리고 예외가 연결될 때(chained exception) 연결된 예외의 정보들을 기록하기도 한다고 합니다. 이 Throwable 객체가 가진 정보와 할 수 있는 행위는 getMessage()printStackTrace()라는 메서드로 구현되어 있는데요. 당연히 이를 상속받은 Error와 Exception에서 두 메서드를 사용한다는 사실은 유추할 수 있을 겁니다. 우리가 실제로 보는 에러 코드들은 이 두 메서드를 이용해서 콘솔상에 나타납니다.

     

    Throwable의 getMessage()를 사용되는 부분들. 아래에는 더 많습니다. 진짜 많아요.

     

    지금까지 알게 된 정보를 정리해보겠습니다. 우선 오류와 예외의 의미적 차이를 구분했습니다. 그리고 구조를 알아보는 과정에서 Throwable이라는 상위 클래스에 대해서도 알아봤습니다. 이 과정에서 오류와 예외는 메시지를 담을 수 있고 어떤 원인에 의해 발생했는지를 연결해 출력해줄 수 있다는 사실도 알게 되었습니다. 이제는 본격적으로 오류와 예외를 각각 알아보도록 하겠습니다.

     


     

    오류란

    우선 몇 가지 오류에 대해서 알아보겠습니다. 

     

    Error의 상속 관계도

     

    - StackOverflowError : 호출의 깊이가 깊어지거나 재귀가 지속되어 stack overflow 발생 시 던져지는 오류입니다.
    - OutOfMemoryError : JVM이 할당된 메모리의 부족으로 더 이상 객체를 할당할 수 없을 때 던져지는 오류입니다. Garbage Collector에 의해 추가적인 메모리가 확보되지 못하는 상황이기도 합니다.

    앞서 말했듯 개발자가 미리 오류를 대처하기는 힘듭니다. 다만 StackOverflowError를 피하기 위해서 재귀를 사용할 때에 조심하거나 가시적인 loop를 사용하는 간접적인 예방이 가능합니다. 또한 OutOfMemoryError를 피하기 위해 새는 메모리를 차단하거나 heap의 크기를 늘려주는 방법을 사용할 수도 있습니다. 후자는 실제로 사용해보지 않아서 적절한 방법인지에 대해서는 더 찾아보도록 하겠습니다.

    자바 개발자가 프로그래밍을 하며 더 신경 써야 하는 부분은 예외 처리기 때문에 바로 예외에 대해서 알아보겠습니다.

     


     

    예외란

    우선 몇 가지 예외에 대해서 알아보겠습니다.

     

    Exception의 상속 관계도

     

    - NullPointerException : 객체가 필요한 경우인데 응용프로그램이 null을 사용하려고 시도할 경우 던져지는/던질 수 있는 예외입니다.
    - IllegalArgumentException : 메서드가 허가되지 않거나 부적절한 argument를 받았을 경우에 던져지는/던질 수 있는 예외입니다.

    위의 두 예외에 대한 설명에서 굵은 글씨로 표기된 던질 수 있는이라는 문장에 내포된 뜻이 있습니다. 바로 응용 프로그램의 로직이 진행되다가 개발자가 임의로 예외를 던질 수 있다는 의미입니다. 앞선 에러와는 다른 개념이기에 강조해봤습니다.

     

    // 현재 우테코에서 진행중인 웹 서버 만들기 미션에서 가져온 예외를 던지는 메서드
    // 내가 만든 ResourceNotFoundException를 던진다.
    
    	public static String getDirectoryEndsWith(String path) {
            return Arrays.stream(values())
                .filter(staticFile -> path.endsWith(staticFile.staticFileExtension))
                .findFirst()
                .orElseThrow(() -> new ResourceNotFoundException("해당 path의 파일이 있는 directory를 찾을 수 없습니다. " + path))
                .getDirectory();
        }

     

    이번에는 예외 내에서 특징에 따라 종류를 분류해보겠습니다.
    위에 첨부한 그림에서 확인하실 수 있듯 예외는 RuntimeException과 그 이외의 예외로 나뉘어있습니다. 이 둘의 차이에 대해서 알아보는 시간을 가지겠습니다. 

     


     

    Checked Exception, Unchecked Exception

    Checked Exception은 Complie Exception이라고도 하며 Exception을 바로 상속받습니다.
    특징으로는 컴파일 시점에 예외를 catch 하는지 정적으로 확인합니다. 만일 컴파일 시점에 예외에 대해 처리(try/catch) 하지 않는다면 컴파일 에러가 발생합니다. 또한 트랜잭션 Rollback이 안된다는 속성도 있습니다.
    예외가 발생하는 메서드에서 throws 예약어를 활용하여 예외를 호출한 메서드에 전달하는 방법으로 처리가 가능합니다.

    // Exception을 상속받아 구현한 Checked Exception
    // Position이라는 객체의 positon이 유효하지 않을 경우 던져진다.
    
    public class InvalidPositionException extends Exception {
    	public InvalidPositionException(String message) {
        	super(message);
        }
    }

     

    // InvalidPositionException가 발생하면 해당 생성자를 호출한 메서드로 Exception을 전달하기 위해 throws 예약어를 사용한다.
    
    public class Position {   
        public Position(String position) throws InvalidPositionException {
            if (position.length() != 2) {
                throw new InvalidPositionException(position + "은 위치 값 형식에 맞지 않습니다.");
            }
            
            x = (int) (position.charAt(0) - 'a');
            y = Integer.parseInt(position.substring(1))-1;
        }
        
        [...]
    }

     

     

    Unchecked Exception은 RuntimeException을 상속받습니다.
    컴파일 시점에 예외를 catch 하는지 확인하지 않습니다. 그렇기에 컴파일 시점에 예외가 발생하는지 여부를 판단할 수 없습니다.
    또한 트랜잭션 Rollback이 된다는 속성도 있습니다.
    예외가 발생하는 메서드에서 throws 예약어를 활용해 예외를 처리할 필요가 없습니다. 하지만 처리해도 괜찮습니다. 즉 명시적으로 예외처리를 강제하지 않습니다.

    // RuntimeException을 상속받아 구현한 Unchecked Exception
    // HttpRequest가 유효하지 않을 경우에 던져진다.
    
    public class InvalidHttpRequestException extends RuntimeException {
        public InvalidHttpRequestException(String message) {
            super(message);
        }
    }

     

    // InvalidHttpRequestException을 사용. 
    // throws로 던져지는 IOException은 bufferedReader.readline()에서 던지는 예외이다.
    
    public static String extractRequestLine(BufferedReader bufferedReader) throws IOException {
        String requestLine = bufferedReader.readLine();
        if (requestLine == null) {
            throw new InvalidHttpRequestException("request line이 없습니다.");
        }
    
        return requestLine;
    }

     

    이렇게 두 종류의 예외의 정의에 대해서 알아봤습니다. 그렇다면 어떨 때에 어떤 예외를 사용할 수 있을까요?

     


     

    예외의 사용 

    예외가 발생할 여지가 있는 메서드를 호출하는 메서드가 예외를 활용해 무엇인가 의미 있는 작업을 할 수 있다면 Checked Exception을 활용할 수 있습니다. 예외 처리에 대한 책임을 확실하게 넘기는 거죠.
    하지만 호출하는 메서드가 예외 상황이나 문제를 해결할 수 없다면 Unchecked Exception을 활용할 수 있습니다. 호출된 메서드에서 예외를 터트려 개발자나 사용자가 에러를 처리할 수 있도록 하는 것이지요.
    저는 둘 중 어떤 상황인지 확실하게 결정할 수 없다면 우선 Unchecked Exception을 사용하는 편입니다.

    Checked Exception을 처리하기 위해서는 throws를 이용해서 피호출 메서드에서 호출하는 메서드로 예외를 던진다고 정리할 수 있습니다. 이 "던짐"은 해당 예외를 처리할 수 있는 메서드까지 던져지게 될 것입니다. 하지만 이런 무분별한 throws의 활용은 코드의 가독성을 떨어트림과 더불어 어떤 메서드의 어떤 부분에서 예외가 발생했는지 알기 어렵게 만드는 주원인입니다.
    이럴 경우에는 try문 안에서 발생하는 Checked Exception을 catch문 안에서 Unchecked Exception으로 바꿔주는 방법을 적용해볼 수 있습니다. 그 결과 예외가 발생한 메서드에서 예외를 처리하거나 개발자 혹은 사용자에게 처리를 위임할 수 있습니다.

    // 앞선 예시 코드를 수정하여 활용
    // bufferedReader.readline()에서 던질 수 있는 IOException을 try-catch로 잡아 UncheckedException으로 바꿔서 던진다.
    
    public static String extractRequestLine(BufferedReader bufferedReader) {
        try {
        	String requestLine = bufferedReader.readLine();
            if (requestLine == null) {
                throw new InvalidHttpRequestException("request line이 없습니다.");
        	}
    
          return requestLine;
        } catch (IOException e) {
            throw new InvalidHttpRequestException("입력 값이 잘못되어 RequestLine 객체를 생성할 수 없습니다.");
        }
    }

     

    이 경우에는 예외가 발생하는 곳에서 예외가 던져지기에 예외를 추적하기 용이하다는 장점이 있습니다. 위와 같이 구현했을 경우 예외가 로그로 남거나 클라이언트에 전달될 것입니다. 클라이언트는 예외 응답을 받아서 사용자를 위해 알림과 같은 방법으로 표시할 수 있습니다. 그리고 사용자는 다시 올바른 입력을 시도할 것입니다.

     


     

    이렇게 오류와 예외 그리고 예외 처리하는 방법에 대해 간단하게 알아보았습니다. 레벨 1 때에 배웠던 내용이지만 어렴풋하게만 기억에 남아서 복습을 진행 중인데 리마인드 할 수 있는 기회가 되는 것 같네요. 배운 내용을 바탕으로 추가적인 조사를 하여 저만의 언어로 선보이는 게 즐겁고 유익합니다. 
    오류나 부족한 부분은 댓글로 남겨주시면 함께 논의해볼 수 있을 것 같습니다. 읽어주셔서 감사합니다.

     

     

    출처
    Java Platform, Standard Edition & Java Development Kit Version 14 API Specification 
    우아한 테크코스 레벨 1, 자바 Exception 교육 자료
    자바 예외 구분: Checked Exception, Unchecked Exception
    Checked Exception을 대하는 자세

    'Language > Java' 카테고리의 다른 글

    String과 StringBuilder  (0) 2020.08.02
    학습 테스트와 단위 테스트  (0) 2020.07.20
    Production Code & Test Code & JUnit  (0) 2020.07.09

    댓글

Toneyparky Blog