[Java] 예외처리 -Exception Handling
1. 예외처리란?
프로그램을 작성하다보면 에러가 나는 상황이 아주 많다. 프로그램의 비정상적인 작동들을 방지하기 위해 예외처리를 하게 되는데, 나는 보통 코드처럼 예외처리를 하고, 주석을 달아 예외처리인 것을 알 수있게 하는방식으로 했었다. 자바는 이런 예외처리를 보다 수월하게 할 수 있게 하려고 try-catch, throw문 등을 제공한다. 이런 try-catch문 등을 활용하면 코드를 봤을 때 한 눈에 봐도 예외처리를 위한 것임을 알 수 있어서 편리할 것 같다.
대표적으로 예외가 발생하는 경우에는 없는 파일을 열려고 할 때(FileNotFoundException), 임의의 수를 0으로 나누려 할 때(ArithmeticException), 배열에 없는 요소에 접근할 때-[3]까지 있는 배열의 [4]요소에 접근하는 등- (ArrayIndexOutOfBoundsException) 정도가 있다.
2. try-catch문
다음은 try-catch문의 기본구조이다. try문 안에서 예외가 발생하지 않는다면 catch문은 실행되지 않는다. 하지만 try문 중 (예외1), (예외2)등에 해당하는 에러가 발생하면 해당 catch문이 실행되게 된다.
try {
...
} catch(예외1) {
...
} catch(예외2) {
...
...
}
예를 들면, 숫자를 0으로 나누었을때 발생하는 예외를 처리하려면 다음과 같이 코드를 작성할 수 있다.
int c;
try {
c = 4 / 0;
}catch(ArithmeticException e) { //try문에서 ArithmeticException이 발생하므로 실행!
c = -1;
}
//최종적으로 c변수에는 -1값이 저장
3. finally문
프로그램 수행 도중 예외가 발생하면 그 지점부터 바로 catch문으로 이동하게 된다. 예외가 발생하는 지점이후에 반드시 실행되어야 하는 문장을 작성하는 것은 불가능해 보인다. 하지만 이런 상황을 위해서 예외가 발생하더라도 finally문에 있는 문장들은 반드시 실행된다.
public class Test {
public void shouldBeRun() {
System.out.println("ok thanks.");
}
public static void main(String[] args) {
Test test = new Test();
int c;
try {
c = 4 / 0;
test.shouldBeRun(); //실행이 안됨
} catch (ArithmeticException e) {
c = -1;
}
}
}
위 코드에서는 c = 4 / 0; 이 라인에서 발생하는 ArithmetricException 때문에 test.shouldBeRun(); 코드가 건너뛰어지고 바로 catch문이 실행되어 버린다. 이제 finally구문을 적용해서 test.shouldBeRun(); 코드가 실행되게 해보자.
public class Test {
public void shouldBeRun() {
System.out.println("ok thanks.");
}
public static void main(String[] args) {
Test test = new Test();
int c;
try {
c = 4 / 0;
} catch (ArithmeticException e) {
c = -1; //이 라인 실행 후
} finally {
test.shouldBeRun(); //실행됨!
}
}
}
finally문은 try문 중 예외발생 여부에 상관없이 무조건 실행된다. 따라서 test.shouldBeRun();이 실행될 것이다.
4. RuntimeException과 Exception
에러에 런타임에러와 실행에러가 있는 것처럼, 예외 역시 예외적인 상황을 에러로 간주하는 것이기 때문에 런타임시 발생하는 예외(RunTimeException)와 컴파일시 발생하는 예외(Exception)가 있다. RunTimeException은 프로그램 작성 시 예측 가능한 예외를 작성할 때 사용하고, RuntimeException은 실행 시 상황에 때라 발생할 수도 있고 아닐수도 있는 경우 작성한다.
5. throw 문
다음 코드를 살펴보자.
public class Test {
public void sayNick(String nick) {
if("fool".equals(nick)) {
return;
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
public static void main(String[] args) {
Test test = new Test();
test.sayNick("fool");
test.sayNick("genious");
}
}
이 sayNick() 메소드는 조건문을 사용해 nick이 "fool"이 되면 강제적으로 메소드를 종료한다. 여기서 nick이 "fool"이 되는 상황을 예외로 만들어보자.
FoolException.java 파일을 생성해 , FoolException이 RunTimeException을 상속받도록 코드를 작성한다.
public class FoolException extends RuntimeException {
}
그리고 나서, 아까의 코드를 아래와 같이 변경해본다.
public class Test {
public void sayNick(String nick) {
if("fool".equals(nick)) {
throw new FoolException(); //throw!
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
public static void main(String[] args) {
Test test = new Test();
test.sayNick("fool");
test.sayNick("genious");
}
}
이 프로그램을 실행하면, 아래와 같은 에러가 나타난다.
Exception in thread "main" FoolException
at Test.sayNick(Test.java:7)
at Test.main(Test.java:15)
return으로 메소드를 빠져나오는 대신에 throw new FoolException()을 이용해 예외를 강제로 발생시켰다. 런타임 예외를 발생시켰으니, 컴파일러가 예외를 잡아낸 것이다.
그렇다면 이번엔 FoolException이 Exception을 상속받게 해보자.
public class FoolException extends Exception {
}
이렇게 하고 아까의 코드를 그대로 실행하면 Test 클래스에서 컴파일 오류가 발생할 것이다. 컴파일러 입장에서는 실행 전부터 잡아낼 수 있는 Checked Exception이기 때문에 예외처리를 컴파일러가 강제하기 때문이다. 다음과 같이 변경해야 정상적으로 컴파일이 될 것이다.
public class Test {
public void sayNick(String nick) {
try {
if("fool".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}catch(FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
}
public static void main(String[] args) {
Test test = new Test();
test.sayNick("fool");
test.sayNick("genious");
}
}
6. throws문 (예외 던지기)
위 코드에서는 sayNick안에서 FoolException을 발생시키고 예외처리도 그 안에서 했는데, sayNick에서 발생한 예외를 sayNick메서드를 호출한 곳에서 처리하는 방법이 있다. throws를 사용하는 것이다.
public void sayNick(String nick) throws FoolException {
if("fool".equals(nick)) {
throw new FoolException();
}
System.out.println("당신의 별명은 "+nick+" 입니다.");
}
이렇게 sayNick을 바꿔주면 컴파일 에러가 main메서드에서 발생하게 된다. throws구문 때문에 예외처리를 해야하는 부분이 sayNick에서 main으로 바뀌었기 때문이다. 컴파일이 되기 위해선 main이 아래와 같이 변경되어야 한다.
public static void main(String[] args) {
Test test = new Test();
try {
test.sayNick("fool");
test.sayNick("genious");
}catch(FoolException e) {
System.err.println("FoolException이 발생했습니다.");
}
}
예외에 대한 처리 (try-catch)도 예외가 발생한 위치와 관계없이 처리를 해주어야하는 자리인 main에서 이루어진 것이다.
그렇다면 예외처리는 어디서 해주는 것이 좋을까? 물론 상황에 따라 다르겠다. 하지만 이 예시의 경우, sayNick에서 예외처리를 하면 test.sayNick("genious")코드는 상관없이 실행되지만, main에서 예외처리를 하면 예외와는 상관이 없는 test.sayNick("genious")코드도 실행되지 못하게 된다.
이처럼 exception을 처리하는 위치는 아주 중요하다. 트랜잭션과도 밀접한 관계가 있다.
7. 트랜잭션과 예외처리
트랜잭션은 하나의 작업단위를 뜻한다.
예를 들어 '제빵'이라는 트랜잭션을 생각했을 때, 아래와 같은 하위 작업들이 있다.
1. 계량하기
2. 반죽하기
3. 오븐에 굽기
이 세 가지 일을 하나라도 실패하면 세 가지 일 모두 취소하고 "제빵"을 처음부터 다시 하는 것이 좋다. 이렇게 모두 취소하는 것을 Rollback이라고 한다.
프로그램이 다음과 같이 작성되어있다고 가정해본다.
제빵() {
계량하기();
반죽하기();
오븐에굽기();
}
계량하기() {
...
}
반죽하기() {
...
}
오븐에굽기() {
...
}
이런 경우 당연히 세 가지 중 하나라도 잘못되면 제빵을 처음부터 하는 방식을 사용해야 겠다. 해서, 각 트랜잭션 메소드가 호출되는 제빵 메서드에서 예외처리를 해보겠다.
제빵() {
try{
계량하기();
반죽하기();
오븐에굽기();
} catch(예외) {
모두취소();
}
}
계량하기() throws 예외 {
...
}
반죽하기() throws 예외 {
...
}
오븐에굽기() throws 예외 {
...
}
이렇게 하면 셋 중 한 과정이라도 예외가 발생했을 때 제빵을 처음부터 다시 하게 될 것이다.
만약 각 계량하기, 반죽하기, 오븐에굽기 메서드에 예외처리를 한다면, 재료없이 허공에 반죽과 오븐에굽는것만 실행이 되거나, 계량만 하고 오븐에 굽거나, 계량과 반죽만 하고 오븐에 굽지 않는 등 뒤죽박죽해져서 빵을 만들 수 없게 될 것이다.
트랜잭션은 이처럼 예외처리와 매우 밀접한 관련이 있고, 프로그램의 흐름에 따라 예외를 적절한 위치에서 handling하는 것은 매우 중요하다고한다.
Reference
Author And Source
이 문제에 관하여([Java] 예외처리 -Exception Handling), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@smallcherry/Java-ExceptionHandling저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)