Chapter 15. 입출력 (I/O)

Chapter 15. 입출력 (I/O)

입출력 (I/O)

자바의 입출력 스트림 구조, 바이트/문자 기반 스트림, 보조 스트림, 표준 입출력, File 클래스, 직렬화를 다룬다.


1. 자바에서의 입출력

1.1 입출력이란?

I/O(Input/Output)란 컴퓨터 내부 또는 외부 장치와 프로그램 간에 데이터를 주고받는 것을 말한다.

1.2 스트림 (Stream)

스트림이란 데이터를 운반하는 데 사용되는 연결 통로이다.

┌──────────┐    InputStream     ┌──────────┐
│          │ ◀───────────────── │          │
│  프로그램  │                    │ 입출력 대상 │
│          │ ──────────────────▶│          │
└──────────┘    OutputStream    └──────────┘
특성설명
단방향하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다
FIFO먼저 보낸 데이터를 먼저 받는다 (큐와 유사)
연속적중간에 건너뜀 없이 연속적으로 데이터를 주고받는다

입출력을 동시에 수행하려면 입력 스트림 + 출력 스트림, 총 2개의 스트림이 필요하다.

1.3 스트림의 분류

                        스트림
             ┌──────────┴──────────┐
         바이트 기반                문자 기반
       (1 byte 단위)           (2 byte 단위)
      ┌──────┴──────┐       ┌──────┴──────┐
  InputStream  OutputStream  Reader     Writer

2. 바이트 기반 스트림

2.1 InputStream과 OutputStream

모든 바이트 기반 스트림의 조상 클래스이다.

입력 스트림출력 스트림입출력 대상
FileInputStreamFileOutputStream파일
ByteArrayInputStreamByteArrayOutputStream메모리 (byte 배열)
PipedInputStreamPipedOutputStream프로세스 간 통신
AudioInputStreamAudioOutputStream오디오 장치

InputStream의 주요 메서드

메서드설명
abstract int read()1 byte를 읽어서 반환. 읽을 데이터가 없으면 -1 반환
int read(byte[] b)배열 b의 크기만큼 읽어서 배열에 저장
int read(byte[] b, int off, int len)최대 len개의 byte를 읽어서 b의 off 위치부터 저장
void close()스트림을 닫고 자원을 반환
int available()블로킹 없이 읽어올 수 있는 바이트 수 반환
long skip(long n)n만큼 건너뛴다
void mark(int readlimit)현재 위치를 표시
void reset()mark()가 호출된 위치로 되돌린다

OutputStream의 주요 메서드

메서드설명
abstract void write(int b)1 byte를 출력
void write(byte[] b)배열 b의 모든 내용을 출력
void write(byte[] b, int off, int len)b의 off부터 len개만큼 출력
void flush()버퍼의 내용을 출력소스에 쓴다
void close()스트림을 닫고 자원을 반환
// read()의 내부 구조 — 추상 메서드를 활용한 템플릿 메서드 패턴
public abstract class InputStream {
    abstract int read();  // 자손 클래스가 구현

    int read(byte[] b, int off, int len) {
        for (int i = off; i < off + len; i++) {
            b[i] = (byte) read();  // 추상 메서드 호출
        }
    }

    int read(byte[] b) {
        return read(b, 0, b.length);  // 위 메서드 재활용
    }
}
스트림은 반드시 닫아야 한다. 사용이 끝난 스트림은 close()로 자원을 반환해야 한다. JVM이 자동으로 닫아주기는 하지만 이에 의존해서는 안 된다. ByteArrayInputStream이나 System.in, System.out처럼 메모리를 사용하는 스트림은 예외적으로 닫지 않아도 된다.

2.2 FileInputStream과 FileOutputStream

파일 입출력에 가장 많이 사용되는 스트림이다.

// 파일 읽기
try (FileInputStream fis = new FileInputStream("input.txt")) {
    int data;
    while ((data = fis.read()) != -1) {
        System.out.print((char) data);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// 파일 복사
try (FileInputStream fis = new FileInputStream("source.txt");
     FileOutputStream fos = new FileOutputStream("copy.txt")) {

    int data;
    while ((data = fis.read()) != -1) {
        fos.write(data);
    }
} catch (IOException e) {
    e.printStackTrace();
}

FileOutputStream 생성자

생성자설명
FileOutputStream(String name)파일에 연결된 출력 스트림 생성 (기존 내용 덮어쓰기)
FileOutputStream(String name, boolean append)append가 true면 기존 내용 뒤에 이어쓰기
FileOutputStream(File file)File 인스턴스로 지정
// 파일에 이어쓰기
try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
    String msg = "새로운 로그 추가\n";
    fos.write(msg.getBytes());
}

2.3 ByteArrayInputStream과 ByteArrayOutputStream

메모리(byte 배열)에 데이터를 입출력한다. 다른 곳에 입출력하기 전에 데이터를 임시로 변환할 때 사용한다. 메모리만 사용하므로 GC가 자동으로 자원을 반환하여 close()가 필수가 아니다.

byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outSrc = null;

ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
ByteArrayOutputStream output = new ByteArrayOutputStream();

int data;
while ((data = input.read()) != -1) {
    output.write(data);
}

outSrc = output.toByteArray();
System.out.println(Arrays.toString(outSrc));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
블로킹(Blocking)이란? 데이터를 읽어올 때 데이터가 도착할 때까지 멈춰 있는 것을 말한다. available()은 블로킹 없이 읽어올 수 있는 바이트 수를 반환하므로, 효율적인 배열 크기 설정에 활용할 수 있다.

3. 바이트 기반 보조 스트림

보조 스트림은 실제 데이터를 주고받는 기능은 없고, 기반 스트림의 기능을 향상시키거나 새로운 기능을 추가한다. 단독으로 사용할 수 없으며 기반 스트림을 먼저 생성한 후 이를 감싸서 사용한다.

// 기반 스트림 생성
FileInputStream fis = new FileInputStream("file.txt");
// 보조 스트림으로 감싸기
BufferedInputStream bis = new BufferedInputStream(fis);
// 보조 스트림을 통해 읽기
bis.read();

3.1 FilterInputStream과 FilterOutputStream

모든 보조 스트림의 조상 클래스이다. 자체적으로는 아무 일도 하지 않으며, 상속을 통해 오버라이딩하여 사용한다.

FilterInputStream의 자손:
├── BufferedInputStream
├── DataInputStream
└── PushbackInputStream

FilterOutputStream의 자손:
├── BufferedOutputStream
├── DataOutputStream
└── PrintStream

3.2 BufferedInputStream과 BufferedOutputStream

버퍼를 이용하여 입출력 효율을 높인다. 한 바이트씩 읽고 쓰는 대신 버퍼에 모아서 한 번에 처리한다.

[프로그램] ←→ [BufferedInputStream 버퍼(8KB)] ←→ [파일]

1. 프로그램이 read() 호출
2. 버퍼가 비어있으면 파일에서 8KB를 한 번에 읽어 버퍼에 저장
3. 버퍼에서 1 byte를 반환
4. 버퍼가 비면 다시 파일에서 8KB 읽기 반복
// 버퍼 있는 스트림 vs 없는 스트림 — 성능 차이
// 버퍼 없이 파일 복사 (느림)
try (FileInputStream fis = new FileInputStream("large.dat");
     FileOutputStream fos = new FileOutputStream("copy.dat")) {
    int b;
    while ((b = fis.read()) != -1) fos.write(b);
}

// 버퍼로 감싸서 파일 복사 (빠름)
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.dat"));
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.dat"))) {
    int b;
    while ((b = bis.read()) != -1) bos.write(b);
}
항목기본 버퍼 크기설명
BufferedInputStream8192 byte (8KB)입력 시 버퍼 크기만큼 미리 읽어옴
BufferedOutputStream8192 byte (8KB)버퍼가 가득 차면 한 번에 출력
BufferedOutputStream 주의사항: 버퍼가 가득 찰 때만 출력하므로, 프로그램 종료 전 마지막 데이터가 버퍼에 남아있을 수 있다. 반드시 close() 또는 flush()를 호출하여 버퍼의 잔여 내용을 출력해야 한다.

3.3 DataInputStream과 DataOutputStream

기본 자료형 단위로 데이터를 읽고 쓸 수 있다. byte 단위가 아닌 int, float, String 등의 단위로 처리한다.

// 기본 자료형 단위로 파일에 쓰기
try (DataOutputStream dos = new DataOutputStream(
        new FileOutputStream("data.bin"))) {
    dos.writeInt(100);
    dos.writeFloat(3.14f);
    dos.writeBoolean(true);
    dos.writeUTF("Hello");
}

// 기본 자료형 단위로 파일에서 읽기 (쓴 순서대로 읽어야 함)
try (DataInputStream dis = new DataInputStream(
        new FileInputStream("data.bin"))) {
    int i = dis.readInt();         // 100
    float f = dis.readFloat();     // 3.14
    boolean b = dis.readBoolean(); // true
    String s = dis.readUTF();      // "Hello"
}
읽는 순서가 중요하다. DataOutputStream으로 쓴 순서와 동일한 순서, 동일한 자료형으로 읽어야 한다. 순서가 맞지 않으면 잘못된 값을 읽게 된다. 더 이상 읽을 데이터가 없으면 EOFException이 발생한다.

3.4 PrintStream

print, println, printf 메서드를 제공한다. System.out이 바로 PrintStream이다.

printf 주요 포맷

포맷설명예시 (int i = 65)
%d10진 정수65
%o8진 정수101
%x16진 정수41
%c문자A
%s문자열65
%f실수65.000000
%e지수 표현6.500000e+01
%5d5자리, 빈자리 공백65
%-5d5자리, 왼쪽 정렬65
%05d5자리, 빈자리 000065
%8.2f전체 8자리, 소수 2자리65.00
int age = 25;
String name = "홍길동";
double score = 95.7;

System.out.printf("이름: %s, 나이: %d, 점수: %.1f%n", name, age, score);
// 이름: 홍길동, 나이: 25, 점수: 95.7

4. 문자 기반 스트림

Java의 char는 2 byte이므로 바이트 기반 스트림으로 문자를 처리하기 어렵다. 문자 기반 스트림은 인코딩 변환도 자동으로 처리한다.

4.1 바이트 기반 → 문자 기반 대응

바이트 기반문자 기반
FileInputStream / FileOutputStreamFileReader / FileWriter
ByteArrayInputStream / ByteArrayOutputStreamCharArrayReader / CharArrayWriter
PipedInputStream / PipedOutputStreamPipedReader / PipedWriter
StringBufferInputStream (deprecated)StringReader / StringWriter

보조 스트림 대응

바이트 기반 보조문자 기반 보조
BufferedInputStream / BufferedOutputStreamBufferedReader / BufferedWriter
FilterInputStream / FilterOutputStreamFilterReader / FilterWriter
PrintStreamPrintWriter
PushbackInputStreamPushbackReader

4.2 Reader와 Writer

문자 기반 스트림의 조상 클래스이다. byte[] 대신 char[]를 사용한다.

Reader 메서드Writer 메서드
int read()void write(int c)
int read(char[] c)void write(char[] c)
int read(char[] c, int off, int len)void write(char[] c, int off, int len)
void close()void write(String str)
void flush() / void close()
인코딩 자동 처리: Reader는 특정 인코딩을 읽어서 유니코드(UTF-16)로 변환하고, Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.

4.3 FileReader와 FileWriter

텍스트 파일을 읽고 쓸 때 사용한다.

// 텍스트 파일 읽기
try (FileReader fr = new FileReader("input.txt")) {
    int ch;
    while ((ch = fr.read()) != -1) {
        System.out.print((char) ch);
    }
}

// 텍스트 파일 쓰기
try (FileWriter fw = new FileWriter("output.txt")) {
    fw.write("안녕하세요\n");
    fw.write("Java I/O 학습 중입니다.");
}

4.4 PipedReader와 PipedWriter

쓰레드 간 데이터 통신에 사용된다. 입력과 출력 스트림을 connect()로 연결한다. 한쪽 스트림만 닫아도 나머지가 자동으로 닫힌다.

4.5 StringReader와 StringWriter

입출력 대상이 메모리(문자열)인 스트림이다. StringWriter는 내부에 StringBuffer를 가지고 있다.

StringWriter sw = new StringWriter();
sw.write("Hello ");
sw.write("World");

String result = sw.toString();          // "Hello World"
StringBuffer buf = sw.getBuffer();      // StringBuffer 직접 접근

5. 문자 기반 보조 스트림

5.1 BufferedReader와 BufferedWriter

버퍼를 이용하여 문자 기반 입출력의 효율을 높인다. BufferedReaderreadLine()으로 라인 단위 읽기가 가능하다.

// 파일을 라인 단위로 읽기
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// 라인 단위로 쓰기
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("첫 번째 줄");
    bw.newLine();  // 줄바꿈
    bw.write("두 번째 줄");
}

5.2 InputStreamReader와 OutputStreamWriter

바이트 기반 스트림을 문자 기반 스트림으로 연결하는 다리 역할을 한다. 인코딩을 지정할 수 있다.

// System.in(바이트 스트림)을 문자 스트림으로 변환하여 라인 단위 입력
BufferedReader br = new BufferedReader(
    new InputStreamReader(System.in));
String input = br.readLine();

// 인코딩을 지정하여 파일 읽기
InputStreamReader isr = new InputStreamReader(
    new FileInputStream("data.txt"), "UTF-8");

// 현재 인코딩 확인
System.out.println(isr.getEncoding());  // UTF-8

6. 표준 입출력과 File

6.1 표준 입출력

자바 어플리케이션 실행 시 자동으로 생성되는 3가지 스트림이다.

스트림타입용도
System.inInputStream콘솔로부터 데이터 입력
System.outPrintStream콘솔로 데이터 출력
System.errPrintStream콘솔로 에러 출력

실제 내부적으로는 BufferedInputStreamBufferedOutputStream의 인스턴스를 사용한다.

표준 입출력 대상 변경

메서드설명
System.setIn(InputStream in)입력 대상 변경
System.setOut(PrintStream out)출력 대상 변경
System.setErr(PrintStream err)에러 출력 대상 변경
// 표준 출력을 파일로 변경
PrintStream ps = new PrintStream(new FileOutputStream("output.log"));
System.setOut(ps);

System.out.println("이 내용은 파일에 출력됩니다");

6.2 RandomAccessFile

자바에서 유일하게 하나의 클래스로 읽기/쓰기가 모두 가능한 스트림이다. DataInputDataOutput 인터페이스를 구현했기 때문에 기본 자료형 단위로 읽고 쓸 수 있다.

가장 큰 특징은 파일 포인터를 이용하여 파일의 어느 위치에서든 읽기/쓰기가 가능하다는 것이다.

try (RandomAccessFile raf = new RandomAccessFile("data.dat", "rw")) {
    // 쓰기
    raf.writeInt(100);
    raf.writeUTF("Hello");

    // 파일 포인터 확인
    System.out.println("현재 위치: " + raf.getFilePointer());

    // 파일 포인터를 처음으로 이동
    raf.seek(0);

    // 읽기
    int num = raf.readInt();
    String str = raf.readUTF();
}
메서드설명
long getFilePointer()현재 파일 포인터 위치 반환
void seek(long pos)파일 포인터를 지정 위치로 이동
int skipBytes(int n)현재 위치에서 n만큼 건너뛰기

6.3 File 클래스

파일과 디렉터리를 다루기 위한 클래스이다. File 인스턴스를 생성해도 실제 파일이 생성되지는 않는다.

// File 인스턴스 생성 (파일이 생성되지는 않음)
File f = new File("C:\\data\\test.txt");

// 새 파일 실제 생성
f.createNewFile();

// 디렉터리 생성
File dir = new File("C:\\data\\newDir");
dir.mkdir();       // 단일 디렉터리 생성
dir.mkdirs();      // 중간 경로까지 모두 생성

경로 관련 메서드

메서드설명
String getName()파일 이름 반환
String getPath()경로 반환
String getAbsolutePath()절대 경로 반환
String getCanonicalPath()정규 경로 반환 (기호, 링크 제거)
String getParent()부모 디렉터리 반환
File f = new File("./src/../data/test.txt");

System.out.println(f.getPath());            // .\src\..\data\test.txt
System.out.println(f.getAbsolutePath());    // C:\project\.\src\..\data\test.txt
System.out.println(f.getCanonicalPath());   // C:\project\data\test.txt

경로 구분자

멤버 변수WindowsUnix/Linux
File.separator\/
File.pathSeparator;:

절대 경로 vs 정규 경로:

  • 절대 경로(Absolute Path): 루트부터 시작하는 전체 경로. .이나 .. 같은 기호가 포함될 수 있어 하나의 파일에 여러 절대 경로가 존재할 수 있다.
  • 정규 경로(Canonical Path): 기호나 링크를 포함하지 않는 유일한 경로.

File의 유용한 메서드

File f = new File("test.txt");

// 파일 정보
f.exists();        // 존재 여부
f.isFile();        // 파일인지
f.isDirectory();   // 디렉터리인지
f.canRead();       // 읽기 가능 여부
f.canWrite();      // 쓰기 가능 여부
f.length();        // 파일 크기 (byte)
f.lastModified();  // 마지막 수정 시간

// 디렉터리 내 파일 목록
File dir = new File("C:\\data");
String[] fileList = dir.list();       // 파일명 배열
File[] files = dir.listFiles();       // File 객체 배열

7. 직렬화 (Serialization)

7.1 직렬화란?

직렬화(Serialization): 객체를 연속적인 데이터(바이트 스트림)로 변환하는 것 역직렬화(Deserialization): 바이트 스트림을 다시 객체로 복원하는 것

        직렬화                          역직렬화
객체 ──────────▶ 바이트 스트림 ──────────▶ 객체
  (인스턴스변수 값)    (파일/네트워크 전송)    (원래 객체 복원)

객체를 저장한다는 것은 모든 인스턴스 변수의 값을 저장하는 것과 같다. 메서드나 클래스 변수는 직렬화 대상이 아니다.

7.2 ObjectInputStream과 ObjectOutputStream

// 직렬화 — 객체를 파일에 저장
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("user.ser"))) {
    oos.writeObject(new UserInfo("홍길동", 25));
}

// 역직렬화 — 파일에서 객체를 읽기
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("user.ser"))) {
    UserInfo info = (UserInfo) ois.readObject();
    System.out.println(info.getName());  // "홍길동"
}

7.3 Serializable 인터페이스

직렬화가 가능한 클래스를 만들려면 java.io.Serializable 인터페이스를 구현해야 한다.

public class UserInfo implements Serializable {
    private String name;
    private int age;
    private transient String password;  // 직렬화 제외

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

    // getter/setter...
}

직렬화 규칙

규칙설명
Serializable 구현직렬화하려면 반드시 구현해야 함
상속조상이 Serializable이면 자손도 직렬화 가능
조상이 미구현조상의 인스턴스 변수는 직렬화에서 제외
transient이 제어자가 붙은 변수는 직렬화 대상에서 제외
static 변수직렬화 대상이 아님 (클래스 변수이므로)
// 상속과 직렬화
class Parent {  // Serializable 미구현
    int parentValue = 100;
}

class Child extends Parent implements Serializable {
    int childValue = 200;
}

// Child를 직렬화하면 childValue(200)만 저장된다
// parentValue(100) 직렬화되지 않는다
transient의 활용: 비밀번호, 세션 토큰 등 보안에 민감한 데이터나, 직렬화가 불필요한 캐시 데이터에 transient를 사용한다. 역직렬화 시 해당 필드는 기본값(null, 0 등)으로 초기화된다.

7.4 serialVersionUID

직렬화된 객체를 역직렬화할 때, 클래스의 버전이 일치해야 한다. 버전이 다르면 InvalidClassException이 발생한다.

// 클래스 내에 serialVersionUID를 직접 정의하여 버전 관리
class MyData implements Serializable {
    static final long serialVersionUID = 1L;

    int value;
    String name;
    // 필드가 추가되어도 serialVersionUID가 같으면 역직렬화 가능
}
항목자동 생성수동 정의
생성 방식클래스 멤버 정보 기반 자동 생성개발자가 직접 값 지정
클래스 변경 시버전이 자동 변경 → 역직렬화 실패버전 유지 가능
권장 여부비권장권장
serialVersionUID는 반드시 직접 정의하라. 자동 생성에 의존하면 클래스에 static 변수 하나만 추가해도 버전이 바뀌어 기존에 직렬화된 데이터를 읽을 수 없게 된다. transient, static 변수 추가처럼 직렬화에 영향을 주지 않는 변경의 경우에도 버전이 달라지는 문제가 있다.

8. 요약

스트림 체계 한눈에 보기

                        스트림
           ┌─────────────┴─────────────┐
       바이트 기반                    문자 기반
     ┌─────┴─────┐             ┌─────┴─────┐
 InputStream  OutputStream   Reader      Writer
     │              │           │           │
  ┌──┴──┐       ┌──┴──┐    ┌──┴──┐    ┌──┴──┐
 File  ByteArr File  ByteArr File CharArr File CharArr
 Piped         Piped        Piped       Piped
                            String      String

           보조 스트림 (기반 스트림을 감싸서 기능 추가)
     ┌─────────────────────────────────────────┐
     │  Buffered — 버퍼를 이용한 성능 향상        │
     │  Data — 기본 자료형 단위 읽기/쓰기         │
     │  Object — 객체 직렬화/역직렬화             │
     │  Print — 다양한 출력 형식                  │
     │  InputStreamReader — 바이트→문자 변환       │
     └─────────────────────────────────────────┘

핵심 개념 정리

개념설명
스트림데이터를 운반하는 단방향 연결 통로
바이트 스트림1 byte 단위. InputStream / OutputStream
문자 스트림2 byte(char) 단위. Reader / Writer
보조 스트림기반 스트림을 감싸서 기능 향상 (Buffered, Data 등)
표준 입출력System.in / System.out / System.err
RandomAccessFile파일 포인터로 임의 위치 읽기/쓰기
File파일/디렉터리 정보 조회 및 관리
직렬화객체 → 바이트 스트림. Serializable 구현 필요
transient직렬화에서 제외할 필드에 사용
serialVersionUID직렬화 클래스의 버전 관리. 직접 정의 권장

스트림 선택 가이드

바이너리 데이터 (이미지, 동영상 등)
  └→ FileInputStream / FileOutputStream + Buffered

텍스트 데이터 (txt, csv, log 등)
  └→ FileReader / FileWriter + BufferedReader / BufferedWriter

콘솔 입력
  └→ BufferedReader + InputStreamReader(System.in)
  └→ 또는 Scanner (JDK 1.5+)

객체 저장/전송
  └→ ObjectOutputStream / ObjectInputStream

기본 자료형 저장
  └→ DataOutputStream / DataInputStream

파일 임의 접근
  └→ RandomAccessFile