[멋사 TIL] 13기 day9

이은서·2024년 12월 14일
0

멋사

목록 보기
10/11
post-thumbnail

"예외를 발생시킨다" : throw

"예외를 발생시킨다"의 의미

"예외를 발생시킨다" = throw 키워드로 ①예외 객체를 생성하여 ②던진다.

예외를 발생시키지 않으면 어떤 문제가 있나?

문제가 발생해도 이를 호출 계층에 알리지 않으면, 오류 상태에서 프로그램이 계속 실행되어 잘못된 결과를 초래할 수 있음.

어디로 던지는데?

메소드를 호출한 곳.

readFile()을 호출한 곳으로.

public void readFile(String fileName) throws IOException {
    if (fileName == null) {
        throw new IOException("파일 이름이 null입니다."); // 예외 발생
    }
}

main()은 다른 곳에서 호출하는 메서드 아니므로 jvm에게로.

public class ThrowExample {
    public static void main(String[] args) {
        throw new RuntimeException("예외 발생!"); // 예외 발생
    }
}



throw vs throws

주의: throws 없이도 예외 객체는 전달된다.

public void divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("0으로 나눌 수 없습니다."); 
        // Unchecked Exception
    }
}
public static void main(String[] args) {
    new Example().divide(10, 0); // 호출 계층으로 예외 전달
}

throw의 역할에 이미 호출 계층으로 예외 객체를 던지는 동작이 포함되어 있기 때문에 throws와 상관없이 작동한다. throws는 예외 객체 전달을 제어하지 않는다!

그럼 throws 왜 필요함?

"예외처리 해줘야 하는구나!"   알 수 있도록.
예외 객체가 호출 계층으로 전달되기 위해서가 아니라,
컴파일러가 호출 계층에서 예외 처리를 강제하기 위해 필요함.

Checked Exception vs Unchecked Exception의 throws

Unchecked ExceptionChecked Exception
throws 필요❌throws 필요⭕
컴파일 오류가 발생하지 않기 때문.컴파일 오류가 발생하기 때문. ➡️ 처리 필수!
↳ 컴파일러: Unchecked Exception(RuntimeException ) 처리 체크 안함Checked Exception는 체크함.
throws를 보고 예외를 처리(try-catch) or 다시 전달(throws) 할 수 있다.

"예외를 처리한다" : try-catch

예외를 처리하는 건 발생시키는 거랑 다른가?

다르다. 발생한 예외를 잡아 복구/문제 완화 하는 것.
프로그램이 중단되지 않도록 안전하게 처리하거나 대체 로직을 실행하여 문제를 해결하는 것.

다양한 단계를 거칠 수 있겠지만 결국 마지막에는 try-catch로 처리해준다. (그렇지 않으면 jvm에게 던지는 것)





call stack

thread(쓰레드)

쓰레드는 프로그램의 실행 단위이다.
하나의 쓰레드가 하나의 실행 흐름을 가진다.
이때 "실행한다"의 의미는 "cpu를 점유한다"는 뜻이다.
(즉 stack frame 속 데이터들도 CPU의 레지스터를 사용하는 셈이다.)

구조

하나의 thread는 하나의 call stack 을 가진다.
모든 stack frame은 하나의 call stack 위에 층층이 쌓인다.

언어별 구조

왜 이런 차이가 발생할까?
C/C++:

  • 스택 프레임: 물리적으로 연속된 메모리에 저장.
  • EBP를 사용해 프레임을 구분

Java, Python:

  • 가상 머신/인터프리터를 통해 실행되므로 스택 프레임이 물리적으로 연속적이지 않다
  • "연속된 메모리"   Base Pointer
  • 자바: Base Pointer 대신, JVM이 내부적으로 프레임 관리를 추상화

JVM의 스택 프레임 구조

  • Java는 JVM 위에서 실행되므로 메모리 구조와 스택 관리가 추상화되어 최적화된다. 따라서 하드웨어 스택이 아니라 JVM이 제공하는 소프트웨어 기반의 가상 스택을 사용
  • 각 thread는 독립적인 JVM 스택을 가짐.
  • 메서드 호출 시 [스택 프레임]이 쌓이고 제거됨 ➕➖
  • 메모리 구조가 추상화되어 있음 (물리적 하드웨어 스택 대신 JVM 스택을 사용하기 때문)

스택 프레임 내부는 스택처럼 동작하는 구조가 아니라, 각 영역이 정해진 용도에 따라 존재한다. 로컬 변수 배열과 연산 스택을 사용하기 때문에 순서가 명확하지 않고 유연한 구조!

  • 프레임 내부의 연산 스택 영역만 스택처럼 동작
  • 접근은 인덱스 기반으로 이루어짐

영역저장
로컬 변수 영역 (Local Variables Array)매개변수와 지역 변수를 저장
연산 스택 (Operand Stack)메서드 내의 연산 과정에서 사용되는 값을 저장
메서드 반환 주소 (Return Address)호출한 메서드로 돌아갈 위치를 저장
Constant Pool Reference클래스의 상수와 메서드 참조를 저장

cf1.   String Constant Pool은 JVM 메모리 내에 존재!

cf2.   JVM 메모리 구조

----------------------------------------
| Method Area                          |  ← 메타데이터 영역
|  - 클래스 정보                       |
|  - 메서드 정보                       |
|  - Static 변수                       |
|  - Constant Pool                     |
|  - JIT 컴파일된 코드                 |
----------------------------------------
| Heap                                 |  ← 객체 저장 영역
|  - String Constant Pool              |  ← "hello" 저장
|  - 인스턴스 객체                     |
----------------------------------------
| Stack (스레드마다 독립적)            |  ← 메서드 호출 정보
|  - 스택 프레임                       |
----------------------------------------
| PC 레지스터 (Program Counter)        |
----------------------------------------





메모리 관점에서 바라본 예외처리

예시코드

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            methodA(); // Step 1
        } catch (ArithmeticException e) { // Step 5
            System.out.println("예외 처리: " + e.getMessage());
        }
        System.out.println("프로그램 정상 종료");
    }

    public static void methodA() {
        methodB(); // Step 2
    }

    public static void methodB() {
        System.out.println(10 / 0); // Step 3: 예외 발생
    }
}

Step 1: main 메서드 실행

[main 스택 프레임]이 생성되고 호출 스택에 쌓인다.
methodA() 호출로 [새로운 스택 프레임]이 추가된다.

[methodA() 스택 프레임]  
[main() 스택 프레임]  

Step 2: methodA 실행

methodAmethodB()를 호출하므로 [methodB 스택 프레임]이 추가된다.

[methodB() 스택 프레임]  
[methodA() 스택 프레임]  
[main() 스택 프레임]  

Step 3: 예외 발생 ( 10 / 0 )

methodB()에서 ArithmeticException 예외 발생
JVM이 예외 객체를 힙 메모리에 생성

힙 메모리: [ArithmeticException 객체 ("divide by zero")]
  • 예외는 현재 메서드(methodB)의 스택 프레임에 전달된다.
  • methodBtry-catch가 없으므로 호출 스택 상위(methodA)로 예외가 전달된다.

= 호출 계층 methodA의 스택 프레임에게 참조값을 넘겨준다.
↳ [ 상위 메서드 methodA ] 가 [예외 발생 메서드 methodB ]를 호출함!

심화

① 예외 객체의 참조값이 현재 메서드(methodB)의 스택 프레임에 등록된다.
methodB의 스택 프레임은 힙 메모리의 예외 객체를 참조하게 된다.
이때 methodB는 예외를 처리할 준비를 갖추지만, 아직 처리하지 않았다.

methodB 실행 중단
methodB가 예외를 처리하지 않고 있기 때문에 실행이 중단되고 스택 프레임이 제거된다.

③ 예외 객체의 참조값은 호출 계층(상위 메서드 methodA)로 전달된다.

Step 4: 호출 스택 상위 메서드로 예외 전파

methodA도 예외를 처리하지 않으므로, [main의 스택 프레임]으로 예외가 전달된다.

[main() 스택 프레임] ← 예외 객체 전달

Step 5: catch 블록에서 예외 처리

main 메서드의 catch 블록이 예외 객체를 참조 변수 e로 받는다.
예외 객체의 메시지를 출력한다.

출력

예외 처리: / by zero
프로그램 정상 종료

호출 스택 변화

main()methodA()methodB() (예외 발생)
② 예외가 발생한 methodB의 스택 프레임 제거
methodA로 전달 → methodA의 스택 프레임 제거
main으로 전파 → catch 블록에서 처리
⑤ 예외 처리 후, main의 나머지 코드 실행





예외 "객체"

예외 상황을 표현하기 위해 "객체"가 필요하다.

프로그램 실행 중 비정상적인 상태인 예외 상황이 발생했을 때 객체 지향적 설계에 맞게 에러를 나타내는 데이터가 아니라 하나의 독립된 "객체"로 예외 상황을 표현한다.

  • 예외 클래스를 상속받아 다양한 예외 상황을 표현할 수 있다.
  • 예외 객체를 throw 키워드를 통해 전파하고, catch 블록에서 객체를 받아 처리한다.

힙 영역에 생성된 이 예외 객체를 catch블록으로 잡는 것.

예외 객체 VS 일반 객체(String)

String 객체 = "hello"

String str = "hello";

"hello"라는 문자열이 힙 메모리에 생성되고, 참조 변수 str이 스택 메모리에서 이 객체를 가리킨다.

예외 객체

예외의 종류 (클래스 이름), 예외 메시지 (예외 원인), 스택 트레이스를 담고 있는 객체가 힙에 생성된다.

try {
    int result = 10 / 0;
} catch (ArithmeticException e) { // 예외 객체를 "잡음"
    System.out.println(e.getMessage());
}

예외상황이 발생하면
JVM이 힙에 ArithmeticException 객체를 자동으로 생성해준다.
e : 스택 메모리에서 이 객체를 참조하는 참조 변수 ( ≓ str, str2 )

"throw에서의 던진다"

"던진다"는 참조값을 넘겨주는 과정.

① 예외 객체가 힙 메모리에 생성.
② TO: 호출 계층(예외 발생 메서드를 호출한 상위 메서드)의 스택 프레임에게 참조값을 넘겨준다.
→ 이를 통해 책임이 상위 계층으로 전가

예외 처리 과정

[ 예외 객체의 참조값 ] 을 호출 계층으로 전달(throw)하면,
상위 메서드에서 이를 확인하고 ①처리(catch)하거나 ②다시 던지는 흐름

"catch 잡는다"

catch 블록에서 마침내 이 예외 객체의 참조값을 참조 변수에 할당해주는 것
e = 예외 객체라는 할당이 일어나는 것이다.

public class CatchExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0; // 예외 발생
        } catch (ArithmeticException e) { // 예외 객체를 "잡음"
            System.out.println("예외 메시지: " + e.getMessage());
            e.printStackTrace(); // 예외 객체의 스택 트레이스 출력
        }
    }
}

✏️ catch (ArithmeticException e) 부분이 예외 객체를 참조 변수 e에 할당





RuntimeException vs CheckedException

  • Checked Exception이라는 명시적인 클래스는 없고,
    Exception의 하위 클래스 중에서 RuntimeException을 제외한 나머지 클래스들이 Checked Exception으로 분류된다.


Checked Exception

외부적 요인으로 발생.
그러므로 반드시 예외 처리를 하도록 강제한다.   try-catch , throws

발생시점
컴파일 시 : "코드로 반드시 처리해~" by 컴파일러 : 강제화

대표적 예시

  • 파일 입출력 작업 시 발생하는 IOException
  • 데이터베이스 연결 시 발생하는 SQLException
  • 존재하지 않는 파일에 접근할 때 발생하는 FileNotFoundException

RuntimeException

내부적 요인: 개발자의 실수로 발생.
따라서 예외 처리를 강제할 필요가 없다.
↳ 로직 수정으로 해결해야 함

  • unchecked exception

발생시점
실행 시 : 예외처리코드 강제 x (컴파일 때 그냥 넘어감)

대표적 예시

  • 배열의 인덱스를 잘못 접근하는 경우 ( ArrayIndexOutOfBoundsException )
  • null 참조에 대한 연산 시도 ( NullPointerException )
  • 잘못된 형 변환 시도 ( ClassCastException )
  • 수학적으로 불가능한 연산 시도 ( ArithmeticException , 예: 0으로 나누기)



Throwable
│
├── Exception           ← Checked Exception
│    ├── IOException
│    ├── SQLException
│    └── (기타 직접 하위 클래스들)
│
├── RuntimeException    ← Unchecked Exception
│    ├── NullPointerException
│    ├── ArithmeticException
│    ├── IndexOutOfBoundsException
│    └── (기타 하위 클래스들)
│
└── Error               ← 시스템 오류 (: OutOfMemoryError)

RuntimeException의 하위 클래스인 Unchecked Exception. 컴파일러가 예외 처리를 강제하지 않는다.




⭐checked exception

반드시 예외 처리를 해줘야 했다.

직접처리 or 던지기

try-catch-finally

예외 직접 처리하기.

throw

예외 던지기.
메서드에 예외처리가 필요함을 나타내기 위해 throws라고 명시되어 있다.
부른 쪽에서 예외처리

try-catchthrows는 대부분 Checked Exception에서 사용이 되며, unchecked exception인 RuntimeException에서 사용되는 경우는 드물다.
물론 특정 상황에서는 사용되기도 한다.




IO 관련 예외처리

IO 작업은 외부 자원에 의존하므로 예외 상황이 자주 발생할 수밖에 없다.

자바 표준 라이브러리의 IO 관련 메서드

자바에서 제공하는 IO 관련 클래스의 메서드는 대부분 CheckedExceptionIOException을 던진다.

대표적 예:

  • FileInputStream 클래스
  • BufferedReader 클래스
  • Socket 클래스
  • etc.

이들은 모두 공통적인 인터페이스와 구조를 따른다.
특히 메서드 정의에서 throws IOException으로 예외 처리를 강제한다.
이런 통일된 구조 덕분에 다양한 IO 기능(파일, 네트워크, 메모리 등)이 비슷한 방식으로 사용된다.

cf. 자바의 IO 클래스는 입출력 대상만 바꾸면 코드의 재사용이 쉽다.

InputStream input = new FileInputStream("file.txt"); // 파일
// InputStream input = new ByteArrayInputStream(data); // 메모리

InputStream 클래스의 read() 메서드
🔗 Java Docs 🔎 java api InputStream

OutputStream 클래스의 write(int b) 메서드
🔗 Java Docs 🔎 java api OutputStream

BufferedReader 클래스의 readLine() 메서드
🔗 Java Docs 🔎 java api BufferedReader

"메소드 정의할 때 throws IOException라고 써있었기 때문에 예외처리를 해줘야 했구나~" : 메서드가 생성될때부터 예외가 같이와서 메서드 쓰려면 반드시 try catch 해줘야

TIP.   RuntimeException이라고 안 붙어 있는 Exception이면 예외처리를 해주자.

사용자 정의 메서드에서 IO 작업을 수행하는 경우

사용자가 직접 예외 처리를 해줘야 한다.
throws IOException을 선언하거나 try-catch를 사용해서!




예외처리

상황이 예외라고 정의하고 예외를 발생시키기.

e.g.1   은행앱에서 잔고보다 큰 금액 인출하려고 할 때
값 자체로는 컴퓨터에서 잘못됐다고 인식하기는 어려울 것

e.g.2   성적관리프로그램
0~100 점수를 벗어난 101점 처리




어느쪽을 선택해야 할까?

기존 표준 예외 쓸까, 사용자 정의 예외 쓸까?

  • 기존 예외 클래스 사용하여 예외를 직접 발생시키기
    - Checked ExceptionUnchecked Exception 모두 해당됨
    - throw 키워드를 사용해 이미 존재하는 예외 클래스를 강제로 발생시킨다.
public class DirectExceptionExample {
    private int balance;

    public void deposit(int money) {
        if (money < 0) {
            throw new IllegalArgumentException("입금 금액은 0원 이상이어야 합니다."); 
            // 기존 예외 클래스 사용
        }
        this.balance += money;
        System.out.println(money + "원 입금되었습니다.");
        System.out.println("현재 잔고: " + this.balance);
    }

    public static void main(String[] args) {
        DirectExceptionExample account = new DirectExceptionExample();
        try {
            account.deposit(-100); // 음수 금액으로 예외 발생
        } catch (IllegalArgumentException e) {
            System.out.println("예외 처리: " + e.getMessage());
        }
    }
}

  • 사용자가 직접 만든 예외 클래스를 사용하여 예외를 간접 발생시키기
    - Exception을 상속한 사용자 정의 예외를 사용
    - Exception을 상속하면 Checked Exception ,
    RuntimeException을 상속하면 Unchecked Exception이 됨
class InvalidDepositException extends Exception { // Checked Exception
    public InvalidDepositException(String message) {
        super(message);
    }
}
public class CustomExceptionExample {
    private int balance;

    public void deposit(int money) throws InvalidDepositException {
        if (money < 0) {
            throw new InvalidDepositException("입금 금액은 0원 이상이어야 합니다."); 
            // 사용자 정의 예외 클래스 사용
        }
        this.balance += money;
        System.out.println(money + "원 입금되었습니다.");
        System.out.println("현재 잔고: " + this.balance);
    }

    public static void main(String[] args) {
        CustomExceptionExample account = new CustomExceptionExample();
        try {
            account.deposit(-100); // 음수 금액으로 예외 발생
        } catch (InvalidDepositException e) {
            System.out.println("예외 처리: " + e.getMessage());
        }
    }
}



던질까, 지금 처리해줄까? (throw vs catch)

상황에 따라 다르다.
어떤 경우에는 스스로 짜는 게 나을 수 있고 어떨 때는 기능을 쓰는 쪽에 던져주는 게 나을 수 있다.
💡컴파일은 언젠지, 실행은 언젠지 고려해보기

[ e.g.2 ]   예외처리를 점수를 입력하는 쪽에서 하는 게 맞을까?

e.g. 엄마가 두부 사오라고 했는데 슈퍼에 두부가 없다.

직접 예외처리

옆동네 마트가기


던지기
엄마한테 전화하기 "두부 없어!"
↳ 엄마는 사실 옆동네 마트 두부를 싫어할 수 있음 - 이 경우 엄마가 예외 처리하는 것이 좋을 수 있다


main에서 던지기가 선넘은 이유

main에서 던진다는 것은 jvm에게 던지겠다는 것이기 때문에~
메소드에서 던지는 건 이해할 만하다

그치만 사용하는 경우는 "예외처리 나중에 하고 일단 지금 코드 집중해봐야겠어~" 이런 거임



가장 나쁜 예외 처리?

try-catch문에서 catch 블록 비어있기
엄마한테 두부 떨어졌다고 하든가 뭐든 해야 하는데 아무 일도 안 하는 거
catch블록에서는 뭐라도 해줘야 함. 메시지라도 출력해줘야 함. 그래야 에러가 발생했다는 걸 알 수 있음. 아무것도 없으면 뭐가 잘못됐는지조차 알 수 없음
떠넘기는 게 안 좋은 게 아님. 오히려 빠르게 보고하는 게 더 좋을 수 있음.
어떤 예외에 대해서는 누가 처리해야 하지를 결정하는 게 중요할 수 있음 프로젝트 할 때





cf1. 예외 객체 자체가 하는 일은 없기 때문에 상속만 받으면 된다. 그런데 생성자를 넣을 수는 있음


cf2. 실행결과에 이렇게 나오면

...unhandled exception...

: 예외처리해 라는 뜻





자동 리소스 닫기

리소스란?

= 프로그램 실행 중 사용하는 외부 자원



기술 도입 이유

Java에서 리소스 관리를 더 효율적으로 하기 위해.

리소스를 반드시 닫아야 하는 이유

파일, 데이터베이스, 네트워크 연결 등은 사용 후 꼭 닫아야 한다.
닫지 않으면:

  • 메모리 누수 발생.
  • 사용 가능한 시스템 리소스 부족. 자원(=resource) 은 제한적이다.
  • 예상치 못한 프로그램 동작.

반드시 닫아야 하는 애들은 close()를 가지고 있다.

리소스는 각자 독립적으로 닫는 것이 좋다.

리소스는 보통 계층적으로 사용된다.

BufferedReader br = new BufferedReader(new FileReader("file.txt"));

리소스를 닫을 때는 열었던 순서와 반대로 닫는다.

BufferedReader를 먼저 닫고 난 뒤 FileReader를 닫는다.

  • finally 블록에서 BufferedReader에서 예외가 발생하면 FileReader는 닫히지 못한다.
  • 반면 Try-With-Resources를 사용하면 리소스마다 독립적으로 close()가 호출되므로, BufferedReader를 닫는 도중 예외가 발생해도, FileReader는 안전하게 닫힌다.




리소스 닫기: then & now

then (java 7 이전)

finally 블록에서 직접 close()를 호출

파일 읽기, 데이터베이스 연결, 네트워크 소켓 연결

BufferedReader br = null;
Connection conn = null;
Socket socket = null;
try {
    br = new BufferedReader(new FileReader("file.txt"));
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "user", "password");
    socket = new Socket("example.com", 80);

    // 작업 수행
} catch (IOException | SQLException e) {
    e.printStackTrace();
} finally {
    try { if (br != null) br.close(); } catch (IOException e) { e.printStackTrace(); }
    try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
    try { if (socket != null) socket.close(); } catch (IOException e) { e.printStackTrace(); }
}
  • 코드 복잡해짐
  • 리소스를 닫는 것을 잊어버리기 쉬워 문제가 발생 위험 ⇈


now (java 8 이후)

try-with-resource : close()가 자동으로 일어나므로 더 유리하다.

파일 읽기, 데이터베이스 연결, 네트워크 소켓 연결

import java.io.*;
import java.sql.*;
import java.net.*;

public class ResourceExample {
    public static void main(String[] args) {
        // try-with-resources를 이용해 리소스를 관리
        try (
            // 파일 읽기
            BufferedReader br = new BufferedReader(new FileReader("file.txt"));

            // 데이터베이스 연결
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "user", "password");

            // 네트워크 소켓 연결
            Socket socket = new Socket("example.com", 80)
        ) {
            // 파일 읽기 작업
            System.out.println("File Content: " + br.readLine());

            // DB 작업
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");
            while (rs.next()) {
                System.out.println("User: " + rs.getString("name"));
            }

            // 네트워크 작업
            OutputStream os = socket.getOutputStream();
            os.write("GET / HTTP/1.1\n\n".getBytes());
        } catch (IOException | SQLException e) {
            e.printStackTrace();
        }
        // 여기까지 오면 br, conn, socket은 자동으로 닫힘
    }
}

필요조건

리소스가 AutoCloseable 인터페이스를 구현해야 함.

public interface AutoCloseable {
    void close() throws Exception;
}

다양한 타입의 리소스를 처리하려면 공통적인 접근 방식이 필요하다. 타입이 다 다르면 부를 수 없다.
AutoCloseable 인터페이스를 사용하면 JVM이 이 인터페이스를 통해 리소스를 닫을 수 있다.





해볼 수 있는 거

  • 부모도 메시지 받는 생성자 있는 거 같으니까 super 써보자.
  • 그냥 생성할 수도 있고~ 메시지 넣어서 생성할 수도 있고~

쫌쫌따리 예외 관련 지식

  • [ 예외(exception) ]과 다르게 [ error ]는 프로그래머가 처리하는 것이 불가능
  • Exception을 상속받은 애는 아무것도 안 가지고 있어도 exception이다.
    (상속의 속성)
  • 메시지 다 똑같다 하면 생성자 인자 말고 디폴트 메서드로도 구현해볼 수 있을 것임

0개의 댓글