오늘은 Java의 예외 처리에 대해 복습을 해 보았다.
예외를 처리하는 방법
Java에서 예외를 처리하는 방법은, try-catch문을 사용하는 것이다.
예외가 발생할 수 있는 코드, 즉 예외가 발생한다면 처리를 해주고 싶은 부분을 try 블럭으로 감싸준다.
그리고 catch 블럭을 이용하여 각 Exception이 발생할 경우를 처리해준다.
만일 예외 발생 여부에 상관없이 공통적으로 수행하고 싶은 작업이 있다면, finally 블럭을 추가하여 적어준다.
try {
// 여기에 쓰인 코드들에서 예외가 발생한다면
// 해당하는 catch문 블럭이 실행된다.
} catch (ArithmeticException e) {
// ArithmeticException이 발생한 경우
} catch (IllegalArgumentException e) {
// IllegalArgumentException이 발생한 경우
} catch (Exception e) {
// 기타 모든 예외 처리
} finally {
// try문이 모두 실행되거나,
// 예외가 발생하여 catch문이 실행된 후,
// 두 경우 모두 공통적으로 수행될 작업들을 작성한다.
}
- 예외가 발생하면, 그 예외에 해당하는 클래스의 인스턴스가 생성된다.
- 생성된 인스턴스를 각 catch문에 명시된 Exception과 instanceof 연산자를 사용하여 검사한다.
- instanceof 연산 결과 true인 catch 블럭을 수행한다.
- 만일 모든 catch 블럭을 검사하였는데 true인 블럭이 없다면 해당 예외는 처리되지 않는다.
가장 마지막 catch문에 Exception 참조변수를 선언하면, 위의 다른 catch문에 포함되지 않는 모든 예외가 처리된다.
그 이유는 Exception이 모든 예외들의 조상 클래스이기 때문이다.
🧐 만일 try 블럭이나 catch 블럭에 return문이 포함되어있다면 finally 블럭은 어떻게 될까?
해당 return문이 실행되기 직전에, finally 블럭이 먼저 실행된 후 반환된다.
public void test() {
try {
System.out.println("아무거나 적어볼게요.");
return;
System.out.println("끝");
} catch (Exception e) {
System.out.println("에러 발생!!!");
return;
} finally {
System.out.println("음음");
}
}
즉 위와 같은 함수 test()를 호출하면, 결과는 다음과 같다.
아무거나 적어볼게요.
음음
예외 직접 일으키기
개발자가 직접 예외를 일으켜줄 수도 있다.
예를 들어 닉네임 설정 시 특수문자를 사용하지 말라고 명시해놓아도 특수문자를 포함해서 제출하는 사용자들은 어디에나 있기 때문이다.
이 경우 사용자 입력에서 특수문자가 존재하는지 확인하고 예외를 직접 일으켜줘야 한다.
예외를 일으킬 때에는 throw 키워드를 사용한다.
- 발생시키려는 예외에 해당하는 클래스의 인스턴스를 생성한다.
- throw 키워드로 예외를 발생시킨다.
Exception e = new Exception("내가 만든 예외");
throw e;
throw new Exception("그냥 이렇게 자주 쓴다. <<나는>>");
닉네임에 특수문자가 포함된 경우 예외를 직접 일으키는 코드를 보자.
public void setNickname(String nickname) {
if(nickname.matches(".*[^a-zA-Z0-9].*")) {
throw new Exception("닉네임에 특수문자를 포함하지 믈르느끈...^^");
}
// ... 계속 무언가를 진행
}
위처럼 직접 예외를 발생시키고 컴파일을 시도하면 setNickname 메서드 위치에서 '예외를 처리하라는 메세지'와 함께 바로 에러가 발생한다.
이는 두 가지 방법으로 처리할 수 있다.
1. try-catch문으로 직접 예외 처리
public void setNickname(String nickname) {
try {
if(nickname.matches(".*[^a-zA-Z0-9].*")) {
throw new Exception("닉네임에 특수문자를 포함하지 믈르느끈...^^");
}
// ... 계속 무언가를 진행
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
근본적으로 바로 예외처리를 해주는 방법이다.
이렇게 예외가 발생한 메서드 내에서 바로 try-catch문으로 예외를 처리해주면 컴파일은 성공적으로 수행된다.
2. throws로 예외 처리 회피하기
회피형처럼 예외를 회피해버릴 수도 있다. 이는 메서드 선언부에 throws 키워드를 사용해서 수행한다.
public void setNickname(String nickname) throws Exception {
if(nickname.matches(".*[^a-zA-Z0-9].*")) {
throw new Exception("닉네임에 특수문자를 포함하지 믈르느끈...^^");
}
// ... 계속 무언가를 진행
}
이렇게 회피하면 더이상 setNickname 메서드 위치에서 에러가 발생하지 않는다. 대신, 이젠 setNickname을 호출한 메서드의 위치에서 에러가 발생하게 된다.
만일 register() 라는 메서드에서 setNickname()을 호출했다고 하자. 이 때 setNickname() 내에서 예외가 발생하면 throws 키워드에 의해 register로 예외 인스턴스가 전달되게 된다. 이제 register 메서드가 예외 처리 책임을 갖게 되는 것이다.
이 경우 register 메서드 역시 try-catch문을 사용하여 예외를 본인이 처리하거나, 혹은 throws 키워드를 통해 처리를 회피할 수 있다. 만일 throws 키워드를 통해 처리를 회피할 경우, register 메서드를 호출한 메서드에게로 똑같이 예외 인스턴스가 넘어가게 되고 ....
만일 이 메서드에서도 회피를 하게 되면 예외가 넘어가고 넘어가고 티기고 티기고 ... 결국 메서드 스택의 가장 밑에 위치한 메서드까지 예외가 전파되어 해당 위치에서 에러가 발생한다.
(결국 누군가는 예외 처리를 해줘야한다는 사실!)
처리해주지 않아도 되는 예외가 있다? O / X
정답은 O이다. 위에서 컴파일 시 '예외를 처리하라'는 에러가 떴었던 코드를 다음과 같이 바꾸면 에러가 나지 않는다.
public void setNickname(String nickname) {
if(nickname.matches(".*[^a-zA-Z0-9].*")) {
throw new RuntimeException("닉네임에 특수문자를 포함하지 믈르느끈...^^");
}
// ... 계속 무언가를 진행
}
달라진 점은 Exception이 아닌 RuntimeException을 발생시키고 있다는 점이다.
위 코드가 적힌 파일을 컴파일하면 '예외를 처리하라'는 에러 없이 컴파일이 완료된다.
그러나 위 코드를 실행하는 중 실제 nickname에 특수문자가 포함되어 throw 문을 만나게 될 경우, RuntimeException이 발생한다.
setNickname() 메서드에서 예외를 처리하지 않았으므로 이 메서드를 호출한 메서드로 예외 인스턴스가 넘어가게 된다. 이 과정 중 어느 메서드에서도 예외를 처리하지 않은 채 메서드 스택이 바닥나게 되면 역시 프로그램이 비정상적으로 종료되게 된다.
그러니까 아예 처리해주지 않아도 된다기 보다는, 처리가 되었는지 여부를 컴파일러가 검사하지 않는 예외가 있다.
그림에서 볼 수 있듯이, 'RuntimeException'과 이를 상속하는 Exception들은 컴파일러가 예외 처리 여부를 검사하지 않는다.
이렇게 컴파일러가 처리를 강제하지 않는 예외의 유형을 Unchecked Exception, 처리를 강제하는 예외 유형을 Checked Exception으로 나누어 얘기한다.
(검사 안 한다고 좋아하면 안 됨 그만큼 개발자가 알아서 잘 처리해야 한다는 의미임)
(궁금증) Unchecked Exception을 throws하면 ...?
Checked 예외의 경우, 예외가 발생한 메서드 내에서 예외 처리를 해주지 않으면 throws 키워드를 반드시 달아주어야 한다.
그리고 이 throws 키워드가 달린 메서드를 호출하는 호출자의 경우 throws 이후에 명시된 Exception을 처리하는 try-catch문을 포함하거나 혹은 throws 키워드로 책임을 다시 회피하는 선택지 중 하나를 반드시 선택하여야 한다. 그렇지 않으면 컴파일 과정에서 에러가 발생한다.
그렇다면 Unchecked 예외의 경우는 어떨까?
Unchecked 예외를 발생시킨 메서드에서 throws 키워드를 통해 책임을 회피하고, 다시 이 메서드를 호출한 메서드에서 (1. try-catch로 직접 처리 / 2. throws로 책임 회피) 두 선택지 중 하나도 선택하지 않는다면 컴파일 에러가 발생할까?
정답은 X이다.
Unchecked 예외에 대해서는 throws를 명시적으로 선언하고 호출자가 이를 처리하지 않아도 컴파일 오류는 발생하지 않는다.
'프로그래밍 언어 > Java' 카테고리의 다른 글
추상클래스 vs 인터페이스, 언제 어떤 것을 사용할까? (0) | 2025.01.07 |
---|---|
Java Enum 이놈아 (1) | 2025.01.03 |
잠깐 ! JVM 정리하고 갑시다 (1) | 2024.12.31 |
[Java] 문자열(String) 내장 함수 정리 (1) | 2024.01.11 |
[Java] CharSequence란? (0) | 2024.01.11 |