오류 및 예외에 대한 이해
오류(Error)🔥 | 예외(Exception)🚨 |
|
|
- 보통 만나게 되는 문제는 예외이고 이는 "예외처리"로 예방이 가능하다.
- 보통 예외는 코드의 실행, 예외 처리 관점 2가지의 종류로 나뉘는데
코드 실행 관점 | 예외처리 관점 |
|
확인된 예외✅ (Checked Exception)
미확인된 예외🚫 (Unchecked Exception)
|
+ 컴파일 에러는 예외 처리를 하지 않아서 발생하는 오류()를 말한다. 만약 예외 처리를 생각도 못했는데 컴파일 중 에러가 나왔다면 이건 그냥 에러다.
1. Checked Exception (체크드 예외)
컴파일 타임에 반드시 예외 처리를 요구하는 예외입니다.
컴파일러가 예외 처리(try-catch 또는 throws 선언)가 되어 있는지 확인하며, 처리하지 않으면 컴파일 에러가 발생합니다.
특징
- 발생 가능성이 높은 예외로 간주되며, 반드시 처리해야 합니다.
- 주로 외부 환경과 상호작용하는 코드에서 발생합니다.
예: 파일 입출력, 네트워크 연결, 데이터베이스 접근 등. - Exception 클래스에서 파생되지만, RuntimeException을 제외한 모든 예외가 해당됩니다.
대표적인 Checked Exception
- IOException (파일 입출력 오류)
- SQLException (SQL 관련 오류)
- ClassNotFoundException (클래스를 찾을 수 없음)
import java.io.*;
public class CheckedExample {
public static void main(String[] args) {
try {
FileReader reader = new FileReader("nonexistent.txt"); // 파일 읽기 시도
} catch (FileNotFoundException e) {
System.out.println("파일을 찾을 수 없습니다: " + e.getMessage());
}
}
}
- 여기서 FileReader는 파일이 존재하지 않을 경우 FileNotFoundException을 발생시킵니다.
- 이를 try-catch 블록으로 처리하지 않으면 컴파일 에러가 발생합니다.
보통 대부분의 예외는 아래의 처리 방식을 기준으로 한다
1 | 예외를 인지, 정의 = 예외 클래스 생성 |
2 | 예외 발생시 알림 = throw( 예약어 ) |
3 | 사용자가 예외 핸들링(처리) |
2. Unchecked Exception (언체크드 예외)
컴파일 타임에 예외 처리를 강제하지 않는 예외입니다.
예외 처리를 하지 않아도 프로그램은 정상적으로 컴파일됩니다. 다만, 런타임 시 발생하면 프로그램이 중단될 수 있습니다.
특징
- 주로 프로그래머의 실수로 인해 발생합니다.
- 개발자가 예외 처리를 강제받지 않아도 되지만, 적절히 처리해야 프로그램이 중단되지 않습니다.
- RuntimeException 클래스와 그 하위 클래스들이 포함됩니다.
대표적인 Unchecked Exception
- NullPointerException (널 참조 접근)
- ArrayIndexOutOfBoundsException (배열 인덱스 초과)
- ArithmeticException (0으로 나누기)
- ClassCastException (잘못된 형변환)
예제
public class UncheckedExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
// 예외 처리 없이 실행
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException 발생
}
}
- 이 코드는 예외 처리를 하지 않아도 컴파일은 되지만, 실행 시 ArrayIndexOutOfBoundsException이 발생하며 프로그램이 중단됩니다.
3. Checked vs Unchecked 정리
구분 | Checked Exception | Unchecked Exception |
검사 시점 | 컴파일 타임 | 런타임 |
예외 처리 강제 여부 | 예, 처리하지 않으면 컴파일 에러 발생 | 아니요, 처리하지 않아도 컴파일 가능 |
주요 원인 | 외부 환경(입출력, 네트워크 등) | 프로그래머 실수 또는 논리 오류 |
상위 클래스 | Exception (단, RuntimeException 제외) | RuntimeException |
대표 예 | IOException, SQLException | NullPointerException, ArithmeticException |
4. 실전에서의 사용
- Checked Exception: 외부 환경과 상호작용하는 코드에서 주로 발생하므로 적절히 예외 처리를 반드시 해야 합니다.
- 예: 파일을 읽거나 쓰는 작업에서 파일이 없을 가능성을 항상 고려.
- Unchecked Exception: 런타임 중 예상치 못한 오류를 처리하려면 예외 처리를 추가로 넣을 수 있지만, 기본적으로 개발자가 코드를 주의 깊게 작성하는 것이 중요합니다.
- 예: 배열 인덱스를 안전하게 접근하도록 범위를 체크.
5. 코드 설계 관점
- Checked Exception은 호출자에게 예외를 알리는 역할을 하므로 예측 가능한 문제를 나타낼 때 사용.
- Unchecked Exception은 코드의 버그나 프로그래머 실수를 나타내며, 예외가 발생했을 때 빠르게 수정을 유도합니다.
예외 발생과 try-catch, finally 문
자바에서 예외는 크게 **Checked Exception**과 Unchecked Exception 두 가지로 구분됩니다. 두 예외의 차이는 컴파일러가 예외 처리 여부를 검사하는지 여부에 따라 나뉩니다.
throw
📌 프로그래머가 의도적으로 예외를 발생시킬 때 사용하는 키워드입니다. 주로 특정 조건에서 문제가 발생할 가능성이 있다고 판단될 때 직접 예외를 던져서(발생시켜서) 경고하는 용도로 사용됩니다.
public class Main {
public static void main(String[] args) {
try {
checkEligibility(15); // 18세 미만이므로 예외 발생
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void checkEligibility(int age) {
if (age < 18) {
throw new IllegalArgumentException("나이는 18세 이상이어야 합니다."); // 예외 발생
}
System.out.println("자격이 있습니다.");
}
}
- 실제로 예외가 발생한 후 실행되는 함수이기에, 이 코드가 실행되면 throw 문과 함께 메서드가 종료된다.
- 이게 있는 이유는 아래 설명할 예외 던지기 때문에 필요하다.
- 일반적인 상황에서 프로그래머가 수정을 위해서라면, 컴파일 에러면 충분하다.
예외 던지기
- 메서드에서 발생한 예외를 처리한 후 다시 호출한 쪽으로 전달하거나, 처리하지 않고 바로 호출한 쪽으로 넘기는 경우에 사용됩니다.
1. 예외 되던지기를 사용하는 이유
예외를 처리하지 않고 상위 메서드로 넘기는 이유는:
- 책임 분리
- 모든 메서드가 자신의 역할에만 집중해야 하기 때문입니다.
- 예외를 완전히 처리하기 위한 **문맥(Context)**은 상위 메서드에서 더 많이 알고 있을 가능성이 큽니다.
- 예를 들어, readFile 메서드가 파일을 읽는 동안 오류가 발생했을 때, 이 메서드가 문제를 해결할 수는 없고, 상위 메서드에서 다른 파일을 선택하거나 사용자에게 알리는 등 더 적절한 작업을 할 수 있습니다.
- 공통 처리 로직 적용
- 상위 계층에서 예외를 한꺼번에 처리하면, 중복 코드가 줄어듭니다.
- 예를 들어, 여러 메서드에서 파일을 다룬다면, 파일이 없을 경우 기본적으로 보여줄 에러 메시지는 상위 계층에서 일괄 처리할 수 있습니다.
- 다양한 예외의 관리
- 하위 메서드에서 발생하는 여러 유형의 예외를 상위 계층에서 분류하고 관리할 수 있습니다.
- 예: FileNotFoundException, SQLException 등 서로 다른 예외를 공통된 방식으로 처리하거나 변환.
2. 예제 코드: 두 번의 예외 처리
(1) 예외를 처리한 후 다시 던지는 경우
public class ExceptionRethrowingExample {
public static void main(String[] args) {
try {
processFile();
} catch (Exception e) {
// 최상위 메서드(main)에서 최종적으로 예외 처리
System.out.println("main에서 예외 처리: " + e.getMessage());
}
}
static void processFile() throws Exception {
try {
readFile();
} catch (Exception e) {
// 하위 메서드에서 일부 로그를 남기고 다시 예외를 던짐
System.out.println("processFile에서 예외 처리 중... 메시지: " + e.getMessage());
throw e; // 예외를 상위 메서드로 넘김
}
}
static void readFile() throws Exception {
// 파일 읽기 중 예외 발생
throw new Exception("파일 읽기 실패");
}
}
processFile에서 예외 처리 중... 메시지: 파일 읽기 실패
main에서 예외 처리: 파일 읽기 실패
- readFile()에서 예외 발생 → processFile()에서 로그 처리 후 예외 되던지기 → main()에서 최종 처리.
- processFile()이 예외를 일부 처리하고 다시 던짐으로써 예외가 호출 계층을 따라 전달됩니다.
(2) 예외 정보를 추가하여 되던지기
public class ExceptionRethrowingWithInfo {
public static void main(String[] args) {
try {
processFile();
} catch (Exception e) {
// 최종적으로 예외 처리
e.printStackTrace();
}
}
static void processFile() throws Exception {
try {
readFile();
} catch (Exception e) {
// 새로운 예외로 감싸서 추가 정보를 전달
throw new Exception("processFile 중 문제 발생", e);
}
}
static void readFile() throws Exception {
// 원래 예외 발생
throw new Exception("파일 읽기 실패");
}
}
java.lang.Exception: processFile 중 문제 발생
at ExceptionRethrowingWithInfo.processFile(ExceptionRethrowingWithInfo.java:10)
at ExceptionRethrowingWithInfo.main(ExceptionRethrowingWithInfo.java:4)
Caused by: java.lang.Exception: 파일 읽기 실패
at ExceptionRethrowingWithInfo.readFile(ExceptionRethrowingWithInfo.java:16)
... 2 more
- 예외를 감싸면서 추가 정보를 전달하기 때문에, 디버깅 시 문제의 흐름을 쉽게 파악할 수 있습니다.
3. 예외 되던지기의 사용 사례
대표적인 사용 상황
- 모듈 간 책임 분리
- 예외 처리 책임을 호출한 메서드에 맡기고, 중간 메서드는 일부 로그나 정리 작업만 수행한 뒤 다시 던집니다.
- 일부 처리 후 상위 메서드로 전달
- 예외가 발생했을 때, 중간 메서드에서 로그를 남기거나 상태를 정리한 후 상위 호출자에게 문제를 알립니다.
- 예외 타입 변환
- 저수준 예외(IOException 등)를 고수준 예외(CustomException 등)로 변환해 호출 계층에 전달.
4. 정리
- 예외 되던지기는 하위 메서드에서 처리하지 못하는 예외를 상위 메서드로 전달하는 중요한 기법입니다.
- 필요한 경우:
- 하위 메서드에서 예외를 완전히 처리할 수 없을 때.
- 추가 정보를 포함하거나 새로운 예외로 변환할 때.
- 호출 계층에서 더 나은 처리를 기대할 때.
throws
📌 해당 메서드가 특정 예외를 던질 가능성이 있음을 알리는 역할을 합니다. 예외 처리 책임을 메서드 호출부로 넘길 때 주로 사용됩니다.
public class Main {
public static void main(String[] args) {
try {
divide(10, 0); // 0으로 나누기 시도, 예외 발생
} catch (ArithmeticException e) {
System.out.println("예외 발생: " + e.getMessage());
}
}
// 예외가 발생할 가능성이 있으므로 throws로 명시
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("0으로 나눌 수 없습니다."); // 예외 발생
}
return a / b;
}
}
예외를 handling
📌 말 그대로 예외 처리를 해주면 된다.
1 | 위험 감지하기 ( throw ) |
2 | 위험 처리 ( try-catch(finally) ) |
- 이건 팁인데, 타인의 코드를 사용할 경우 클래스, 메소드의 에러처리를 잘 해줘야한다. 추후 꼬이면 아주 고통스러운 시간을 보내게 된다...
public class StudyException {
public static void main(String[] args) {
OurClass ourClass = new OurClass();
try {
// 1. 위험한 메소드의 실행을 "시도" 해 봅니다.
// "시도" 해보는 코드가 들어가는 블럭입니다.
ourClass.thisMethodIsDangerous();
} catch (OurBadException e) {
// 2. 예외가 발생하면, "잡아서" handling 합니다.
// 예외가 발생하는경우 "handling" 하는 코드가 들어가는 블럭입니다.
// 즉 try 블럭 내의 구문을 실행하다가 예외가 발생하면
// 예외가 발생한 줄에서 바로 코드 실행을 멈추고
// 여기 있는 catch 블럭 내의 코드가 실행됩니다.
System.out.println(e.getMessage());
} finally {
// 3. 예외의 발생 여부와 상관없이, 실행시켜야 하는 코드가 들어갑니다.
// 무조건 실행되는 코드가 들어가는 블럭입니다.
System.out.println("우리는 방금 예외를 handling 했습니다!");
}
}
}
- 위험을 감지했다면, try-catch(finally) 키워드 이용하기
- **try** - **catch**는 각각 중괄호{}를 통해 실행할 코드들을 담습니다.
- try 단어의 **“시도한다”**라는 뜻에 맞게 중괄호{} 안에는 예외가 발생할 수 있지만 실행을 시도할 코드를 담습니다.
- catch 단어의 **“잡는다”**라는 의미에 맞게 중괄호{} 안에는 try 안에 있는 코드를 실행하다가 예외가 났을 때 실행할 코드를 담습니다.
- catch 는 소괄호()를 통해 어떤 예외 클래스를 받아서 처리할지 정의해 주어야 합니다.
- catch로 모든 예외를 다 받고 싶으면 Exception 을 넣어주면 됩니다.
- catch로 일부 예외만 받아서 처리하고 싶으면 해당 예외 클래스명을 넣어주면 됩니다.
- 1개의 try 문에 catch 문은 여러 개 사용할 수 있습니다. ex) 1try : 4catch
- 기존 **try - catch**의 맨 마지막에 **finally**를 붙여서 마지막에 반드시 실행할 코드를 넣을 수 있습니다.
모든 클래스에 다 이렇게 넣는가?
- 당연히 아니다... 핵심적인 로직이 있는 중요한 메서드에만 예외 처리를 추가하거나,
- 여러 메서드에서 예외가 발생할 수 있는 경우, 상위 메서드(또는 main 메서드 등)에 try-catch 블록을 두어 한 번에 예외를 처리합니다.
예외 클래스 구조 이해하기
자바의 Throwable Class
- 시작은 모든 객체의 원형인 Object 클래스에서 시작합니다.
- 아까 정의한 “문제 상황”을 뜻하는 Throwable 클래스가 Object 클래스를 상속합니다.
- Throwable 클래스의 자식으로 앞서배운 에러(Error)와 예외(Exception) 클래스가 있습니다.
- 에러(Error) 클래스와 예외(Exception) 클래스는 각각 IOError 클래스, RuntimeException 클래스와 같이 구분하여 처리됩니다.
- 참고로 그림의 RuntimeException을 상속한 예외들은 UncheckedException, 반대로 상속하지 않은 예외들은 CheckedException으로 구현되어 있습니다.
즉 NullPointException, ArrayIndexOutOfBoundsException 등의 예외 구현체들은 명시적인 에러 처리를 하지 않아도 컴파일 에러가 발생하지는 않겠죠? 또 Checked Exception에 속하는 에러 구현체들은 핸들링 하지 않으면 컴파일 에러가 발생하는 대신, 컴파일이 됐다면 100% 복구가 가능한 에러였다는 것 역시 알아두시면 좋을 것 같습니다. |
- 위의 설명대로 exception은 Object 클래스를 통해 많은 예외 처리를 상속 받은 상태이다. 하지만 결국 프로그램의 세계는 무궁무진 하기에 직접 에러를 정의 구현하는 경우도 있다.
class OurBadException extends Exception {
public OurBadException() {
super("위험한 행동을 하면 예외처리를 꼭 해야합니다!");
}
}
Chained Exception
📌 원인 예외를 새로운 예외에 등록한 후 다시 새로운 예외를 발생시키는데, 이를 예외 연결이라고 합니다.
원인 예외 | 연결된 예외 |
b 라이브러리가 없음 | 함수가 실행 안됨 |
- 이렇게 하는 이유는 추후 한번에 묶어서 다루기가 편하기 때문이다.
- 원인 예외를 다루기 위한 메소드
- initCause()
- 지정한 예외를 원인 예외로 등록하는 메소드
- getCause()
- 원인 예외를 반환하는 메소드
- initCause()
// 연결된 예외
public class main {
public static void main(String[] args) {
try {
// 예외 생성
NumberFormatException ex = new NumberFormatException("가짜 예외이유");
// 원인 예외 설정(지정한 예외를 원인 예외로 등록)
ex.initCause(new NullPointerException("진짜 예외이유"));
// 예외를 직접 던집니다.
throw ex;
} catch (NumberFormatException ex) {
// 예외 로그 출력
ex.printStackTrace();
// 예외 원인 조회 후 출력
ex.getCause().printStackTrace();
}
// checked exception 을 감싸서 unchecked exception 안에 넣습니다.
throw new RuntimeException(new Exception("이것이 진짜 예외 이유 입니다."));
}
}
// 출력
Caused by: java.lang.NullPointerException: 진짜 예외이유
예외 처리
📌예외 처리 방식은 크게 3가지로 나뉜다
1 | 예외 복구하기 |
2 | 예외 처리 회피하기 |
3 | 예외 전환하기 |
예외 복구하기
public String getDataFromAnotherServer(String dataPath) {
try {
return anotherServerClient.getData(dataPath).toString();
} catch (GetDataException e) {
return defaultData;
}
}
- 실제로 try-catch로 예외를 처리하고 프로그램을 정상 상태로 복구하는 방법입니다.
- 가장 기본적인 방식이지만, 현실적으로 복구가 가능한 상황이 아닌 경우가 많거나 최소한의 대응만 가능한 경우가 많기 때문에 자주 사용되지는 않습니다.
예외 처리 회피하기
public void someMethod() throws Exception { ... }
public void someIrresponsibleMethod() throws Exception {
this.someMethod();
}
- 이렇게 처리하면, someMethod()에서 발생한 에러가 someIrresponsibleMethod()의 throws를 통해서 그대로 다시 흘러나가게 되겠죠, 물론 같은 객체 내에서 이런 일은 하지는 않습니다, 예외 처리 회피를 보여드리기 위한 단순한 예시 코드입니다.
- 관심사를 분리해서 한 레이어에서 처리하기 위해서 이렇게 에러를 회피해서 그대로 흘러 보내는 경우도 있습니다.
예외 전환하기
public void someMethod() throws IOException { ... }
public void someResponsibleMethod() throws MoreSpecificException {
try {
this.someMethod();
} catch (IOException e) {
throw new MoreSpecificException(e.getMessage());
}
}
- 예외 처리 회피하기의 방법과 비슷하지만, 조금 더 적절한 예외를 던져주는 경우입니다.
- 보통은 예외 처리에 더 신경 쓰고 싶은 경우나, 오히려 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 바꿔서 던지고 싶은 경우 사용합니다.
< 추가설명 >
- 추상화된 예외로 변환: 구체적인 예외를 숨기고, 상위 계층에서는 필요하지 않은 세부 사항을 가리지 않도록 상위 계층에서 의미 있는 예외로 전환합니다. 예를 들어, 데이터베이스 접근 중 SQLException이 발생하면, 이 예외를 DataAccessException과 같은 의미 있는 커스텀 예외로 바꾸는 식입니다.
- 런타임 예외로 일괄 처리: Checked Exception을 RuntimeException 계열로 변환하여 전체 코드에서 예외 처리를 일괄적으로 처리할 수 있게 합니다. 예외 처리를 모든 곳에서 강제하지 않아 코드가 더 간결해지죠.
< 런타임 예외 일괄 처리 부가 설명 >
1. Checked Exception의 특성
- Checked Exception은 예외가 발생할 가능성이 있는 모든 곳에서 반드시 try-catch 구문으로 처리하거나 throws 키워드를 이용해 메서드 선언부에 던져야 합니다.
- 이로 인해 예외가 발생할 가능성이 있는 메서드가 여러 군데 있다면, 그곳마다 예외 처리를 반복적으로 작성해야 해서 코드가 길어지고 복잡해질 수 있어요.
2. RuntimeException으로 전환 시 장점
- RuntimeException 계열 예외는 일명 "unchecked" 예외라고도 불리며, 이 예외는 컴파일러가 예외 처리를 강제하지 않아요.
- 그래서 try-catch로 처리하지 않아도 컴파일 오류가 발생하지 않습니다.
- 즉, 반드시 처리하지 않아도 되는 예외로 바뀌므로 코드가 간결해집니다. 예외가 꼭 필요한 상위 레벨에서만 일괄 처리할 수 있어요.
'Back-End (Web) > JAVA' 카테고리의 다른 글
[JAVA] 쓰레드 & 람다 함수 & 스트림 (3) | 2024.11.13 |
---|---|
[JAVA] Generic (3) | 2024.11.12 |
[JAVA] 인터페이스 (2) | 2024.11.12 |
[JAVA] 클래스 간의 관계와 상속 (0) | 2024.11.12 |
[JAVA] package와 import (0) | 2024.11.12 |