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 Exception Checked Exception throws필요❌throws필요⭕컴파일 오류가 발생하지 않기 때문.컴파일 오류가 발생하기 때문. ➡️ 처리 필수! ↳ 컴파일러: Unchecked Exception( RuntimeException) 처리 체크 안함Checked Exception는 체크함. throws를 보고 예외를 처리(try-catch) or 다시 전달(throws) 할 수 있다.
try-catch다르다. 발생한 예외를 잡아 복구/문제 완화 하는 것.
프로그램이 중단되지 않도록 안전하게 처리하거나 대체 로직을 실행하여 문제를 해결하는 것.
다양한 단계를 거칠 수 있겠지만 결국 마지막에는 try-catch로 처리해준다. (그렇지 않으면 jvm에게 던지는 것)
쓰레드는 프로그램의 실행 단위이다.
하나의 쓰레드가 하나의 실행 흐름을 가진다.
이때 "실행한다"의 의미는 "cpu를 점유한다"는 뜻이다.
(즉 stack frame 속 데이터들도 CPU의 레지스터를 사용하는 셈이다.)
하나의 thread는 하나의 call stack 을 가진다.
모든 stack frame은 하나의 call stack 위에 층층이 쌓인다.

왜 이런 차이가 발생할까?
C/C++:
Java, Python:
스택 프레임 내부는 스택처럼 동작하는 구조가 아니라, 각 영역이 정해진 용도에 따라 존재한다. 로컬 변수 배열과 연산 스택을 사용하기 때문에 순서가 명확하지 않고 유연한 구조!
- 프레임 내부의 연산 스택 영역만 스택처럼 동작
- 접근은 인덱스 기반으로 이루어짐
| 영역 | 저장 |
|---|---|
| 로컬 변수 영역 (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: 예외 발생
}
}
[main 스택 프레임]이 생성되고 호출 스택에 쌓인다.
methodA() 호출로 [새로운 스택 프레임]이 추가된다.
[methodA() 스택 프레임]
[main() 스택 프레임]
methodA가 methodB()를 호출하므로 [methodB 스택 프레임]이 추가된다.
[methodB() 스택 프레임]
[methodA() 스택 프레임]
[main() 스택 프레임]
10 / 0 )methodB()에서 ArithmeticException 예외 발생
JVM이 예외 객체를 힙 메모리에 생성
힙 메모리: [ArithmeticException 객체 ("divide by zero")]
methodB)의 스택 프레임에 전달된다.methodB는 try-catch가 없으므로 호출 스택 상위(methodA)로 예외가 전달된다.= 호출 계층
methodA의 스택 프레임에게 참조값을 넘겨준다.
↳ [ 상위 메서드methodA] 가 [예외 발생 메서드methodB]를 호출함!
심화
① 예외 객체의 참조값이 현재 메서드(
methodB)의 스택 프레임에 등록된다.
methodB의 스택 프레임은 힙 메모리의 예외 객체를 참조하게 된다.
이때methodB는 예외를 처리할 준비를 갖추지만, 아직 처리하지 않았다.②
methodB실행 중단
methodB가 예외를 처리하지 않고 있기 때문에 실행이 중단되고 스택 프레임이 제거된다.③ 예외 객체의 참조값은 호출 계층(상위 메서드
methodA)로 전달된다.
methodA도 예외를 처리하지 않으므로, [main의 스택 프레임]으로 예외가 전달된다.
[main() 스택 프레임] ← 예외 객체 전달
main 메서드의 catch 블록이 예외 객체를 참조 변수 e로 받는다.
예외 객체의 메시지를 출력한다.
출력
예외 처리: / by zero
프로그램 정상 종료
호출 스택 변화
①
main()→methodA()→methodB()(예외 발생)
② 예외가 발생한methodB의 스택 프레임 제거
③methodA로 전달 →methodA의 스택 프레임 제거
④main으로 전파 →catch블록에서 처리
⑤ 예외 처리 후,main의 나머지 코드 실행
프로그램 실행 중 비정상적인 상태인 예외 상황이 발생했을 때 객체 지향적 설계에 맞게 에러를 나타내는 데이터가 아니라 하나의 독립된 "객체"로 예외 상황을 표현한다.
힙 영역에 생성된 이 예외 객체를 catch블록으로 잡는 것.
"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
Exception의 하위 클래스 중에서 RuntimeException을 제외한 나머지 클래스들이 Checked Exception으로 분류된다.외부적 요인으로 발생.
그러므로 반드시 예외 처리를 하도록 강제한다. try-catch , throws
발생시점
컴파일 시 : "코드로 반드시 처리해~" by 컴파일러 : 강제화
대표적 예시
IOExceptionSQLExceptionFileNotFoundExceptionRuntimeException내부적 요인: 개발자의 실수로 발생.
따라서 예외 처리를 강제할 필요가 없다.
↳ 로직 수정으로 해결해야 함
발생시점
실행 시 : 예외처리코드 강제 x (컴파일 때 그냥 넘어감)
대표적 예시
ArrayIndexOutOfBoundsException )NullPointerException )ClassCastException )ArithmeticException , 예: 0으로 나누기)Throwable
│
├── Exception ← Checked Exception
│ ├── IOException
│ ├── SQLException
│ └── (기타 직접 하위 클래스들)
│
├── RuntimeException ← Unchecked Exception
│ ├── NullPointerException
│ ├── ArithmeticException
│ ├── IndexOutOfBoundsException
│ └── (기타 하위 클래스들)
│
└── Error ← 시스템 오류 (예: OutOfMemoryError)

RuntimeException의 하위 클래스인 Unchecked Exception. 컴파일러가 예외 처리를 강제하지 않는다.
반드시 예외 처리를 해줘야 했다.
try-catch-finally예외 직접 처리하기.
throw예외 던지기.
메서드에 예외처리가 필요함을 나타내기 위해 throws라고 명시되어 있다.
부른 쪽에서 예외처리
try-catch와 throws는 대부분 Checked Exception에서 사용이 되며, unchecked exception인
RuntimeException에서 사용되는 경우는 드물다.
물론 특정 상황에서는 사용되기도 한다.
자바에서 제공하는 IO 관련 클래스의 메서드는 대부분 CheckedException인 IOException을 던진다.
대표적 예:
FileInputStream 클래스BufferedReader 클래스Socket 클래스이들은 모두 공통적인 인터페이스와 구조를 따른다.
특히 메서드 정의에서 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이면 예외처리를 해주자.
사용자가 직접 예외 처리를 해줘야 한다.
throws IOException을 선언하거나 try-catch를 사용해서!
상황이 예외라고 정의하고 예외를 발생시키기.
e.g.1 은행앱에서 잔고보다 큰 금액 인출하려고 할 때
값 자체로는 컴퓨터에서 잘못됐다고 인식하기는 어려울 것
e.g.2 성적관리프로그램
0~100 점수를 벗어난 101점 처리
Checked Exception과 Unchecked 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블록에서는 뭐라도 해줘야 함. 메시지라도 출력해줘야 함. 그래야 에러가 발생했다는 걸 알 수 있음. 아무것도 없으면 뭐가 잘못됐는지조차 알 수 없음
떠넘기는 게 안 좋은 게 아님. 오히려 빠르게 보고하는 게 더 좋을 수 있음.
어떤 예외에 대해서는 누가 처리해야 하지를 결정하는 게 중요할 수 있음 프로젝트 할 때
...unhandled exception...
: 예외처리해 라는 뜻
= 프로그램 실행 중 사용하는 외부 자원
Java에서 리소스 관리를 더 효율적으로 하기 위해.
파일, 데이터베이스, 네트워크 연결 등은 사용 후 꼭 닫아야 한다.
닫지 않으면:
반드시 닫아야 하는 애들은 close()를 가지고 있다.
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
BufferedReader를 먼저 닫고 난 뒤 FileReader를 닫는다.
finally 블록에서 BufferedReader에서 예외가 발생하면 FileReader는 닫히지 못한다.close()가 호출되므로, BufferedReader를 닫는 도중 예외가 발생해도, FileReader는 안전하게 닫힌다.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(); }
}
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이다.
(상속의 속성)- 메시지 다 똑같다 하면 생성자 인자 말고 디폴트 메서드로도 구현해볼 수 있을 것임