본문 바로가기
JAVA

[JAVA]자바 예외 처리와 입출력 (IO): 견고한 프로그램을 만드는 핵심!🛡️

by 다다면체 2025. 2. 28.
728x90
반응형

안녕하세요! 오늘은 자바 프로그래밍에서 정말 중요한 두 가지 주제, 예외 처리입출력 (IO) 에 대해 쉽고 재미있게 알아보려고 합니다. 튼튼하고 안정적인 프로그램, 즉 Robust한 프로그램을 만들기 위해서는 이 두 가지 개념을 확실하게 이해하는 것이 필수적이에요! 마치 집을 지을 때 튼튼한 기초 공사와 문과 창문을 잘 만드는 것이 중요한 것처럼요! 🏠

자, 그럼 예외 처리와 입출력의 세계로 함께 출발해 볼까요? ✨

반응형

🔥 예외 (Exception) 와 예외 처리 (try-catch-finally): 프로그램의 안전망!

프로그램을 만들다 보면 예상치 못한 문제, 즉 예외 (Exception) 가 발생할 수 있습니다. 마치 운전을 하다가 갑자기 튀어나오는 장애물을 만나는 것과 같아요! 🚧 예외가 발생하면 프로그램이 갑자기 멈춰버리거나 오작동할 수 있죠. 이러한 상황에 대비하여 프로그램을 안전하게 유지하는 것이 바로 예외 처리 (Exception Handling) 입니다.

왜 예외 처리가 중요할까요? 🤔

  • 프로그램의 비정상 종료 방지: 예외가 발생해도 프로그램이 갑자기 멈추지 않고, 예외를 처리하여 정상적으로 실행을 이어갈 수 있도록 합니다.
  • 안정성 확보: 예상치 못한 상황에서도 프로그램이 안정적으로 작동하도록 만들어 줍니다.
  • 사용자 친화적인 프로그램: 예외 발생 시 사용자에게 친절한 오류 메시지를 보여주거나, 적절한 안내를 제공하여 사용자 경험을 향상시킵니다.

🧱 try-catch-finally 블록: 예외 처리의 기본 구조

자바에서는 try-catch-finally 블록을 사용하여 예외를 처리합니다. 마치 안전 그물망을 쳐서 혹시 모를 사고에 대비하는 것과 같아요! 🥅

try {
    // 예외가 발생할 가능성이 있는 코드
    // ...
} catch (ExceptionType1 e1) {
    // ExceptionType1 예외 발생 시 처리할 코드
    // ...
} catch (ExceptionType2 e2) {
    // ExceptionType2 예외 발생 시 처리할 코드
    // ...
} finally {
    // 예외 발생 여부와 상관없이 항상 실행되는 코드 (선택적)
    // ...
}
  • try 블록: 예외가 발생할 가능성이 있는 코드를 try 블록 안에 작성합니다. 마치 위험 지역에 들어가기 전에 "여기서 사고가 날 수도 있어!" 라고 미리 경고하는 것과 같아요! ⚠️
  • catch 블록: try 블록 안에서 예외가 발생하면, 해당 예외 타입에 맞는 catch 블록이 실행됩니다. 여러 개의 catch 블록을 사용하여 다양한 예외를 처리할 수 있어요. 마치 다양한 종류의 사고에 대비하여 여러 개의 안전 장치를 준비해 놓는 것과 같습니다. 🚑🚒🚓
  • finally 블록: finally 블록은 예외 발생 여부와 상관없이 항상 실행되는 코드 블록입니다. 주로 리소스를 정리하거나 마무리 작업을 할 때 사용합니다. 마치 사고가 나든 안 나든, 사고 현장을 정리하는 마무리 작업반과 같아요! 🧹 (finally 블록은 선택적으로 사용할 수 있습니다.)

예제 코드 (try-catch-finally):

public class ExceptionExample {
    public static void main(String[] args) {
        int numerator = 10;
        int denominator = 0;

        try {
            int result = numerator / denominator; // 0으로 나누기 예외 발생 가능성
            System.out.println("결과: " + result); // 예외 발생 시 이 줄은 실행되지 않음
        } catch (ArithmeticException e) {
            System.out.println("오류: 0으로 나눌 수 없습니다!"); // 예외 처리 코드 실행
            System.out.println("예외 메시지: " + e.getMessage()); // 예외 메시지 출력
        } finally {
            System.out.println("finally 블록은 항상 실행됩니다."); // finally 블록 실행
        }

        System.out.println("프로그램은 계속 실행됩니다."); // 프로그램은 정상적으로 종료되지 않음
    }
}

 

실행 결과:

오류: 0으로 나눌 수 없습니다!
예외 메시지: / by zero
finally 블록은 항상 실행됩니다.
프로그램은 계속 실행됩니다.

위 예제에서 try 블록 안에서 0으로 나누는 예외 (ArithmeticException) 가 발생했지만, catch 블록에서 예외를 처리했기 때문에 프로그램이 비정상적으로 종료되지 않고, finally 블록과 그 이후의 코드도 정상적으로 실행되는 것을 확인할 수 있습니다.

🚦 Checked Exception vs Unchecked Exception: 예외의 종류

자바의 예외는 크게 Checked Exception (체크 예외)Unchecked Exception (언체크 예외) 두 가지 종류로 나눌 수 있습니다. 마치 감기에 걸릴 확률이 높은 날씨와 사고가 날 확률이 높은 길처럼, 예측 가능성과 처리 방식에 따라 예외를 구분하는 거예요! 🌡️🚧

  • Checked Exception (체크 예외):
    • 컴파일 시점에 예외 처리 강제: 반드시 예외 처리를 해야 하는 예외입니다. 컴파일러가 체크 예외 처리 여부를 확인하고, 예외 처리를 하지 않으면 컴파일 에러를 발생시킵니다. 마치 법적으로 안전 벨트를 꼭 매야 하는 것처럼, 반드시 예외 처리를 해야 하는 예외인 거죠! 🦺
    • 예상 가능한 예외: 주로 외부 환경 (파일, 네트워크 등) 과 관련된 예외로, 프로그램 실행 전에 예외 발생 가능성을 예측하고 대비할 수 있습니다.
    • 예시: IOException, FileNotFoundException, SQLException 등
  • Unchecked Exception (언체크 예외):
    • 컴파일 시점에 예외 처리 강제 X: 예외 처리를 강제하지 않는 예외입니다. 예외 처리를 하지 않아도 컴파일 에러는 발생하지 않습니다. 하지만 런타임 시에 예외가 발생하면 프로그램이 비정상적으로 종료될 수 있습니다. 마치 운전 중 갑자기 튀어나오는 고라니처럼, 예측하기 어렵고 갑작스럽게 발생하는 예외와 같습니다. 🦌
    • 주로 프로그래밍 실수로 발생: 주로 개발자의 실수 (논리 오류, 잘못된 매개변수 전달 등) 로 인해 발생하는 예외입니다.
    • 예시: NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException, IllegalArgumentException, ClassCastException 등

Checked Exception vs Unchecked Exception 비교:

구분 Exception (체크 예외) Unchecked Exception (언체크 예외)
예외 처리 강제 O X
컴파일 에러 발생 O (처리 안 할 시) X
예상 가능성 높음 낮음 (주로 프로그래밍 실수)
주요 발생 원인 외부 환경 (IO, 네트워크, DB) 프로그래밍 실수 (논리 오류 등)
예시 IOException, FileNotFoundException NullPointerException, ArithmeticException

🛠️ Custom Exception 만들기: 나만의 특별한 예외!

자바에서는 기본적으로 제공하는 예외 클래스 외에도 사용자 정의 예외 (Custom Exception) 를 만들 수 있습니다. 마치 내가 직접 만든 특별한 경고 표지판처럼, 프로그램의 특정 상황에 맞는 예외를 정의하고 사용할 수 있는 거예요! ⚠️

Custom Exception을 만드는 이유:

  • 코드 가독성 향상: 예외 이름을 통해 예외 상황을 명확하게 설명할 수 있어 코드 가독성을 높입니다.
  • 예외 처리의 세분화: 특정 예외 상황에 대한 맞춤형 예외 처리를 할 수 있습니다.
  • 의미 있는 예외 정보 제공: 예외 객체에 추가적인 정보를 담아 예외 처리 로직에서 활용할 수 있습니다.

Custom Exception 만드는 방법:

  1. Exception 클래스 또는 Exception 클래스의 자식 클래스를 상속하여 새로운 클래스를 정의합니다. (일반적으로 Exception 또는 RuntimeException 을 상속받습니다.)
  2. 생성자를 정의합니다. (선택적으로 예외 메시지, 예외 발생 원인 등을 매개변수로 받는 생성자를 만들 수 있습니다.)

예제 코드 (Custom Exception):

// 사용자 정의 예외 클래스 정의 (Checked Exception)
class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message); // 부모 클래스 Exception의 생성자 호출 (예외 메시지 전달)
    }
}

// 사용자 정의 예외 클래스 정의 (Unchecked Exception)
class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message); // 부모 클래스 RuntimeException의 생성자 호출 (예외 메시지 전달)
    }
}

public class CustomExceptionExample {
    public static void withdraw(int balance, int amount) throws InsufficientBalanceException { // 체크 예외 선언
        if (balance < amount) {
            throw new InsufficientBalanceException("잔액이 부족합니다!"); // 사용자 정의 체크 예외 발생
        }
        System.out.println("출금 완료");
    }

    public static void validateInput(int age) {
        if (age < 0) {
            throw new InvalidInputException("나이는 0보다 작을 수 없습니다!"); // 사용자 정의 언체크 예외 발생
        }
        System.out.println("입력 유효성 검사 통과");
    }

    public static void main(String[] args) {
        int balance = 10000;
        int withdrawAmount = 15000;

        try {
            withdraw(balance, withdrawAmount); // 체크 예외 발생 가능성이 있는 메소드 호출
        } catch (InsufficientBalanceException e) {
            System.out.println("예외 발생: " + e.getMessage()); // 예외 처리
        }

        try {
            validateInput(-5); // 언체크 예외 발생 가능성이 있는 메소드 호출
        } catch (InvalidInputException e) {
            System.out.println("예외 발생: " + e.getMessage()); // 예외 처리 (선택적)
        }
    }
}

 

실행 결과:

예외 발생: 잔액이 부족합니다!
예외 발생: 나이는 0보다 작을 수 없습니다!

위 예제에서는 InsufficientBalanceException (잔액 부족 예외) 와 InvalidInputException (잘못된 입력 예외) 라는 두 개의 사용자 정의 예외 클래스를 만들고, 각각 체크 예외와 언체크 예외로 활용하는 것을 보여줍니다.

💾 입출력 스트림 (Stream) 개요 (Byte Stream, Character Stream): 데이터의 흐름!

프로그램은 데이터를 외부로부터 읽어오거나, 프로그램 결과를 외부로 내보내야 하는 경우가 많습니다. 마치 물이 파이프를 통해 흐르듯이, 프로그램과 외부 장치 (파일, 네트워크, 키보드, 모니터 등) 사이의 데이터 흐름을 스트림 (Stream) 이라고 합니다. 🌊  

 

자바에서는 스트림을 통해 다양한 입출력 작업을 처리할 수 있도록 입출력 스트림 (IO Stream) API를 제공합니다.

IO Stream의 종류:

IO Stream은 데이터의 종류에 따라 크게 Byte Stream (바이트 스트림)Character Stream (문자 스트림) 으로 나눌 수 있습니다. 마치 물을 나르는 파이프와 기름을 나르는 파이프처럼, 데이터 종류에 따라 다른 종류의 파이프를 사용하는 것과 같아요! 🚰🛢️

  • Byte Stream (바이트 스트림):
    • 바이트 단위 데이터 처리: 1바이트 단위로 데이터를 입출력하는 스트림입니다. 이미지, 영상, 오디오, 실행 파일 등 모든 종류의 이진 데이터를 처리할 수 있습니다.
    • 주요 클래스: InputStream (입력 바이트 스트림), OutputStream (출력 바이트 스트림)
    • 대표적인 구현 클래스: FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutputStream 등
  • Character Stream (문자 스트림):
    • 문자 단위 데이터 처리: 문자 단위 (2바이트 또는 가변 길이) 로 데이터를 입출력하는 스트림입니다. 텍스트 파일, HTML 파일 등 텍스트 데이터를 효율적으로 처리하는 데 특화되어 있습니다.
    • 문자 인코딩 자동 처리: 문자 인코딩 (UTF-8, EUC-KR 등) 을 자동으로 처리하여 텍스트 데이터 손실 없이 입출력할 수 있습니다.
    • 주요 클래스: Reader (입력 문자 스트림), Writer (출력 문자 스트림)
    • 대표적인 구현 클래스: FileReader, FileWriter, BufferedReader, BufferedWriter 등

Byte Stream vs Character Stream 비교:

구분 Byte Stream (바이트 스트림) Character Stream (문자 스트림)
데이터 단위 바이트 (1바이트) 문자 (2바이트 또는 가변 길이)
처리 데이터 종류 모든 종류의 이진 데이터 텍스트 데이터
문자 인코딩 처리 X O (자동 처리)
주요 클래스 InputStream, OutputStream Reader, Writer
대표적인 구현 클래스 FileInputStream, FileOutputStream FileReader, FileWriter

🗂️ 파일 입출력 (File I/O) 실습: 파일과 데이터 주고받기!

파일 입출력 (File I/O) 은 프로그램이 파일 시스템의 파일을 읽고 쓰는 작업을 의미합니다. 마치 서류를 파일 캐비닛에 넣거나 꺼내는 것처럼, 프로그램에서 데이터를 파일에 저장하거나 파일에서 데이터를 불러올 수 있는 거예요! 📁

파일 입출력 과정 (일반적인 순서):

  1. 스트림 객체 생성: 파일과 연결된 스트림 객체를 생성합니다. (FileInputStream, FileOutputStream, FileReader, FileWriter 등) 마치 파일 캐비닛의 문을 여는 것과 같아요! 🚪
  2. 데이터 입출력: 스트림을 이용하여 데이터를 읽거나 씁니다. 마치 서류를 캐비닛에 넣거나 꺼내는 작업과 같아요! 📄
  3. 스트림 닫기: 스트림 사용이 끝나면 반드시 스트림을 닫아 리소스를 해제해야 합니다. 마치 파일 캐비닛 문을 닫고 잠그는 것과 같아요! 🔒 (자원 누수 방지!)

예제 코드 (파일 쓰기 - FileWriter):

import java.io.FileWriter;
import java.io.IOException;

public class FileWriterExample {
    public static void main(String[] args) {
        String filePath = "output.txt"; // 저장할 파일 경로
        String content = "안녕하세요!\n자바 파일 입출력 실습입니다.\n파일에 텍스트를 씁니다.";

        FileWriter writer = null; // FileWriter 객체 선언 (finally 블록에서 사용하기 위해 try 블록 밖에서 선언)

        try {
            writer = new FileWriter(filePath); // 파일에 데이터를 쓸 FileWriter 객체 생성 (파일 없으면 새로 생성)
            writer.write(content); // 문자열 데이터를 파일에 쓰기
            System.out.println("파일 쓰기 완료: " + filePath);
        } catch (IOException e) {
            System.out.println("파일 쓰기 오류 발생: " + e.getMessage());
        } finally {
            try {
                if (writer != null) {
                    writer.close(); // 스트림 닫기 (반드시 finally 블록에서!)
                }
            } catch (IOException e) {
                System.out.println("스트림 닫기 오류 발생: " + e.getMessage());
            }
        }
    }
}

 

예제 코드 (파일 읽기 - FileReader):

import java.io.FileReader;
import java.io.IOException;

public class FileReaderExample {
    public static void main(String[] args) {
        String filePath = "output.txt"; // 읽어올 파일 경로

        FileReader reader = null; // FileReader 객체 선언 (finally 블록에서 사용하기 위해 try 블록 밖에서 선언)

        try {
            reader = new FileReader(filePath); // 파일에서 데이터를 읽어올 FileReader 객체 생성
            int data;
            while ((data = reader.read()) != -1) { // 파일 끝까지 문자 단위로 읽기 (read() 메소드는 int 타입으로 문자 코드 값 반환, 파일 끝에 도달하면 -1 반환)
                System.out.print((char) data); // 읽어온 문자 출력 (char 타입으로 형변환)
            }
        } catch (IOException e) {
            System.out.println("파일 읽기 오류 발생: " + e.getMessage());
        } finally {
            try {
                if (reader != null) {
                    reader.close(); // 스트림 닫기 (반드시 finally 블록에서!)
                }
            } catch (IOException e) {
                System.out.println("스트림 닫기 오류 발생: " + e.getMessage());
            }
        }
    }
}

💨 버퍼 (Buffer) 와 성능 향상: 속도 UP! 효율성 UP!

버퍼 (Buffer) 는 데이터를 임시로 저장하는 메모리 공간입니다. 마치 택배를 한꺼번에 모아서 배송하는 허브 터미널처럼, 데이터를 한 번에 모아서 처리함으로써 입출력 성능을 향상시킬 수 있습니다. 🚚📦

버퍼를 사용하는 이유 (성능 향상):

  • 입출력 횟수 감소: 데이터를 한 바이트씩 읽고 쓰는 대신, 버퍼에 데이터를 모아서 한 번에 처리하면 입출력 횟수를 줄여 성능을 향상시킬 수 있습니다. (디스크 접근 횟수 감소, 네트워크 트래픽 감소 등)
  • CPU 효율 증가: CPU가 입출력 작업에 덜 관여하게 되어 다른 작업을 더 효율적으로 처리할 수 있습니다.

버퍼 스트림 클래스:

자바에서는 버퍼 기능을 제공하는 버퍼 스트림 클래스를 제공합니다.

  • BufferedInputStream/BufferedOutputStream (바이트 버퍼 스트림): 바이트 스트림에 버퍼 기능을 추가합니다.
  • BufferedReader/BufferedWriter (문자 버퍼 스트림): 문자 스트림에 버퍼 기능을 추가합니다.

예제 코드 (버퍼 스트림 - BufferedWriter/BufferedReader):

import java.io.*;
import java.nio.Buffer;

public class BufferStreamExample {
    public static void main(String[] args) {
        String filePath = "buffered_output.txt";
        String content = "버퍼 스트림을 사용하여 파일에 씁니다.\n성능이 향상됩니다!";

        BufferedWriter bufferedWriter = null;
        BufferedReader bufferedReader = null;

        try {
            // BufferedWriter 사용 (버퍼 출력 스트림)
            bufferedWriter = new BufferedWriter(new FileWriter(filePath)); // FileWriter에 BufferedWriter 연결
            bufferedWriter.write(content);
            System.out.println("버퍼 파일 쓰기 완료: " + filePath);

            // BufferedReader 사용 (버퍼 입력 스트림)
            bufferedReader = new BufferedReader(new FileReader(filePath)); // FileReader에 BufferedReader 연결
            String line;
            System.out.println("버퍼 파일 읽기:");
            while ((line = bufferedReader.readLine()) != null) { // 한 줄씩 읽기 (readLine() 메소드 사용)
                System.out.println(line);
            }

        } catch (IOException e) {
            System.out.println("IO 오류 발생: " + e.getMessage());
        } finally {
            try {
                if (bufferedWriter != null) bufferedWriter.close(); // BufferedWriter 닫기
                if (bufferedReader != null) bufferedReader.close(); // BufferedReader 닫기
            } catch (IOException e) {
                System.out.println("스트림 닫기 오류: " + e.getMessage());
            }
        }
    }
}

🎁 직렬화 (Serialization) 와 객체 입출력: 객체를 파일에 저장하고 불러오기!

직렬화 (Serialization)객체를 바이트 스트림으로 변환하는 과정입니다. 직렬화된 객체는 파일이나 네트워크를 통해 저장하거나 전송할 수 있으며, 다시 역직렬화 (Deserialization) 과정을 통해 원래 객체로 복원할 수 있습니다. 마치 레고 블록을 분해해서 상자에 담았다가 다시 꺼내 조립하는 것과 같아요! 📦🧱

직렬화의 필요성:

  • 객체 영속화: 프로그램 종료 후에도 객체 상태를 파일에 저장하여 영구적으로 보관할 수 있습니다. (데이터베이스 대신 파일 저장)
  • 객체 네트워크 전송: 객체를 네트워크를 통해 다른 시스템으로 전송할 수 있습니다. (분산 시스템, 객체 기반 통신)
  • 객체 캐싱: 객체를 직렬화하여 캐시에 저장하고, 필요할 때 빠르게 역직렬화하여 사용할 수 있습니다. (성능 향상)

직렬화 방법:

  1. Serializable 인터페이스 구현: 직렬화하려는 클래스가 java.io.Serializable 인터페이스를 구현해야 합니다. (Serializable 인터페이스는 마커 인터페이스로, 특별한 메소드를 구현할 필요는 없습니다.) 마치 "나는 직렬화될 수 있는 객체입니다!" 라고 선언하는 것과 같아요! 🏷️
  2. ObjectOutputStream/ObjectInputStream 사용: 객체를 직렬화하고 역직렬화하기 위해 ObjectOutputStream (객체 출력 스트림) 과 ObjectInputStream (객체 입력 스트림) 을 사용합니다.

예제 코드 (객체 직렬화 및 역직렬화):

import java.io.*;

// 직렬화 대상 클래스 (Serializable 인터페이스 구현)
class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + '\'' + ", age=" + age + '}';
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        String filePath = "person.ser"; // 객체를 저장할 파일 경로
        Person person = new Person("Alice", 30); // 직렬화할 객체 생성

        // 객체 직렬화 (ObjectOutputStream 사용)
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath))) { // try-with-resources 구문 사용 (스트림 자동 close)
            oos.writeObject(person); // 객체를 파일에 직렬화하여 쓰기
            System.out.println("객체 직렬화 완료: " + filePath);
        } catch (IOException e) {
            System.out.println("객체 직렬화 오류: " + e.getMessage());
        }

        // 객체 역직렬화 (ObjectInputStream 사용)
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath))) { // try-with-resources 구문 사용 (스트림 자동 close)
            Person deserializedPerson = (Person) ois.readObject(); // 파일에서 객체 역직렬화하여 읽기 (형변환 필요)
            System.out.println("객체 역직렬화 완료: " + deserializedPerson); // 역직렬화된 객체 출력
        } catch (IOException | ClassNotFoundException e) { // Multi-catch 구문 사용 (IOException, ClassNotFoundException 동시 처리)
            System.out.println("객체 역직렬화 오류: " + e.getMessage());
        }
    }
}

🎉 예외 처리와 입출력, 이제 Robust한 프로그램을 만들어 보세요!

자, 이렇게 해서 자바의 예외 처리와 입출력 (IO) 에 대한 내용을 모두 살펴보았습니다! 예외 처리의 중요성, try-catch-finally 블록, 예외 종류, 사용자 정의 예외, 입출력 스트림, 파일 입출력, 버퍼, 직렬화까지! 이제 여러분은 더욱 튼튼하고 안정적인 자바 프로그램을 만들 수 있는 강력한 무기 🛡️ 를 장착하신 거예요!

예외 처리와 입출력은 프로그래밍의 기본적이면서도 핵심적인 부분입니다. 오늘 배운 내용을 꾸준히 연습하고 활용하여, 어떤 상황에서도 끄떡없는 Robust한 자바 프로그램을 개발해 보세요! 💪

궁금한 점이나 더 자세히 알고 싶은 내용이 있다면 언제든지 댓글로 질문해주세요! 😊  

 

 

728x90
반응형