Java 예외 처리 정리(Exception)

LeeYulhee·2023년 11월 2일
0

👉 Exception의 정의


  • 자바에서 Exception은 프로그램 실행 중에 발생할 수 있는 예외적인 상황을 나타내는 클래스
  • 모든 예외 클래스는 java.lang.Exception 클래스의 하위 클래스
  • 예외는 크게 두 가지 카테고리로 분류
    • Checked Exception
      • 컴파일 시점에 체크되는 예외
      • 이러한 예외들은 예외 처리를 강제함
        • 개발자가 해당 예외에 대한 처리 코드를 작성하지 않으면 컴파일 자체가 되지 않음
      • 예를 들어, IOException, SQLException 등이 있음
    • Unchecked Exception
      • 실행 시점(Runtime)에 발생하는 예외로, 이들은 RuntimeException의 하위 클래스
      • 컴파일러가 이 예외를 체크하지 않음
        • 개발자가 명시적으로 처리하지 않아도 컴파일에는 영향을 주지 않음
      • 예를 들어, NullPointerException, ArrayIndexOutOfBoundsException 등이 있음
  • 예외 처리를 함으로써, 프로그램이 더 견고해지고 예측 가능한 행동을 하며, 유저에게 더 친절한 피드백을 줄 수 있음



👉 Throwable 클래스


  • Throwable 클래스는 Java에서 오류(Error)와 예외(Exception) 클래스의 상위 클래스
  • 모든 오류와 예외는 Throwable의 인스턴스
  • 이 클래스에는 예외와 오류가 발생했을 때 유용한 정보를 제공하는 여러 메소드가 포함되어 있음
    • printStackTrace() 등



👉 예외 처리 방법


  • 📌 try-catch
    try {
        // 예외가 발생할 수 있는 코드
    } catch (ExceptionType e) {
        // 예외를 처리하는 코드
        // 오류 메시지를 로깅하거나 사용자에게 알림 등
    }
    // catch 블록 이후의 코드는 예외가 발생하더라도 계속 실행
    • 예외가 발생할 수 있는 코드 블록을 try 블록 안에 넣고, 해당 예외를 처리할 catch 블록을 제공
    • try-catch 블록을 사용하면 정상적인 비즈니스 로직과 오류 처리 로직을 분리할 수 있음
    • 예외가 발생해도 프로그램이 비정상 종료되는 것을 방지하고 실행 상태를 유지할 수 있음

  • 📌 try-catch-finally
    try {
        // 예외가 발생할 수 있는 코드
    } catch (ExceptionType e) {
        // 예외를 처리하는 코드
    } finally {
        // 예외 발생 여부와 관계없이 실행되는 코드
    }
    • 선택적인 finally 블록은 예외가 발생하든 안 하든 반드시 실행되는 코드를 포함
    • finally 블록은 항상 실행됨
      • try 블록 내에서 return문이 실행되거나 catch 블록 내에서 예외가 다시 발생해도 finally 블록은 실행
    • 주로 자원 해제와 같은 정리 코드에 사용

  • 📌 try-with-resources
    try (ResourceType resource = new ResourceType()) {
        // 자원을 사용하는 코드
    } catch (ExceptionType e) {
        // 예외를 처리하는 코드
    }
    • 자바 7 이상에서는 자동으로 자원을 해제해주는 try-with-resources 문법을 사용할 수 있음
    • 이 구문을 사용하면 finally 블록에서 명시적으로 자원을 해제하는 코드를 작성하지 않아도 됨
      • 자원을 사용하는 클래스가 AutoCloseable 인터페이스를 구현해야 함



👉 throw와 throws의 차이


  • 📌 throw
    • 단일 예외를 던짐
    • 프로그래머가 프로그램에서 예외를 발생시키기 위해 사용
    • 즉, 예외를 강제로 발생시킬 때 사용하는 키워드
    • 현재 실행 흐름을 중단하고, 호출 스택을 따라가며 해당 예외를 처리할 수 있는 catch 블록을 찾음
    • 만약 처리할 catch 블록을 찾지 못하면, 예외는 JVM에 의해 처리되고 프로그램은 종료
    • 예시
      throw new Exception("이것은 예외를 발생시킵니다.");

  • 📌 throws
    • 메서드가 여러 예외를 던질 수 있다고 선언할 때 사용
    • 메서드 또는 생성자의 선언에 사용
      • 해당 메서드/생성자를 호출할 때 해당 메서드/생성자 내에서 발생할 수 있는 예외를 호출자에게 전달(던지는)한다는 것을 나타냄
      • 해당 메서드/생성자 내에서 예외를 처리하지 않고, 대신에 이를 호출하는 메서드로 예외 처리의 책임을 넘기겠다는 의미
    • 해당 메소드에서 처리하지 않고 발생할 수 있는 예외를 명시할 때 사용
    • 이를 통해 예외가 메소드를 호출한 상위 메소드로 전파될 수 있음
    • 예시
      public void readFile(String fileName) throws IOException {
          // 여기서 IOException이 발생할 수 있는 코드 (예: 파일을 열려고 시도하는 코드)
      }
      • 이 메서드를 호출하는 코드는 이제 IOException을 처리하기 위한 try-catch 블록을 포함해야 하거나, 또 다시 이 예외를 전파하기 위해 자신의 메서드 선언에서 throws IOException을 사용해야 함



👉 궁금해진 부분 : throws의 종착지에는 try-catch가 있는지?


  • throws로 선언된 예외는 호출 체인을 따라 계속해서 전파되고, 결국 어딘가에서는 try-catch 블록을 사용하여 해당 예외를 catch하고 처리해야 함
    • 이는 예외가 발생했을 때 가능한 모든 선택지를 고려하고, 예외를 처리하는 것이 논리적으로 가장 의미가 있는 곳에서 처리하도록 하기 위함
    • 예시
      public void handleUserInput() {
          try {
              // 입력을 읽는 코드, 여기서 IOException이 발생할 수 있음
          } catch (IOException e) {
              // 로그 남기기
              // 사용자에게 오류 메시지 표시
              // 필요하다면 사용자로부터 다시 입력 받기
          }
      }
  • 예외를 처리하는 최종 종착지는 다음 중 하나가 될 수 있음
    • 개발자가 직접 작성한 코드 내의 try-catch 블록
      • 예외를 처리할 수 있는 적절한 위치에서 개발자는 try-catch 블록을 사용하여 예외를 잡아내고, 예외에 대한 적절한 처리를 수행할 수 있음
    • 자바 런타임 시스템
      • 만약 모든 호출 체인 상에서 예외가 잡혀서 처리되지 않으면, 최종적으로 자바 런타임 시스템이 해당 예외를 잡음
      • 이 경우 자바는 스택 트레이스(stack trace)를 출력하고 프로그램을 종료
        • 스택 트레이스
          java.lang.NullPointerException: Attempt to invoke virtual method on a null object reference
              at com.example.MyClass.myMethod(MyClass.java:10)
              at com.example.Main.main(Main.java:5)
          • 프로그램의 실행 중에 특정 시점에서 메서드 호출이 쌓여 있는 스택의 상태를 보여주는 정보
          • 예외가 발생했을 때, 스택 트레이스는 예외가 발생한 지점과 예외를 던진 메서드로부터 호출 스택을 따라 거슬러 올라가는 메서드의 목록을 제공



👉 작성했던 코드에 try-catch 적용 예시


  • 검증에 문제가 있으면 오류를 발생시키는 코드
    public class InputValidator {
    
        // 중복 로직 메서드로 분리
        public static void validateInput(Map<String, String> input, Map<String, String> data, String... keys) {
            for (String key : keys) {
                if (CheckUtil.isEmptyOrNull(input.get(key))) {
                    throw new CustomException(getCustomExceptionMessageForKey(key));
                }
            }
        }
    
        // 메시지 매핑
        private static String getCustomExceptiontMessageForKey(String key) {
            Map<String, String> customExceptionMessages = new HashMap<>();
            customExceptionMessages.put("USER_ID", "회원 아이디가 입력되지 않았습니다. 입력 항목은 USER_ID 입니다.");
    				customExceptionMessages.put("USER_PW", "회원 비밀번호가 입력되지 않았습니다. 입력 항목은 USER_PW 입니다.");
    				customExceptionMessages.put("USER_NM", "회원 이름이 입력되지 않았습니다. 입력 항목은 USER_NM 입니다.");        
    				// 그 외 오류 메세지 추가
    
            return customExceptionMessages.getOrDefault(key, "필요한 값이 입력되지 않았습니다.");
        }
        
        public static void validatePasswordEquality(Map<String, String> input) {
            if (!input.get("USER_PW2").equals(input.get("USER_PW"))) {
                throw new CustomException("비밀번호와 비밀번호 확인이 일치하지 않습니다.");
            }
        }
    
        public static void validateEncryptedPasswordEquality(Map<String, String> input, Map<String, String> data) {
            String checkPassword = Encrypt.sha256Encode(input.get("USER_PWCHK"));
            if (!checkPassword.equals(data.get("USER_PW"))) {
                throw new CustomException("비밀번호가 다릅니다. 올바른 비밀번호를 입력해주세요. 입력 항목은 USER_PWCHK 입니다.");
            }
        }
    }
  • 기존 코드
    public class Service {
        public void createUser(Map<String, String> input) {
            InputValidator.validateInput(input, "USER_ID");
            InputValidator.validatePasswordEquality(input);
    
    		//그 외 로직
        }
    }
    • CustomException 오류 발생 시, 프로그램이 비정상적으로 종료됨
      • try-catch로 오류를 처리하는 곳이 없음
  • 오류 처리 코드
    public class Service {
        public void createUser(Map<String, String> input) {
    			try { 
    		      	InputValidator.validateInput(input, "USER_ID");
    		      	InputValidator.validatePasswordEquality(input);
    			} catch (CustomException e) {
    				System.out.println(e.getMessage());  // 예외 메시지를 출력
                	// 필요한 추가적인 오류 처리
    			}
    
    		// 'try-catch' 블록 이후의 코드는 예외 발생 여부와 상관없이 실행
            System.out.println("프로그램은 계속 실행됩니다.");
        }
    }
    • 이 코드에서 InputValidator.validateInput 메서드가 CustomException을 던지면, main 메서드 내의 catch 블록이 그 예외를 잡아 처리
      • System.out.println(e.getMessage()) 코드는 예외에 대한 정보를 출력하고, catch 블록 이후의 코드는 예외 처리가 끝난 후 계속해서 실행
      • ⇒ 프로그램은 비정상 종료되지 않고 남아 있는 작업을 계속할 수 있음
    • InputValidator 클래스에서 try-catch를 적용하지 않음
      • 📌 catch하는 코드의 위치를 정할 때 고려할 부분
        • 책임의 원칙
          • 메서드나 클래스는 자신의 책임 범위 내에서 발생하는 예외를 처리해야 함
        • 재사용성
          • 만약 클래스(InputValidator)가 여러 컨텍스트에서 사용될 수 있고, 예외를 다루는 방식이 호출하는 측마다 다를 수 있다면, 예외를 throw하고 이를 호출한 측에서 처리하게 하는 것이 더 유연할 수 있음
        • 오류 메시지와 복구
          • 사용자에게 오류 메시지를 제공하거나 특정 오류에 대한 복구 작업을 수행해야 한다면, 이러한 처리는 대체로 예외를 발생시킨 직접적인 코드 영역 바깥에서 수행되는 것이 좋음
        • 프로그램 흐름 제어
          • 예외는 비정상적 상황을 나타내기 위한 수단
          • 때로는 예외를 사용하여 프로그램의 흐름을 제어하는 것이 아니라, 유효성 검사를 통해 정상적인 흐름을 조정하는 것이 더 적합할 수 있음



👉 궁금해진 부분 : try-catch의 catch 부분에서 Exception을 발생시키는 경우


public static String sha256Encode( String password ) {
		
		try {
			MessageDigest md = MessageDigest.getInstance( "SHA-256" );
			byte[] digest = md.digest( password.getBytes( StandardCharsets.UTF_8 ) );
			return bytesToHex( digest );
		} catch ( NoSuchAlgorithmException e ) {
			throw new RuntimeException( e );
		}
	}
  • 발생하는 일
    • catch 블록 내에서 생성된 새로운 예외 (RuntimeException)는 try-catch 블록을 포함하고 있는 메소드의 실행 흐름을 즉시 중단시키고, 해당 메소드를 호출한 상위 메소드로 예외를 전파
    • 상위 메소드에서도 이 예외를 처리하는 try-catch 블록이 없다면, 예외는 계속 호출 스택을 따라 전파
    • 어딘가에서 이 예외를 처리하는 catch 블록을 만나지 않는다면, 예외는 최종적으로 JVM(자바 가상 머신)에 도달하고, JVM은 예외의 스택 트레이스를 출력한 뒤 프로그램을 종료 시킴
  • 정리
    • NoSuchAlgorithmException이 발생하면 catch 블록이 실행되고, 새로운 RuntimeException이 던져 짐
    • 이 예외는 해당 메소드(sha256Encode)를 호출한 상위 메소드로 전파되고, 처리되지 않는 한 계속해서 전파됨
  • 📌 이런 패턴을 사용하는 이유
    • 체크 예외(Checked Exception)를 언체크 예외(Unchecked Exception)로 변환하기 위해
      • NoSuchAlgorithmException은 체크 예외이기 때문에 이를 명시적으로 처리하거나 선언해야 하는 반면, RuntimeException은 언체크 예외이며 명시적인 처리나 선언을 필요로 하지 않음
        • NoSuchAlgorithmException의 상속 계층 구조
          java.lang.Object
          	java.lang.Throwable
          		java.lang.Exception
          			java.lang.GeneralSecurityException
          				java.security.NoSuchAlgorithmException
    • 예외를 더 추상적인 수준으로 변환하여 호출자에게 특정 구현 세부사항을 숨기고, 호출자가 처리해야 할 예외의 범위를 단순화하기 위해
profile
끝없이 성장하고자 하는 백엔드 개발자입니다.

0개의 댓글

관련 채용 정보