아마도 코드를 실행하고 NullPointerException , ClassCastException 또는 그보다 더 나쁜 상황이 발생하는 상황을 경험한 적이 있을 것입니다 . 이것은 디버깅, 분석, 인터넷 검색 등의 긴 프로세스로 이어집니다. 예외는 있는 그대로 훌륭합니다. 예외는 문제의 본질과 발생한 위치를 나타냅니다. 기억을 되살리고 조금 더 배우고 싶다면 예외: checked, unchecked 및 custom 문서를 살펴보세요 .

즉, 고유한 예외를 만들어야 하는 상황이 있을 수 있습니다. 예를 들어 코드가 어떤 이유로 사용할 수 없는 원격 서비스에서 정보를 요청해야 한다고 가정합니다. 또는 누군가가 은행 카드 신청서를 작성하고 우연히든 아니든 이미 시스템의 다른 사용자와 연결된 전화번호를 제공한다고 가정합니다.

물론 여기에서 올바른 동작은 여전히 ​​고객의 요구 사항과 시스템 아키텍처에 따라 다르지만 전화 번호가 이미 사용 중인지 여부를 확인하고 사용 중인 경우 예외를 발생시키는 작업을 맡았다고 가정해 보겠습니다.

예외를 만들어 봅시다:


public class PhoneNumberAlreadyExistsException extends Exception {

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

다음으로 검사를 수행할 때 사용합니다.


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

예제를 단순화하기 위해 하드코딩된 여러 전화번호를 사용하여 데이터베이스를 나타냅니다. 마지막으로 예외를 사용해 봅시다.


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

이제 Shift+F10 (IDEA를 사용하는 경우)을 눌러야 합니다 . 즉, 프로젝트를 실행합니다. 콘솔에 표시되는 내용은 다음과 같습니다.

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException: 지정된 전화번호는 이미 다른 고객이 사용 중입니다!
예외에서.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

당신을 보면! 당신은 자신의 예외를 만들고 조금 테스트했습니다. 이 성과를 축하합니다! 작동 방식을 더 잘 이해하려면 코드를 약간 실험해 보는 것이 좋습니다.

다른 확인을 추가합니다. 예를 들어 전화번호에 문자가 포함되어 있는지 확인합니다. 아시다시피 미국에서는 전화번호를 기억하기 쉽게 하기 위해 1-800-MY-APPLE과 같이 문자를 자주 사용합니다. 귀하의 수표는 전화번호에 숫자만 포함되어 있는지 확인할 수 있습니다.

좋습니다. 확인된 예외를 만들었습니다. 다 괜찮고 좋겠지만...

프로그래밍 커뮤니티는 확인된 예외를 선호하는 진영과 반대하는 진영으로 나뉩니다. 양측은 강력한 주장을 펼치고 있습니다. 둘 다 최고 수준의 개발자를 포함합니다. Bruce Eckel은 확인된 예외를 비판하고 James Gosling은 이를 옹호합니다. 이 문제는 영원히 해결되지 않을 것 같습니다. 즉, 확인된 예외 사용의 주요 단점을 살펴보겠습니다.

확인된 예외의 주요 단점은 처리 해야 한다는 것입니다. 여기에는 두 가지 옵션이 있습니다. try-catch 를 사용하여 제자리에서 처리하거나 , 여러 곳에서 동일한 예외를 사용하는 경우 throws를 사용하여 예외를 발생시키고 최상위 클래스에서 처리하는 것입니다.

또한 "보일러플레이트" 코드, 즉 많은 공간을 차지하지만 무거운 작업을 많이 수행하지 않는 코드로 끝날 수 있습니다.

많은 예외가 처리되는 상당히 큰 응용 프로그램에서 문제가 발생합니다. 최상위 메서드의 throw 목록은 쉽게 확장되어 12개의 예외를 포함할 수 있습니다.

공용 OurCoolClass()는 FirstException, SecondException, ThirdException, ApplicationNameException...을 throw합니다.

개발자는 일반적으로 이것을 좋아하지 않으며 대신 트릭을 선택합니다. 모든 확인된 예외가 공통 조상인 ApplicationNameException 을 상속하도록 합니다 . 이제 핸들러에서 예외 ( checked !) 도 잡아야 합니다 .


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

여기서 우리는 또 다른 문제에 직면합니다. 마지막 catch 블록에서 무엇을 해야 합니까? 위에서 예상되는 모든 상황을 이미 처리했으므로 이 시점에서 ApplicationNameException은 " 예외 : 이해할 수 없는 오류가 발생했습니다." 라는 의미일 뿐입니다 . 다음과 같이 처리합니다.


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

그리고 결국, 우리는 무슨 일이 일어났는지 모릅니다.

하지만 이렇게 모든 예외를 한 번에 던질 수는 없나요?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

예, 할 수 있습니다. 그러나 "throws Exception"은 우리에게 무엇을 말합니까? 뭔가 고장난 것입니다. 이유를 이해하려면 위에서 아래로 모든 것을 조사하고 오랫동안 디버거와 친해져야 합니다.

"예외 삼키기"라고도 하는 구조를 만날 수도 있습니다.


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

설명을 위해 여기에 추가할 것이 많지 않습니다. 코드는 모든 것을 명확하게 하거나 오히려 모든 것을 불분명하게 만듭니다.

물론 실제 코드에서는 이것을 볼 수 없다고 말할 수 있습니다. 이제 java.net 패키지 에서 URL 클래스 의 내부(코드)를 자세히 살펴보겠습니다 . 알고 싶다면 나를 따라와!

다음은 URL 클래스 의 구조 중 하나입니다 .


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

보시다시피 MalformedURLException 이라는 흥미로운 검사 예외가 있습니다 . "프로토콜이 지정되지 않았거나, 알 수 없는 프로토콜이 발견되었거나, 사양이 null이거나, 구문 분석된 URL이 관련 프로토콜의 특정 구문을 준수하지 못하는 경우"입니다 .

그건:

  1. 프로토콜이 지정되지 않은 경우.
  2. 알 수 없는 프로토콜이 있습니다.
  3. 사양은 null 입니다 .
  4. URL이 연결된 프로토콜의 특정 구문을 준수하지 않습니다.

URL 객체 를 생성하는 메서드를 만들어 봅시다 :


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

IDE에서 이 줄을 작성하자마자(저는 IDEA로 코딩하고 있지만 Eclipse 및 NetBeans에서도 작동합니다) 다음을 볼 수 있습니다.

즉, 예외를 발생시키거나 코드를 try-catch 블록으로 래핑해야 합니다. 지금은 무슨 일이 일어나고 있는지 시각화하기 위해 두 번째 옵션을 선택하는 것이 좋습니다.


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

보시다시피 코드는 이미 다소 장황합니다. 그리고 우리는 위에서 언급했습니다. 이것이 확인되지 않은 예외를 사용하는 가장 분명한 이유 중 하나입니다.

Java에서 RuntimeException을 확장하여 확인되지 않은 예외를 만들 수 있습니다 .

확인되지 않은 예외는 Error 클래스 또는 RuntimeException 클래스 에서 상속됩니다 . 많은 프로그래머는 이러한 예외가 프로그램이 실행되는 동안 복구할 수 없는 오류를 나타내기 때문에 프로그램에서 처리할 수 있다고 생각합니다.

확인되지 않은 예외가 발생하면 일반적으로 코드를 잘못 사용하여 null이거나 유효하지 않은 인수를 전달하여 발생합니다.

자, 코드를 작성해 봅시다:


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

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

다른 목적을 위해 여러 생성자를 만들었습니다. 이를 통해 예외에 더 많은 기능을 제공할 수 있습니다. 예를 들어 예외가 발생하면 오류 코드가 표시되도록 만들 수 있습니다. 먼저 오류 코드를 나타내는 열거형을 만들어 보겠습니다.


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

이제 예외 클래스에 다른 생성자를 추가해 보겠습니다.


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

그리고 필드를 추가하는 것을 잊지 마십시오(거의 잊어버렸습니다).


private Integer errorCode;
    

물론 이 코드를 얻는 방법은 다음과 같습니다.


public Integer getErrorCode() {
   return errorCode;
}
    

확인하고 비교할 수 있도록 전체 클래스를 살펴보겠습니다.

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

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

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

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

짜잔! 예외가 완료되었습니다! 보시다시피 여기에는 특별히 복잡한 것이 없습니다. 실제로 확인해 봅시다:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

작은 애플리케이션을 실행하면 콘솔에 다음과 같은 내용이 표시됩니다.

이제 추가한 추가 기능을 활용해 보겠습니다. 이전 코드에 약간을 추가합니다.


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

객체로 작업하는 것과 같은 방식으로 예외로 작업할 수 있습니다. 물론 Java의 모든 것이 객체라는 것을 이미 알고 계실 것입니다.

그리고 우리가 한 일을 보세요. 먼저 메서드를 변경하여 이제 throw하지 않고 입력 매개 변수에 따라 단순히 예외를 생성합니다. 다음으로 switch-case 문을 사용하여 원하는 오류 코드 및 메시지와 함께 예외를 생성합니다. 그리고 main 메서드에서 생성된 예외를 받고, 오류 코드를 받고, 던졌습니다.

이것을 실행하고 콘솔에서 무엇을 얻는지 봅시다:

보세요 — 예외에서 얻은 오류 코드를 인쇄한 다음 예외 자체를 던졌습니다. 또한 예외가 발생한 위치를 정확히 추적할 수도 있습니다. 필요에 따라 모든 관련 정보를 메시지에 추가하고, 추가 오류 코드를 생성하고, 예외에 새 기능을 추가할 수 있습니다.

글쎄, 당신은 그것에 대해 어떻게 생각하세요? 나는 모든 것이 당신을 위해 해결되기를 바랍니다!

일반적으로 예외는 다소 광범위한 주제이며 명확하지 않습니다. 그것에 대해 더 많은 논쟁이있을 것입니다. 예를 들어 Java만 예외를 확인했습니다. 가장 인기 있는 언어 중에서 해당 언어를 사용하는 언어를 본 적이 없습니다.

Bruce Eckel은 그의 저서 "Thinking in Java"의 12장에서 예외에 대해 아주 잘 썼습니다. 꼭 읽어 보시기 바랍니다! 또한 Horstmann의 "Core Java"의 첫 번째 볼륨을 살펴보십시오. 7장에도 흥미로운 내용이 많이 있습니다.

작은 요약

  1. 모든 것을 로그에 기록하세요! throw된 예외에 메시지를 기록합니다. 이는 일반적으로 디버깅에 많은 도움이 되며 무슨 일이 일어났는지 이해할 수 있게 해줍니다. catch 블록을 비워 두지 마십시오 . 그렇지 않으면 예외를 "삼키고" 문제를 찾는 데 도움이 되는 정보가 없습니다.

  2. 예외에 관해서는 한 번에 모두 잡는 것은 좋지 않습니다(내 동료가 "포켓몬 이 아니라 Java입니다"라고 말함 ) .

  3. 가능한 한 빨리 예외를 던집니다. 이것은 좋은 자바 프로그래밍 습관입니다. Spring과 같은 프레임워크를 연구할 때 "빠른 실패" 원칙을 따르는 것을 볼 수 있습니다. 즉, 오류를 빠르게 찾을 수 있도록 가능한 한 빨리 "실패"합니다. 물론 이것은 특정 불편을 초래합니다. 그러나 이 접근 방식은 보다 강력한 코드를 만드는 데 도움이 됩니다.

  4. 코드의 다른 부분을 호출할 때 특정 예외를 포착하는 것이 가장 좋습니다. 호출된 코드가 여러 예외를 throw하는 경우 이러한 예외의 상위 클래스만 포착하는 것은 좋지 않은 프로그래밍 방식입니다. 예를 들어 FileNotFoundExceptionIOException 을 발생시키는 코드를 호출한다고 가정합니다 . 이 모듈을 호출하는 코드에서 Exception 을 catch하는 단일 catch 대신 각 예외를 catch하는 두 개의 catch 블록을 작성하는 것이 좋습니다 .

  5. 사용자와 디버깅을 위해 예외를 효과적으로 처리할 수 있는 경우에만 예외를 포착하십시오.

  6. 자신의 예외를 작성하는 것을 주저하지 마십시오. 물론 Java에는 모든 경우에 맞는 기성품이 많이 있지만 때로는 여전히 자신의 "바퀴"를 발명해야 합니다. 그러나 이 작업을 수행하는 이유를 명확하게 이해하고 표준 예외 집합에 이미 필요한 항목이 없는지 확인해야 합니다.

  7. 고유한 예외 클래스를 만들 때 이름 지정에 주의하십시오! 클래스, 변수, 메서드 및 패키지의 이름을 올바르게 지정하는 것이 매우 중요하다는 것을 이미 알고 있을 것입니다. 예외는 예외가 아닙니다! :) 항상 Exception 이라는 단어로 끝나며 , 예외 이름은 그것이 나타내는 오류 유형을 명확하게 전달해야 합니다. 예를 들어 FileNotFoundException 입니다 .

  8. 예외를 문서화하십시오. 예외에 대해 @throws Javadoc 태그를 작성하는 것이 좋습니다. 이는 코드가 모든 종류의 인터페이스를 제공할 때 특히 유용합니다. 또한 나중에 자신의 코드를 이해하는 것이 더 쉽다는 것을 알게 될 것입니다. MalformedURLException 이 무엇인지 어떻게 알 수 있습니까 ? 자바독에서! 예, 문서 작성에 대한 생각은 그다지 매력적이지 않지만 6개월 후에 자신의 코드로 돌아가면 자신에게 감사하게 될 것입니다.

  9. 리소스를 해제하고 try-with-resources 구성을 무시하지 마십시오 .

  10. 다음은 전체 요약입니다. 예외를 현명하게 사용하십시오. 예외 발생은 리소스 측면에서 상당히 "비싼" 작업입니다. 대부분의 경우 예외 발생을 피하고 대신 간단하고 "저렴한" if -else 를 사용하여 작업의 성공 여부를 나타내는 부울 변수를 반환하는 것이 더 쉬울 수 있습니다 .

    또한 응용 프로그램 논리를 예외에 연결하고 싶은 유혹이 있을 수 있지만 분명히 그렇게 해서는 안 됩니다. 글의 시작 부분에서 말했듯이 예외는 예상되는 상황이 아닌 예외적인 상황에 대한 것이며 이를 방지하기 위한 다양한 도구가 있습니다. 특히 nullPointerException 을 방지하기 위한 Optional 또는 read() 메서드가 던질 수 있는 IOException 을 방지하기 위한 Scanner.hasNext 등이 있습니다 .