예외 처리 (Java)

2024. 4. 5. 13:12

 

 

예외 처리란 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이다.

예외 처리를 한다고 해서 프로그램의 예외 상황 자체를 막을 수는 없다.

예외의 발생으로 인한 실행 중인 프로그램의 갑작스런 비정상적인 동작을 막고, 에러를 잡아 복구를 시도하거나 아니면 회피하는식으로 처리를 해서, 프로그램이 정상적인 실행 상태를 유지할 수 있도록 하는 것이다.

 

  • try - catch
    • try 블록에는 예외 발생 가능 코드가 위치하고 만일 코드에 오류가 발생되면, 오류 종류(예외 클래스)에 맞는 catch 문으로 가서 catch 블록 안에 있는 코드를 실행시킨다. 오류가 발생하지 않으면 catch 문은 실행하지 않는다.
    • catch 문에는 예외클래스 타입과 변수 e가 선언되어 있는데, 만일 try문에서 예외가 발생하면 그 예외에 맞는 예외클래스가 catch문에 argument로 선언되어 있으면 실행되어 옳지 못한 동작에 대해서 대비를 할 수 있다.
    • 예외 클래스 개수만 해도 수백개인데 이것을 일일히 코드에 다 작성하는 것은 무리이다.
    • 따라서 클래스의 상속 관계(다형성)을 이용하여 예외클래스의 상위 클래스인 Exception 클래스 타입을 catch문 argument에 선언하면, 코드 몇 줄 만으로 자바의 나머지 모든 예외클래스를 catch 문으로 받아들일 수 있다.
    • 다만 세세하게 어떤 예외인지는 Exception 클래스만으로는 알 수 없다.
    • printStackTrace() 메서드를 catch 문 안에서 실행하면 부모 예외 클래스로 한꺼번에 예외 상황을 받아들여도 어떠한 예외 상황인지 세세하게 출력하여 추적할 수 있다.
  • try - catch - finally
    • 어떤 예외가 발생하더라도 반드시 실행되어야 하는 부분이 있어야 한다면 finally 문으로 처리가 가능하다.
    • 만일 어떠한 메서드를 반드시 실행시켜야 하는데, 만일 중간에 에러가 나버리면 catch 문으로 점프해버려 결국 뒤의 코드는 실행되지 못하게 된다.
    • 심지어 메서드의 return 문이 있어도 일단 finally의 코드를 실행하고 리턴한다.
  • 예외 메세지 출력
    • printStackTrace() : 예외 발생 당시의 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력
    • getMessage() : 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
    • 오류와 예외 모두 자바의 최상위 클래스인 Object를 상속받는다. 그리고 그 사이에 Throwable 클래스와 상속관계가 있다. Throwable 클래스의 역할은 오류나 예외에 대한 메시지를 담는 것이다. 대표적으로 위 두 메서드가 있다. 당연히 Throwable을 상속받은 Error와 Exception 클래스에서도 위 두 메서드를 사용할 수 있다.
    • 단, printStackTrace()는 프로그램의 내부 요소를 자세하게 추적하여 오류 메세지를 내보이기에 이를 외부에 노출시키면 보안적인 문제가 될 수 있으니 관계자만 확인할 수 있도록 만들어 주는 것이 좋다.
  • 예외 발생시키기(throw)
    • 만일 프로그램적으로 에러가 아니라도 로직상 개발자가 일부러 에러를 내서 로그에 기록하고 싶은 상황이 올 수 있다. 자바에서는 throw 키워드를 사용하여 강제로 예외를 발생시킬 수 있다.
    • 원래는 프로그램이 알아서 에러를 탐지하고 처리하였지만, 이번에는 사용자가 일부러 에러를 throw하여 에러를 catch한다는 개념이다.
    • 이때 new 생성자로 예외 클래스를 초기화하여 던지는데, 이 클래스 생성자에 입력값을 주게되면, catch 문의 getMessage() 메서드에서 출력할 메세지를 지정하게 된다.
  • 예외 떠넘기기(throws)
    • 예외가 발생할 수 있는 코드를 작성할 때 try - catch 블록으로 처리하는 것이 기본이지만, 경우에 따라서는 다른 곳에서 예외를 처리하도록 호출한 곳으로 예외를 떠넘길 수도 있다.
    • throws는 메서드 선언부 끝에 작성되어 메서드에서 예외를 직접 처리(catch)하지 않은 예외를 호출한 곳으로 떠넘기는 역할을 한다.
    • 예외를 발생시키는 키워드는 throw이고, 예외를 메서드에 선언하는 키워드는 throws이다.
    • throws 예외 클래스명을 기재하면, 해당 메서드 안에서 예외가 발생할 경우 try - catch 문이 없으면 해당 메서드를 호출한 상위 스택 메서드로 가서 예외 처리를 하게 된다.
    • 즉, 예외 클래스를 메서드의 throws에 명시하는 것은 이곳에서 예외를 처리하는 것이 아니라 자신을 호출한 메서드에게 예외를 전달하여 예외 처리를 떠맡기는 것이다. 이런 식으로 계속 전달 되다가, 제일 마지막에 있는 main 메서드에서 throws를 사용하면 가상머신에서 처리된다.
  • 예외 발생과 코드의 트랜잭션
    • 트랜잭션은 하나의 작업 단위를 뜻한다.
    • 즉, 자바 코드에서 메서드 블럭 내의 코드들이 예외가 발생해도 모두 실행되느냐 아니면 예외가 발생하면 그 상태로 중지하느냐의 작업 단위를 개발자가 어떤 형태의 예외 처리 방법을 사용하느냐에 따라 달라지게 된다.
    • 예를 들어, 각 메서드에서 일일히 try - catch 하면, 메인 메서드에 있는 메서드 실행 코드 부분은 3개 모두 실행 자체는 된다. 왜냐하면 예외 처리를 각 메서드에서 하기 때문에 상위의 메인 메서드의 코드들은 모두 실행되게 된다.
    • 반면에 throws를 통해 예외 처리를 상위 메서드에서 모아 처리를 한다면, 코드 어느 한곳에서 예외가 발생하면 그 뒤의 나머지 코드들은 당연히 실행되지 않게 된다.
    • 이처럼 try - catch 문은 어디에 사용하냐 어디서 throws 하느냐에 따라 자바 코드의 작업 단위(트랜잭션)가 완전히 달라질 수 있게 되는 것이다. 따라서 자신의 프로젝트에 따라 적절한 예외 처리 로직을 짜주어야 하는 방향으로 나아가야 한다.
  • 연결된 예외 (Chained Exception)
    • 예외를 다른 예외로 감싸 던지기
    • 연결된 예외는 한 예외가 다른 예외를 발생시킬 수 있는 기능이다.
    • 우리가 클래스를 상속하여 다형성을 이용하여 부모 클래스 타입으로 다뤄온 것 처럼, 예외도 마치 부모 예외로 감싸서 보내 마치 예외의 다형성 처럼 다룰 수 있다.
    • 예를 들어 예외 A가 발생했다면 이를 예외 B로 감싸서 throw하는 식으로, 마치 예외를 다른 예외로 감싸서 던진다고 보면 된다. 그래서 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.
    • Exception 클래스가 상속하고 있는 Throwable 클래스에는 chained exception을 가능하게 해주는 다음 메서드를 지원한다.
      • Throwable initCause(Throwable cause) : 지정한 예외를 원인 예외로 등록
      • Throwable getCause() : 원인 예외를 반환
    • 이 메서드를 이용해서 예외를 다른 예외로 포장해 던질 수 있게 된다.
    • 발생한 예외를 그대로 처리하면 될 것을, 굳이 원인 예외로 포장해서 다른 예외로 던지는 이유는, 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서다. 위에서 언급했던 것처럼 다형성 예외 버전이라 봐도 된다.
    • 그리고 처음부터 명확한 에러 정보를 주는 것 보다는 단계별로 어떠한 원인의 에러에 의해서 에러가 났다는 정보를 주는 것이 더 좋기 때문이다. 
    •  

  • Checked 예외를 Unchecked 예외로 변환
    • 연결된 예외(chained exception)를 사용하는 또 다른 이유는 checked 예외를 unchecked 예외로 바꿀 수 있도록 하기 위함이다. 예를 들어 checked exception의 종류의 예외를 포함한 코드를 작성하면 컴파일러가 예외 처리 (try - catch)를 강제한다. 
    • 가장 대표적인 예로 FileWriter 클래스를 이용해 파일을 불러오는 코드를 작성하면 반드시 try - catch로 감싸주어야 컴파일이 된다.
    • 이런 식으로 설계한 이유는, 처음 자바 언어를 개발했을 때 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위해서인데, 실제로 별 것 아닌 예외도 checked exception으로 등록한 것이 많다.
    • 런타임 예외로 처리해도 될 것들이 아직도 checked exception으로 등록되어 강제적으로 try - catch 문을 사용해야 하는 불편함이 있고, 또한 로직상 Runtime Exception으로 할 수 밖에 없는 경우가 있기 때문에 추가된 기법이다.

 

 

  • Exception Handling
    • 예외 복구
      • 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법
      • Exception이 발생하여도 어플리케이션은 정상 동작
      • 반복문을 이용하여 예외가 발생하더라도 일정 수만큼 재시도를 하여 예외 복구를 시도
      • 만일 최대 재시도 횟수를 넘기게 되는 경우 예외를 발생시키거나 다른 플랜으로 문제 해결 시도
      • 네트워크에 연결하거나 하는 등의 로직에서 유용
    • 예외 처리 회피
      • 처리하지 않고 호출한 쪽으로 throw
      • 추천되지는 않는다.
      • 호출한 쪽에서 예외를 처리하는 것이 바람직할 때 사용
      • 해당 로직에서 예외가 발생했을 때 처리하지 않고 회피하는 것이 최선의 방법일 때만 사용
    • 예외 전환
      • 명확한 의미의 예외로 전환 후 throw
      • 메서드 밖으로 예외를 던지지만, 무작정 던지지 않고 적절한 예외로 필터링해서 넘기는 방법
      • 조금 더 명확한 의미로 전달되기 위해 적합한 의미를 가진 예외로 변경해서 throws 하는 것
      • 이외에도 예외 처리를 상위 클래스로 단순하게 퉁치기 위해 포장(wrap)하는 방법도 일컫는다.
    • 주의 사항
      1. catch 에는 로깅, 복구 등의 로직을 추가하기
        • 예외를 아무 로직 없이 catch만 하는 것은 바람직 하지 않다.
        • 또한 catch에 단순히 throw만 하는 것도 바람직 하지 않다.
        • 로그를 출력하거나, 문제를 원상 복구 시키는 로직을 첨가하는 등 catch만 수행하지 않고 해당 예외에 대한 처리를 해주어야 한다.
      2. 예외 Stack을 남겨 추적, 유지보수성 높이기
        • Exception의 추적성과 유지보수 성을 높이기 위해서, e.toSring()이나 e.getMessage()로 마지막 예외 메시지만 남기기보다, 전체 Exception Stack을 다 넘기는 편이 좋다.
        • 대표적인 slf4j 라이브러리의 log.error() 역시 e.printStackTrace()처럼 Exception의 stack도 남긴다.
      3. Logging Framework 사용하기
        • e.printStackTrace() 대신 LoggingFramework(slf4j, commons logging, log4j, logback)를 활용하자
        • 로그 파일을 쪼갤 수 있고, 여러 서버의 log를 한 곳에서 모아서 보는 System을 활용할 수도 있다.

 

 

Try - With - Resource

  • 보통 resource란 외부의 데이터(DB, Network, File)를 일컫는다.
  • 이런 resource들은 자바 내부에 위치한 요소들이 아니기 때문에, 이러한 프로세스 외부에 있는 데이터에 자바 코드에서 접근하려고 할 때 문제(예외)가 발생할 수 있는 여지가 존재한다.
  • 특히나 입출력에 관련된 resource들에 접근해서 사용하고 나면 닫는 것이 굉장히 중요하다. 파일에 접근해 파일을 열고 내용을 쓰고 난 후 꼭 닫아주어야 한다. 왜냐하면 어떤 resource를 사용하다가 다른 곳에서 같은 resource에 접근해 막 사용하다 보면 꼬일 수 있기 때문이다.
  •  

  •  

  • 코드 안정성은 그렇다 쳐도 겨우 파일을 열고 닫는데는 코드 3줄이면 될 것을 괜히 예외 처리한다고 덕지덕지 붙여 굉장히 가독성이 안좋아졌다.
  • Try with Resource 문
    • 주로 입출력(I/O)과 관련된 클래스를 사용할 때 굉장히 유용하다. 입출력에 사용한 객체를 자동으로 반환시켜주기 때문이다.
    • 사용법은 아래와 같이 try 블록에 괄호()를 추가하여 파일을 열거나 자원을 할당하는 명령문을 명시하면, 해당 try 블록이 끝나자마자 자동으로 파일을 닫거나 할당된 자원을 해제해준다.
    • try (파일을 열거나 자원을 할당하는 명령문) { ... }
    • 위의 파일 I/O 코드를 리팩토링하면 
try (FileWriter file = new FileWriter("data.txt")) {
    file.write("hello world");
} catch (IOException e) {
    e.printStackTrace();
}

 

  • try - with - resource 문의 괄호 안에 객체를 생성하는 문장을 넣으면, 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그리고 다음에 catch 블록 또는 finally 블록이 수행된다.
  • 또한 괄호 안에 I/O 객체 문장을 두 개 이상 넣어줄 수 도 있다. (세미콜론으로 각 문장을 구분)
  • 어떠한 클래스가 try - with - resource 문으로 사용될 수 있기 위해서는 AutoCloseable 인터페이스를 구현 받아야 한다.
  • AutoCloseable 안에 close() 메서드가 존재한다.

 

 

  • 에러 (Error)
    • java.lang.Error 클래스의 하위 클래스들이다. Error는 메모리가 부족하는 등과 같이 시스템이 비정상적인 상황인 경우에 사용한다. 주로 JVM에서 발생시킥 때문에 애플리케이션 코드에서 잡아서는 안되며, 잡아서 대응할 수 있는 방법도 없다. 따라서 시스템 레벨에서 특별한 작업을 하는게 아니라면 이러한 에러 처리는 하지 않아도 된다.
  • 예외 (Exception)
    • 애플리케이션 코드에서 예외가 발생하였을 경우에 사용된다.
    • 체크 예외
      • RuntimeException 클래스를 상속받지 않은 예외 클래스들이다.
      • 복구 가능성이 있는 예외이므로 반드시 예외를 처리하는 코드를 함께 작성해야 한다.
      • 실제로 애플리케이션 개발에서 발생하는 예외들은 복구 불가능한 경우가 많다. 예를 들어 SQLException 같은 체크 예외를 catch해도, 쿼리를 수정해서 재배포하지 않는 이상 복구되지 않는다. 그래서 실제 개발에는 대부분 언체크 예외를 사용한다.
    • 언체크 예외
      • RuntimeException 클래스를 상속받는 예외 클래스들이다.
      • 복구 가능성이 없는 예외들이다. 컴파일러가 예외처리를 강제하지 않는다. (= 복구 할 수 없는 코드) = 자동으로 복구가 안되고, 코드를 수정해야 함.
      • Error와 마찬가지로 에러를 처리하지 않아도 컴파이 에러가 발생하지 않는다.
      • 즉, 런타임 예외는 예상치 못했던 상황에서 발생하는 것이 아니므로 굳이 예외 처리를 강제하지 않는다.
      • 스프링이 제공하는 선언적 트랜잭션(@Transaction)안에서 에러 발생 시 체크 예외는 롤백이 되지 않고, 언체크 예외는 롤백이 된다.
    • 예외 전환
      • 예외 회피와 마찬가지로 예외를 복구할 수 없는 상황에 사용되며, 예외 처리 회피와 다르게 적절한 예외로 변환하여 던진다. 크게 두 가지 목적이 있다.
        • 의미 있고 추상화된 예외로 바꾸는 경우
        • 런타임 예외로 포장하여 불필요한 처리를 줄여주는 경우
      • 내부에서 발생한 예외를 그대로 던지는 것이 적절한 의미를 부여하지 못한다면 의미 있고 추상화된 예외로 바꾸는 것이 좋다. 예를 들어 사용자 등록 시 동일한 아이디 존재하면 SQLException이 나오는데, 이대로 던지면 서비스 계층에서 명확한 의미 파악이 힘들다. 그래서 DuplicateUserIdException과 같은 예외로 바꿔서 던지면 의미 전달 + 상황에 따라 복구 작업 시도가 가능하다.
      • 또한 체크 예외에 의해 불필요하게 해주는 에러 처리가 많아지면 해결을 위해 런타임 예외로 포장한다. 자세한 로그를 남기거나 알림을 주는 등의 방식으로 처리할 수 있다.
    • 올바른 예외 처리 방법
      1. 조치가 없는 try - catch -> 상황에 맞는 조치 진행
        • try - catch로 예외를 잡고 아무 조치도 하지 않는 것은 상당히 조심해야 한다. 예외가 발생하여도 무관하며, 다음 라인을 실행하겠다는 의도가 있는 것이 아니라면 반드시 피해야한다.
        • 이러한 코드는 오류가 있어서 예외가 발생했는데 그것을 무시하고 진행하는 꼴이므로 어떤 기능이 비정상적으로 동작하거나, 메모리나 리소스가 고갈되는 등의 문제를 야기할 수 있다. (리소스 사용 도중 에러나면 자원이 해제되지 않을 수 있음) 그러므로 예외를 처리할 때는 빈 값을 반환하는 등의 조치를 통해 상황을 적절하게 복구하거나 작업을 중단시키고 관리자에게 이를 전달해야 한다.
      2. 무분별한 throws Exception -> 언체크 예외로 전환
        • 무책임하게 throws하는 것도 안좋다. 왜냐하면 해당 문구를 보고 여기서 어떠한 문제가 발생할 수 있는지와 같은 의미있는 정보를 얻을 수 없기 때문이다. 또한 이 메서드를 다른 메서드에서 사용중이라면 throws Exception이 전파되므로 좋지 못하다.
        • 만약 SQLException과 같이 복구가 불가능한 예외들이라면 기계적으로 throws를 던지지 않고 가능한 빨리 언체크/런타임 예외로 전환해주는 것이 좋다.
    • Spring의 예외 처리
      • @Repository 빈들에는 대표적으로 예외 전환 기법이 사용되고 있다.
        • 추상화된 예외로 전달한다.
        • @Repository에는 각기 다른 데이터베이스에 의한 에러를 Spring의 데이터베이스 에러인 DataAccessException으로 전환해주는 기능이 있다. 
        • 이를 통해 개발자는 데이터베이스에 종속되지 않는 개발을 할 수 있다.
      • 언체크 예외로 전환
        • 데베에 의한 에러는 일반적으로 체크 예외이다.
        • 데베에 의해 체크 예외를 throws 받아도 우리가 할 작업은 따로 없고 에러 코드를 내려주는 것 뿐이다.
        • 대표적으로 중복된 ID로 가입을 하는 경우, Constraint 에러가 발생하였다면 우리가 해야할 것은 올바른 에러 코드를 내려주는 것 뿐, 체크 예외로 처리할 필요가 없다. 
        • Spring은 이러한 문제를 해결하기 위해 예외를 언체크 예외로 정의하고 있으며, 이를 통해 우리는 무분별한 throw를 하지 않아도 되도록 도와준다.

 

 

 

============강의 정리==========

QA. catch로 예외를 잡을 때 왜 Throwable 예외를 잡으면 안되는지 ?

 

 

 

 

 

QA

  1. throws로 예외를 상위 메서드로 떠넘기고 최종 main에서도 떠넘겼을 때 발생하는 일?
    • JVM은 처리되지 않은 예외를 받으면 스택 트레이스를 출력하고 프로그램을 종료한다.
    • 웹어플리케이션은 여러 사용자들이 이용하므로 오류 페이지를 보여주고 종료되지는 않는다.
  2. 스프링 프레임워크에서 제공하는 선언적 트랜잭션(@Transaction)안에서 에러 발생 시 체크 예외와 언체크 예외의 각각 롤백 여부
  3. Exception 자체를 밖으로 던지는게 왜 안좋은 방법인지?
  4. Throwable이 serialize를 구현하는 이유??

 

 

 

 

'Java' 카테고리의 다른 글

직렬화 역직렬화  (0) 2024.04.10
Garbage Collection  (1) 2024.04.10
OOP(Object Oriented Programming)  (0) 2024.04.05
쓰레드(Thread)  (0) 2024.04.05
상속  (0) 2024.03.13

BELATED ARTICLES

more