Java I/O

mskimdev·2026년 3월 16일

Java Library

목록 보기
3/4

Java I/O

파일을 읽고 쓰거나, 네트워크로 데이터를 주고받을 때 I/O가 등장한다. I/O는 Input/Output의 약자로, 데이터가 프로그램 안으로 들어오고(Input) 나가는(Output) 것을 말한다. 처음엔 Stream이니 Reader니 하는 클래스가 너무 많아서 어디서부터 시작해야 할지 막막한데, 구조를 이해하면 그렇게 복잡하지 않다.


스트림(Stream)이란?

Java I/O는 스트림(Stream) 을 기반으로 동작한다.

스트림은 데이터가 흐르는 통로다. 수도관을 생각하면 이해하기 쉽다. 수원지(파일, 네트워크)에서 물(데이터)이 파이프(스트림)를 통해 흘러 목적지(프로그램)에 도달하는 것과 같다.

스트림은 단방향이다. 읽기 전용 스트림과 쓰기 전용 스트림이 따로 존재한다.


바이트 스트림 vs 문자 스트림

Java I/O는 크게 두 종류로 나뉜다.

바이트 스트림 (Byte Stream)

데이터를 바이트(byte) 단위로 처리한다. 이미지, 동영상, 실행 파일처럼 모든 종류의 파일을 다룰 수 있다.

  • 입력: InputStream (최상위 추상 클래스)
  • 출력: OutputStream (최상위 추상 클래스)
  • 대표 구현체: FileInputStream, FileOutputStream

문자 스트림 (Character Stream)

데이터를 문자(char) 단위로 처리한다. 텍스트 파일을 읽고 쓸 때 사용한다. 내부적으로 바이트를 문자로 변환해주기 때문에 한글 같은 다국어 처리에 적합하다.

  • 입력: Reader (최상위 추상 클래스)
  • 출력: Writer (최상위 추상 클래스)
  • 대표 구현체: FileReader, FileWriter

파일 읽기 / 쓰기

텍스트 파일 읽기 — BufferedReader

FileReader만 써도 읽을 수 있지만, BufferedReader로 감싸면 버퍼를 사용해 성능이 훨씬 좋아진다. 한 줄씩 읽을 수 있는 readLine()도 제공한다.

// try-with-resources: 블록이 끝나면 자동으로 close() 호출
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

텍스트 파일 쓰기 — BufferedWriter

try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("첫 번째 줄");
    bw.newLine(); // 줄바꿈
    bw.write("두 번째 줄");
} catch (IOException e) {
    e.printStackTrace();
}

FileWriter("output.txt", true) 처럼 두 번째 인자로 true를 주면 기존 파일에 이어서 쓴다(append). 기본값은 덮어쓰기다.


try-with-resources

I/O 작업 후에는 반드시 스트림을 닫아야(close) 한다. 닫지 않으면 메모리 누수나 파일 잠금 문제가 생긴다.

Java 7부터는 try-with-resources 문법으로 자동으로 닫아준다.

// 기존 방식 — finally에서 직접 닫아야 했다
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("test.txt"));
    // 파일 읽기
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { e.printStackTrace(); }
    }
}

// try-with-resources — 훨씬 간결하다
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    // 파일 읽기
} catch (IOException e) {
    e.printStackTrace();
}

AutoCloseable 인터페이스를 구현한 클래스라면 모두 try-with-resources에서 쓸 수 있다.


Java NIO — 더 빠른 I/O

Java 7부터 java.nio 패키지가 강화되었다. 기존 I/O보다 성능이 좋고, 코드도 간결하다.

import java.nio.file.*;

// 파일 전체 읽기 (작은 파일에 적합)
String content = Files.readString(Path.of("test.txt"));
System.out.println(content);

// 파일 전체 쓰기
Files.writeString(Path.of("output.txt"), "저장할 내용");

// 줄 단위로 읽기
List<String> lines = Files.readAllLines(Path.of("test.txt"));
lines.forEach(System.out::println);

파일 하나를 간단히 읽고 쓰는 상황이라면 Files 유틸리티 클래스가 훨씬 편하다.


직렬화 (Serialization)

객체를 파일에 저장하거나 네트워크로 전송하려면 객체를 바이트로 변환해야 한다. 이 과정을 직렬화(Serialization) 라고 하고, 반대로 바이트를 다시 객체로 복원하는 것을 역직렬화(Deserialization) 라고 한다.

// 직렬화하려면 Serializable 인터페이스를 구현해야 한다
public class User implements Serializable {
    private String name;
    private int age;
    // ...
}

// 객체 저장 (직렬화)
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
    oos.writeObject(new User("김민수", 25));
}

// 객체 불러오기 (역직렬화)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
    User user = (User) ois.readObject();
    System.out.println(user.getName()); // 김민수
}

마무리

Java I/O는 클래스 이름이 많아서 처음엔 복잡해 보이지만, 구조 자체는 단순하다.

  • 텍스트 파일 → BufferedReader / BufferedWriter
  • 바이너리 파일 → FileInputStream / FileOutputStream
  • 간단한 파일 작업 → Files 유틸리티 클래스 (NIO)

이 세 가지 케이스만 기억하고, 나머지는 필요할 때 찾아보면 된다.

profile
<- 개발 공부하는 나

0개의 댓글