1. 학습 주제
예외 처리의 기본 구조와 입출력 스트림의 종류를 정리했다.
특히 예외를 단순히 try-catch 문법으로만 보는 것이 아니라, 왜 계층 구조로 설계하는지, 그 안에서 다형성이 어떻게 적용되는지까지 함께 이해하는 것이 중요했다.
IO에서는 파일과 스트림의 개념, 바이트 스트림과 문자 스트림의 차이, 그리고 버퍼 보조 스트림의 역할까지 확인했다.
- throw, throws, try-catch-finally 정리하기
- checked exception과 unchecked exception 차이 이해하기
- 사용자 정의 예외 만들기
- 예외 계층 구조를 사용하는 이유 이해하기
- 예외 처리에서 다형성이 어떻게 동작하는지 정리하기
- File, 바이트 스트림, 문자 스트림, 버퍼 스트림 학습하기
- try-with-resources로 자원 반납 자동화하기
2. 예외(Exception)란
예외는 프로그램 실행 중 발생할 수 있는 비정상 상황이다.
예를 들어
- 돈이 부족한데 결제를 시도하는 경우
- 존재하지 않는 파일을 읽으려는 경우
- 입출력 중 오류가 나는 경우
예외가 발생했는데 처리하지 않으면 프로그램이 비정상 종료될 수 있다.
그래서 자바는 예외를 객체로 다루고, 필요한 곳에서 잡거나 위임하도록 만든다.
3. throw와 throws
이 둘은 꼭 구분해야 한다.
3.1 throw
실제로 예외를 발생시키는 키워드이다.
throw new Exception("돈이 부족합니다!");
- 예외 객체를 직접 만들어 던진다
- “이 상황은 정상 흐름이 아니다”라고 알리는 역할이다
3.2 throws
메서드가 예외를 직접 처리하지 않고 호출한 쪽으로 넘길 때 사용한다.
public void checkEnoughMoney(int price, int money) throws Exception
- 이 메서드는 예외가 발생할 수 있다고 선언하는 것
- 실제 처리 책임은 호출한 쪽으로 넘어간다
한 줄 정리
- throw 예외를 발생시킨다
- throws 예외 처리 책임을 넘긴다
4. try-catch-finally
예외가 발생할 수 있는 코드를 안전하게 감싸는 기본 구조이다.
try {
et.checkEnoughMoney(3000, 1000);
} catch (Exception e) {
System.out.println("상품 구입 불가");
} finally {
System.out.println("계산이 끝나 영수증을 출력합니다.");
}
4.1 try
- 예외가 발생할 가능성이 있는 코드 작성
4.2 catch
- 발생한 예외를 잡아서 처리
4.3 finally
- 예외 발생 여부와 상관없이 반드시 실행
- 자원 반납, 마무리 작업에 자주 사용
중요한 점
- try 안에서 예외가 발생하면 그 아래 코드는 즉시 중단된다
- 제어가 바로 catch로 이동한다
- finally는 거의 항상 실행된다
5. Checked Exception과 Unchecked Exception
이 부분은 아주 중요하다.
5.1 Checked Exception
컴파일 시점에 반드시 처리 여부를 확인하는 예외이다.
처리하지 않으면 컴파일 에러가 난다.
대표 예
- IOException
- FileNotFoundException
예제에서도 파일 입출력은 거의 모두 checked exception을 다룬다.
new FileInputStream("...");
이 코드는 파일이 없을 수 있기 때문에 자바가 “반드시 처리해라”라고 강제한다.
특징
- try-catch 하거나
- throws로 위임해야 한다
- 컴파일러가 확인한다
즉 checked exception은 “복구 가능성이 있어 보여서 개발자에게 꼭 처리하라고 요구하는 예외”라고 이해하면 좋다.
5.2 Unchecked Exception
컴파일러가 강제로 처리하라고 요구하지 않는 예외이다.
실행 중에 발생할 수 있다.
대표 예
- NullPointerException
- ArithmeticException
- ArrayIndexOutOfBoundsException
- ClassCastException
예를 들어
- null 객체에 메서드 호출
- 0으로 나누기
- 배열 범위 초과 접근
이런 것은 주로 “코드 작성 실수”에 가까운 경우가 많다.
특징
- try-catch를 안 써도 컴파일은 된다
- 하지만 실행 중 터질 수 있다
- 보통 논리 오류를 고치는 쪽이 더 중요하다
5.3 둘의 차이 정리
Checked Exception
- 컴파일러가 처리 강제
- 주로 외부 환경 문제
- 복구 가능한 상황이 많음
- 예 파일 없음, 네트워크 오류, 입출력 실패
Unchecked Exception
- 컴파일러가 처리 강제 안 함
- 주로 코드 실수나 잘못된 사용
- 예 null 참조, 잘못된 캐스팅, 인덱스 범위 초과
한 줄로 정리하면
- checked “이건 네가 대비해야 해”
- unchecked “이건 네 코드가 잘못됐을 가능성이 커”
6. 예외도 계층 구조로 만드는 이유
예외는 클래스이기 때문에 상속 구조를 가진다.
이번 예제에서는 사용자 정의 예외를 계층적으로 만들었다.
NegativeException extends Exception
MoneyNegativeException extends NegativeException
PriceNegativeException extends NegativeException
NotEnoughMoneyException extends Exception
이렇게 계층 구조를 만드는 이유는 크게 세 가지이다.
6.1 공통 성격을 묶기 위해
PriceNegativeException과 MoneyNegativeException은 둘 다 “음수 관련 문제”이다.
그래서 공통 부모인 NegativeException으로 묶을 수 있다.
이렇게 하면 코드 의미가 더 명확해진다.
- 가격 음수
- 돈 음수
- 둘 다 음수 문제의 하위 타입
6.2 예외 처리 범위를 유연하게 하기 위해
세부 예외를 따로 잡을 수도 있고, 상위 예외로 한 번에 잡을 수도 있다.
즉 계층 구조가 있으면
- 구체적으로 처리할 수도 있고
- 묶어서 처리할 수도 있다
6.3 다형성을 활용하기 위해
이 부분이 매우 중요하다.
예외 계층 구조는 상속 구조이기 때문에 다형성이 그대로 적용된다.
예를 들어 MoneyNegativeException 객체는 동시에
- MoneyNegativeException
- NegativeException
- Exception
- Throwable
- Object
로 볼 수 있다.
즉 자식 예외 객체를 부모 예외 타입으로 받을 수 있다.
이게 catch 문과 throws 선언을 유연하게 만들어 준다.
7. 예외 처리에서 다형성이 어떻게 적용되는가
예외도 클래스이므로 상속과 다형성이 그대로 적용된다.
예를 들어
catch (PriceNegativeException | MoneyNegativeException e)
이 코드는 두 자식 예외를 각각 잡는 방식이다.
반대로
catch (Exception e)
이 코드는 더 상위 타입인 Exception으로 모든 하위 예외를 받을 수 있다.
이게 가능한 이유는 다형성 때문이다.
7.1 catch에서의 다형성
실제로 던져진 예외가 MoneyNegativeException이어도
catch (Exception e)가 받을 수 있다.
왜냐하면
- MoneyNegativeException is-a Exception
- 즉 자식 객체를 부모 타입 변수로 받을 수 있기 때문이다
7.2 throws에서의 다형성
메서드 선언부에서도 상위 예외 타입으로 넓게 선언할 수 있다.
public void method() throws IOException
그리고 오버라이딩에서는 더 구체적인 자식 예외를 선언할 수 있다.
@Override
public void method() throws FileNotFoundException
이것도 다형성과 상속 구조 덕분이다.
- FileNotFoundException은 IOException의 자식
- 더 구체적인 예외를 선언하는 것은 가능하다
7.3 왜 유용한가
예외를 일일이 전부 따로 처리하지 않아도 된다.
공통 부모 타입으로 묶어서 처리할 수 있기 때문이다.
즉 예외 계층 구조 + 다형성이 있으면
- 공통 처리 가능
- 세부 처리도 가능
- 확장에 유리함
8. 사용자 정의 예외
예제에서는 직접 예외 클래스를 만들어 사용했다.
throw new PriceNegativeException("상품 가격은 음수일 수 없다.");
throw new MoneyNegativeException("가진 돈이 음수일 수 없다.");
throw new NotEnoughMoneyException("가진 돈보다 상품 가격이 더 비싸다.");
장점
- 예외 의미가 훨씬 명확해진다
- 단순히 Exception 하나만 쓰는 것보다 가독성이 좋다
- 어떤 문제인지 분기 처리하기 쉬워진다
예외 메시지를 부모 Exception에 넘기는 것도 중요하다.
public NegativeException(String message) {
super(message);
}
그래야 e.getMessage()로 메시지를 꺼낼 수 있다.
9. 오버라이딩과 예외 처리 규칙
예외도 오버라이딩 규칙의 영향을 받는다.
부모
public void method() throws IOException
자식
@Override
public void method() throws FileNotFoundException
가능한 경우
- 같은 예외 선언
- 더 구체적인 자식 예외 선언
- 예외를 아예 안 던지는 것
불가능한 경우
- 더 넓은 부모 예외 선언
- 전혀 관계없는 새로운 checked exception 선언
이 규칙이 필요한 이유는, 부모 타입으로 자식을 다루는 다형성을 깨지 않기 위해서다.
부모 타입 사용자는 IOException까지만 대비하고 있는데,
자식이 갑자기 더 넓은 Exception을 던지면 계약이 깨진다.
즉 이 규칙도 결국 다형성을 안전하게 유지하기 위한 장치라고 볼 수 있다.
10. IO란
IO는 Input/Output, 즉 입력과 출력을 의미한다.
예
- 파일 읽기/쓰기
- 키보드 입력 받기
- 화면 출력
- 네트워크 데이터 송수신
이번 범위에서는 주로 파일과 키보드 입력을 다뤘다.
11. File 클래스
File 클래스는 파일 자체의 내용을 읽는 스트림은 아니고, 파일이나 경로 정보를 다루는 객체이다.
File file = new File("src/com/ohgiraffers/section01/test.txt");
할 수 있는 것
- 파일 생성
- 파일 삭제
- 경로 확인
- 크기 확인
file.createNewFile();
file.length();
file.getPath();
file.getAbsolutePath();
file.delete();
중요한 점
- File 객체를 만든다고 파일이 즉시 생성되는 것은 아니다
- 실제 생성은 createNewFile() 호출 시 이뤄진다
12. 스트림이란
스트림은 데이터가 흐르는 통로이다.
자바 IO는 이 스트림을 통해 파일이나 입력 장치와 데이터를 주고받는다.
크게 두 가지로 나뉜다.
12.1 바이트 스트림
- 1바이트 단위
- 모든 파일 처리 가능
- 이미지, 영상, 바이너리 파일 등에 적합
12.2 문자 스트림
- 1문자 단위
- 텍스트 처리에 적합
- 한글 같은 문자 처리에 더 자연스럽다
13. 바이트 스트림
13.1 FileInputStream
파일에서 1바이트씩 읽어온다.
FileInputStream fin = new FileInputStream("...");
while((value = fin.read()) != -1) {
System.out.println((char)value);
}
- read()는 더 읽을 값이 없으면 -1 반환
- 텍스트뿐 아니라 모든 파일에 사용 가능
13.2 FileOutputStream
파일에 1바이트씩 쓴다.
FileOutputStream fout = new FileOutputStream("...");
fout.write(97);
- 97은 아스키 코드로 a
- 바이트 배열도 쓸 수 있다
14. 문자 스트림
14.1 FileReader
파일을 문자 단위로 읽는다.
FileReader fr = new FileReader("...");
while((value = fr.read()) != -1) {
System.out.println((char)value);
}
- 텍스트 파일 읽기에 적합
- 인코딩과 실행 환경 문자셋이 다르면 글자가 깨질 수 있다
14.2 FileWriter
문자 단위로 파일에 출력한다.
FileWriter fw = new FileWriter("...");
fw.write("안녕하세요 반가워요");
- 문자열이나 문자 배열을 자연스럽게 쓸 수 있다
- 텍스트 파일 작성에 적합하다
15. 버퍼 보조 스트림
기본 스트림 위에 덧붙여 성능과 편의 기능을 추가하는 스트림이다.
15.1 BufferedWriter / BufferedReader
BufferedWriter bw = new BufferedWriter(new FileWriter("..."));
BufferedReader br = new BufferedReader(new FileReader("..."));
장점
- 버퍼를 사용해 입출력 횟수를 줄여 성능 향상
- BufferedReader는 readLine() 제공
- 텍스트 파일을 줄 단위로 읽기 편하다
String temp;
while ((temp = br.readLine()) != null) {
System.out.println(temp);
}
15.2 flush()
버퍼에 모인 데이터를 실제 목적지로 밀어내는 동작이다.
bw.flush();
다만 close() 과정에서도 보통 자동으로 flush가 함께 수행된다.
16. 형변환 보조 스트림
System.in은 바이트 스트림이다.
그런데 문자열 단위로 읽고 싶으면 문자 스트림으로 바꿔야 한다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
구조
- System.in 바이트 입력
- InputStreamReader 바이트를 문자로 변환
- BufferedReader 버퍼 + readLine() 기능 추가
이 조합은 “키보드 입력을 한 줄 문자열로 받기”에 자주 사용된다.
17. try-with-resources
IO에서는 스트림을 반드시 닫아야 한다.
그런데 직접 finally에서 닫는 코드는 번거롭다.
try(FileOutputStream fout = new FileOutputStream("...")) {
...
}
이렇게 하면
- try가 끝날 때
- 예외가 나든 정상 종료든
- 자동으로 close()가 호출된다
즉 자원 반납을 안전하고 간단하게 처리할 수 있다.
실무에서도 매우 자주 쓰이는 방식이다.
18. 예외와 IO가 연결되는 이유
IO는 외부 파일, 디스크, 경로, 장치 같은 환경에 의존한다.
그래서 실패 가능성이 항상 존재한다.
예
- 파일이 없음
- 경로가 잘못됨
- 읽기 권한 없음
- 쓰기 중 오류 발생
이런 상황은 프로그램 내부 논리 오류라기보다 “외부 환경 문제”에 가깝다.
그래서 자바는 IO 관련 예외를 주로 checked exception으로 두고,
“이건 반드시 대비해라”라고 강제한다.
즉 IO에서 checked exception이 많은 이유는
- 실패 가능성이 현실적으로 높고
- 개발자가 복구/대응 로직을 준비해야 하기 때문이다
19. 흐름 정리
19.1 예외 처리
- throw로 예외 발생
- throws로 호출한 쪽에 위임
- try-catch-finally로 처리
- 사용자 정의 예외로 의미를 더 명확히 표현
- 예외 계층 구조와 다형성으로 공통 처리와 세부 처리를 모두 가능하게 함
19.2 입출력
- File로 파일 메타정보 다루기
- 바이트 스트림으로 모든 파일 처리
- 문자 스트림으로 텍스트 처리
- 버퍼 보조 스트림으로 성능과 편의성 향상
- try-with-resources로 자원 반납 자동화
20. 핵심 정리
- 예외는 실행 중 발생하는 비정상 상황을 객체로 표현한 것이다.
- throw는 예외를 발생시키고, throws는 처리 책임을 위임한다.
- checked exception은 컴파일러가 처리 여부를 강제하고, unchecked exception은 강제하지 않는다.
- checked exception은 주로 외부 환경 문제, unchecked exception은 주로 코드 실수와 관련이 있다.
- 예외를 계층 구조로 만드는 이유는 공통 성격을 묶고, 유연한 처리와 다형성을 가능하게 하기 위해서다.
- 예외도 클래스이므로 상속과 다형성이 적용되며, 상위 타입으로 하위 예외를 받을 수 있다.
- 오버라이딩에서 예외 선언이 제한되는 이유도 다형성을 안전하게 유지하기 위해서다.
- IO 스트림은 데이터가 흐르는 통로이며, 바이트 스트림과 문자 스트림으로 나뉜다.
- BufferedReader, BufferedWriter는 성능 향상과 편의 기능을 제공하는 보조 스트림이다.
- IO는 외부 환경에 의존하므로 checked exception이 많이 등장한다.
- try-with-resources는 스트림 같은 자원을 자동으로 닫아주는 안전한 문법이다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]SQL 조회 기초: SELECT, ORDER BY, WHERE, DISTINCT, LIMIT, JOIN (0) | 2026.05.14 |
|---|---|
| [TIL]enum, lambda, stream 실무 관점 정리 (0) | 2026.05.13 |
| [TIL]Java Collection Framework, List / Set / Map 핵심 정리 (0) | 2026.05.11 |
| [TIL]Java API, Wrapper, 오토박싱/오토언박싱, 제네릭 (1) | 2026.05.08 |
| [TIL]객체 배열 활용, 상속, 오버라이딩, 다형성, 동적 바인딩, 추상클래스와 인터페이스 (0) | 2026.05.07 |