자바의 오류처리: 예외(Exception) 와 트랜잭션 처리

자바에서 오류 처리는 예외(Exception)를 통해 관리됩니다. 프로그램 실행 중 발생할 수 있는 다양한 오류를 예외로 정의하고, 이를 처리하거나 방지하는 구조를 설계하는 것이 중요합니다.

기본적인 예외 처리 구조

자바에서 예외 처리는 try-catch 블록을 사용합니다. try 블록은 예외가 발생할 수 있는 코드를 포함하고, catch 블록은 예외가 발생했을 때 실행되는 코드를 작성합니다.

예외가 발생할 수 있는 코드, 예외를 발생했을 때 실행되는 코드라는 말을 이해하셔야 합니다.
try {
    <수행할 문장 1>;
    <수행할 문장 2>;
    ...
} catch(예외1) {
    <수행할 문장 A>;
    ...
} catch(예외2) {
    <수행할 문장 a>;
    ...
}

 

아래 코드는 0으로 나누는 연산이 발생했을 때, ArithmeticException 예외를 잡아내고 적절한 대처를 합니다.

이와 같은 구조로 예외를 관리하면 프로그램이 예기치 않게 종료되는 것을 방지할 수 있습니다.

public class Sample {
    public static void main(String[] args) {
        int c;
        try {
            c = 4 / 0;
        } catch(ArithmeticException e) {
            c = -1;  // 예외가 발생하여 이 문장이 수행된다.
        }
    }
}

 

프로그램 수행 도중 예외가 발생하면 프로그램이 중지되거나 예외 처리에 의해 catch 구문이 실행됩니다. 하지만 어떤 예외가 발생하더라도 반드시 실행되어야 하는 부분이 있어야 한다면 어떻게 해야 할까요?

그때는 finally 블록을 사용하시면 됩니다.

finally 

예외 발생 여부와 상관없이 항상 실행되어야 하는 코드가 있다면 finally 블록을 사용합니다.

이는 파일 닫기, 네트워크 연결 종료 등 리소스를 정리할 때 유용합니다.

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try {
            c = 4 / 0;
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            sample.shouldBeRun();  // 예외에 상관없이 무조건 수행된다.
        }
    }
}

사용자 정의 예외

자바에서는 필요에 따라 사용자 정의 예외를 만들 수 있습니다. 예를 들어, 특정 조건에서만 발생하는 예외를 만들고 이를 활용할 수 있습니다.

public class Sample {

  public void sayNick(String name) {
    
    if("바보".equals(name)) {
      return;
    }
    System.out.println("당신의 별명은" + name + "입니다.");
  }

  public static void main(String[] args) {
    Sample sample = new Sample();
    
    sample.sayNick("바보");
    sample.sayNick("형님");
  }
}

 

위 코드를 참고해서 "바보" 문자열이 입력되면 return 으로 메서드를 빠져나가지 않고 적극적으로 예외를 발생시켜 봅시다.

그러기 위해 다음과 같이 FoolException 클래스를 추가해야 합니다.

class FoolException extends RuntimeException {}
public class Sample {
  public void sayNick(String name) {
    if("바보".equals(name)) {
      throw new FoolException();
    }
    System.out.println("당신의 별명은" + name + "입니다.");
  }
  
  public static void main(String[] args) {
    Sample sample = new Sample();
    sample.sayNick("바보");
    sample.sayNick("형님");
  }
}

 

여기까지 설정을 한 뒤에 실행을 하면 아래와 같은 오류가 발생합니다.

Exception in thread "main" FoolException
    at Sample.sayNick(Sample.java:7)
    at Sample.main(Sample.java:14)

 

이러한 오류가 발생하는 이유는 FoolException 이 상속받은 클래스는 RuntimeException이라서 그렇습니다.

예외는 크게 두가지로 구분합니다.

RuntimeException: 실행 시 발생하는 예외
Exception: 컴파일 시 발생하는 예외

 

Exception 은 예측이 가능한 경우에 사용하고, RuntimeException 은 발생할 수도 있고 발생하지 않을 수도 있는 경우에 사용합니다. 그래서 Exception을 Checked Exception, RuntimeException을 Unchecked Exception 이라고도 합니다.

 

여기서 궁금한 부분은 "그럼 RuntimeException 은 언제 발생하는가" 입니다.

어렵게 생각할 필요 없이 배열의 인덱스를 잘못 참조하거나 0으로 나누기를 시도하는 경우 등 보통 프로그램 로직에 문제가 있을 때 발생하며, 명시적으로 처리하지 않아도 됩니다.

 

지금까지 해왔던 오류는 RuntimeException입니다. 따라서 Exception 오류를 만들어야 합니다.

 

Exception

Exception 은 우리가 예측할 수 있는 오류를 처리하고 싶을 때 사용합니다.

 

위에 코드에서 RuntimeException을 상속하던 것을 Exception 을 상속하도록 변경합니다.

그런데 이렇게만 하면 Sample 클래스에서 컴파일 오류가 발생할 것입니다.

FoolException 이 예측 가능한 Checked Exception으로 변경되어 예외 처리를 컴파일러가 강제하기 때문입니다.

따라서 다음과 같이 변경해야 정상적으로 컴파일될 것입니다.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) {
        try {
            if("바보".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

 

이와 같이 컴파일 오류를 방지하기 위해 사전에 try-catch 문으로 감싸주어야 합니다.

 

앞 예제를 보면 sayNick 메서드에서 FoolException을 발생시키고 예외 처리도 try-catch 문으로 sayNick 메서드에서 했습니다. 하지만 이렇게 하지 않고 sayNick 을 호출한 곳에서 FoolException 을 처리하도록 예외를 위로 던질 수 있는 방법이 많이 사용됩니다.

즉 sayNick 함수 안에서 오류를 처리하는 게 아니라 sayNick 함수를 사용하는 곳에서 오류를 처리하게 합니다.
public class Sample {
  public void sayNick(String name) throw FoolException {
    if("바보".equals(name)) {
      throw new FoolException();
    }
    System.out.println("당신의 별명은" + nick + "입니다.")
  }
  
  public static void main(String[] args) {
    Sample sample = new Sample();
    try {
      sample.sayNick("바보");
      sample.sayNick("형님");
    } catch (FoolException e) {
      System.err.println("FoolException이 발생했습니다.");
    }
  }
}

 

이와 같이 sayNick 메서드를 변경하면 main 메서드에서 컴파일 오류가 발생할 것입니다.

throws 구문 때문에 FoolException의 예외를 처리해야 하는 대상이 sayNick 메서드에서 main 메서드로 변경되었기 때문입니다.

 

이러한 방법이 더 많이 사용되는 이유는 sayNick 함수 자체에서 오류를 다루면 아래 코드가 전부 실행되는 문제점이 발생합니다.

sample.sayNick("바보");
sample.sayNick("형님");

 

하지만 main 메서드에서 예외 처리를 한 경우에는 두 번째 문장인 sample.sayNick("야호"); 가 수행되지 않습니다.

왜냐하면 이미 첫 번째 문장에서 예외가 발생하면서 catch 문으로 빠져버리기 때문입니다.

 

이러한 이유로 프로그래밍할 때 예외를 처리하는 위치는 대단히 중요합니다.

프로그램의 수행여부를 결정하기도 하고 다음에 나올 쇼핑몰 예제와도 밀접한 관계가 있기 때문입니다.

모두 취소하지 않으면 데이터 정합성이 크게 흔들리게 됩니다.
이렇게 모두 취소하는 행위를 롤백( RollBack )이라고 합니다.
여기서 데이터 정합성이란 간단히 말해 데이터들의 값이 서로 일관성 있게 일치하는 것을 말합니다.

 

쇼핑몰 예제

쇼핑몰의 '상품발송'이라는 트랜잭션을 가정했습니다. '상품발송' 이라는 트랜잭션에는 다음과 같은 작업들이 있습니다.

트랜잭션은 여러 작업을 하나의 단위로 묶어 관리하는 것을 말합니다.
만약 작업 중 하나라도 실패하면 전체 작업을 취소(Rollback) 하여 데이터의 정합성을 유지해야 합니다.

 

쇼핑몰의 운영자는 이 3가지 일 중 하나라도 실패하면 3가지 모두 취소하고 '상품발송' 전의 상태로 되돌리고 싶어 합니다.

이런 경우 어떻게 예외 처리를 하는 것이 좋을까요?

 

다음과 같이 포장, 영수증발행, 발송 메서드에서는 각각 예외를 던지고 상품발송 메서드에서 던져진 예외를 처리한 뒤 모두 취소하는 것이 완벽한 트랜잭션 처리 방법입니다.

 

다음은 트랜잭션을 설명하기 위해 상품 발송과 관련된 프로그램을 슈도코드로 작성하였습니다.

실제 코드는 아니지만 어떻게 동작하는지 이해하실 수 있습니다.

슈도코드(의사코드)란 특정 프로그래밍 언어의 문법을 따라 쓰인 것이 아니라, 일반적인 언어로 코드를 흉내 내어 알고리즘을 작성한 코드를 말한다. 흉내만 내는 코드이기 때문에 실제 코드처럼 컴퓨터에서 실행할 수 없으며, 특정 언어로 프로그램을 작성하기 전에 알고리즘을 대략적으로 모델링하는 데에 쓰인다.
상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    }catch(예외) {
        모두취소();  // 하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}

 

이와 같이 코드를 작성하면 포장, 영수증발행, 발송이라는 세 개의 단위 작업 중 하나라도 실패할 경우 예외가 발생되어 상품발송이 모두 취소될 것입니다.

 

부가적으로 아래와 같은 코드를 짜면 엄청 큰일 납니다.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    }catch(예외) {
       포장취소();
    }
}

영수증발행() {
    try {
       ...
    }catch(예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    }catch(예외) {
       발송취소();
    }
}

'Java' 카테고리의 다른 글

자바의 Generic  (1) 2024.12.19
자바의 toString 메서드  (0) 2024.12.17
String  (0) 2024.12.17
Java - Abstract  (0) 2024.12.11
업캐스팅 & 다운캐스팅  (1) 2024.12.11