I/O 기본

sungs·2025년 8월 2일

자바

목록 보기
54/95

I/O

InputStrem/OutputStream으로, 외부에서 데이터를 가져오거나 외부로 데이터를 보내는 것이다. 파일, 네트워크, 콘솔, 저장소 등 다양한 곳에 비슷한 메서드를 통해 데이터를 입출력할 수 있다.

스트림을 사용할 때는 byte만 받을 수 있다.

파일에서 입출력

new FileOutputStream("파일 경로 및 이름")

파일로 데이터를 내보내는 스트림이다. 파일이 없으면 자동으로 만들어 낸다. 이때 filenotfoundexception이 터질 수 있으므로 던지거나 잡아줘야 한다.

write(byte[])

바이트 단위로 데이터를 출력한다. 예를 들어 65를 쓰면 A를 파일에 보낸다. 참고로 매개변수는 byte가 아닌 int다.

new FileInputStream("파일 경로 및 이름")

파일에서 데이터를 가져오는 스트림이다. 이때도 예외 터질 수 있다.

read(byte[], offset, lentgh)

파일로부터 데이터를 가져온다.

  • byte[]: 읽어온 데이터를 저장할 대상인 바이트 배열(버퍼).
  • offset: 데이터를 저장하기 시작할 배열의 시작 인덱스(offset).
  • length: 배열의 길이.

읽을 데이터가 더 없으면 -1을 반환한다.

readAllbytes()

한 번에 다 읽어온다.

close()

외부에서 데이터를 가져오거나 출력할 때는 반드시 close() 메서드로 닫아줘야 한다. 참고로 close()를 사용하면 내부에서 flush()를 호출한다. 그리고 마지막으로 연결된 스트림에 close를 호출해야 한다. 그래야지 연쇄적으로 스트림이 닫힌다.

flush()

버퍼에 남아있는 데이터를 다 내보내서 정리한다. 버퍼는 바이트 형태로 데이터가 담겨 있는 공간이다.

메모리에서 입출력

  • ByteArrayOutputStream, ByteArrayInputStream: 각각 OutputStream, InputStream을 상속받아 사용된다. 나머지는 파일에서하고 비슷하다.

사실 메모리에서는 스트림을 잘 사용하지 않는다. 컬렉션으로 관리하는 게 더 편하기 때문이다.

콘솔에서 입출력

  • printStream(): OutputStream을 상속받았으며, System.out이 이에 속한다. write로 똑같이 데이터를 내보내고 prinln으로 출력할 수 있다.

I/O 장점

메모리, 네트워크, 저장소, 파일 등 여러가지 곳에서 데이터를 비슷한 방법으로 관리할 수 있어서 편리하다. 게다가 기존 메서드를 오버라이드하기만 하면 돼서 재사용하기도 편하다.

예제

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class SimpleByteStreamIOExample {
    private static final String FILE_NAME = "data.bin";

    public static void main(String[] args) {
        // 파일에 쓸 내용 (바이트로 변환)
        String content = "버퍼 없이 한 바이트씩 쓰고 읽는 예제입니다.";
        byte[] dataToWrite = content.getBytes();

        // --- 1. FileOutputStream을 이용해 for 루프로 파일에 한 바이트씩 쓰기 (Write) ---
        try (FileOutputStream fos = new FileOutputStream(FILE_NAME)) {
            System.out.println("for 루프를 이용해 파일에 한 바이트씩 데이터를 씁니다:");
            // dataToWrite 배열의 각 바이트를 한 바이트씩 파일에 씁니다.
            for (int i = 0; i < dataToWrite.length; i++) {
                fos.write(dataToWrite[i]);
            }

        } catch (IOException e) {
            System.err.println("파일 쓰기 중 오류가 발생했습니다: " + e.getMessage());
        }

        System.out.println("------------------------------------");

        // --- 2. FileInputStream을 이용해 while 루프로 파일에서 한 바이트씩 읽기 (Read) ---
        try (FileInputStream fis = new FileInputStream(FILE_NAME)) {
            System.out.println("파일에서 한 바이트씩 데이터를 읽습니다:");
            

            int byteRead; // read() 메소드가 읽어온 바이트를 저장할 변수
            long fileSize = 0;

            // read()를 이용해 한 바이트씩 읽고, 파일의 끝(-1)에 도달할 때까지 반복합니다.
            while ((byteRead = fis.read()) != -1) {
                fileSize++; // 읽은 바이트 수 증가
                // 읽은 바이트를 문자로 변환하여 출력합니다.
                System.out.print((char) byteRead);
            }
 
        } catch (IOException e) {
            System.err.println("파일 읽기 중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

이렇게 byte[]에다가 데이터를 담아서 사용할 수 있다. 참고로 입출력할 때 IOExepction이 터지기 때문에 잡아줘야 한다. 다만 이렇게 하면 하나씩 바이트가 입출력되므로 너무 느려진다. 짐을 여러 개 들 수 있는데 하나씩 옮기는 꼴이다. 따라서 한 번에 많은 양의 바이트를 옮기기 위해 버퍼를 사용한다.

버퍼 사용

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class SimpleByteStreamIOExample {
    // 버퍼의 크기를 상수로 정의합니다.
    private static final int BUFFER_SIZE = 512;
    private static final String FILE_NAME = "data.bin";

    public static void main(String[] args) {
        // 파일에 쓸 내용 (바이트로 변환)
        String content = "OutputStream과 InputStream 예제입니다!\n버퍼를 이용해 저장됩니다.";
        byte[] dataToWrite = content.getBytes();

        // --- 1. FileOutputStream을 이용해 버퍼로 파일에 쓰기 (Write) ---
        try (FileOutputStream fos = new FileOutputStream(FILE_NAME)) {
            System.out.println("버퍼를 직접 관리하며 파일에 데이터를 씁니다:");

            byte[] buffer = new byte[BUFFER_SIZE];
            int bufferIndex = 0;

            // dataToWrite 배열의 각 바이트를 버퍼에 담습니다.
            for (int i = 0; i < dataToWrite.length; i++) {
                buffer[bufferIndex++] = dataToWrite[i];

                // 버퍼가 가득 차면 파일에 쓰고, 인덱스를 초기화합니다.
                if (bufferIndex == BUFFER_SIZE) {
                    fos.write(buffer);
                    bufferIndex = 0;
                }
            }

            // 루프가 끝난 후, 버퍼에 남아있는 데이터가 있다면 모두 씁니다.
            if (bufferIndex > 0) {
                fos.write(buffer, 0, bufferIndex);
            }

            System.out.println("바이트 버퍼가 파일에 성공적으로 작성되었습니다.");
        } catch (IOException e) {
            System.err.println("파일 쓰기 중 오류가 발생했습니다: " + e.getMessage());
        }

        System.out.println("------------------------------------");

        // --- 2. FileInputStream을 이용해 버퍼로 파일에서 읽기 (Read) ---
        try (FileInputStream fis = new FileInputStream(FILE_NAME)) {
            System.out.println("파일에서 버퍼를 이용해 데이터를 읽습니다:");
            
            byte[] buffer = new byte[BUFFER_SIZE];
            long fileSize = 0;
            int bytesRead; // read() 메소드가 읽어온 바이트 수를 저장할 변수

            // read(byte[] buffer)를 이용해 버퍼에 데이터를 읽어오고, 읽어온 바이트 수를 fileSize에 누적합니다.
            while ((bytesRead = fis.read(buffer)) != -1) {
                fileSize += bytesRead;
            }

        } catch (IOException e) {
            System.err.println("파일 읽기 중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

이렇게 버퍼를 사용해서 데이터를 입출력할 수 있다.

원리는 버퍼에 데이터를 담아서 나른다고 생각하면 편하다. 버퍼가 가득차면 보내고 비워낸 다음에 다시 받고.

출력할 때는 버퍼의 크기에 따라 출력 속도가 달라진다. 다만 디스크가 데이터를 읽는 게 4바이트 혹은 8바이트미으로 8바이트 이상을 넘어가면 더욱 빨라지지는 않는다. 이렇게 하면 성능은 훨씬 좋아진다. 하지만 코드가 길어진다는 단점이 존재한다. 그래서 Buffered라는 걸 사용하기도 한다.

Buffered 사용

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class SimpleByteStreamIOExample {
    private static final String FILE_NAME = "data.bin";
    private static final int BUFFER_SIZE = 1024; // 버퍼의 크기를 1KB로 설정

    public static void main(String[] args) {
        // 파일에 쓸 내용 (바이트로 변환)
        String content = "BufferedOutputStream과 BufferedInputStream 예제입니다!\n이 예제는 버퍼 스트림을 사용해 더 효율적입니다.";
        byte[] dataToWrite = content.getBytes();

        // --- 1. BufferedOutputStream을 이용해 파일에 쓰기 (Write) ---
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(FILE_NAME))) {
            System.out.println("버퍼 스트림을 이용해 파일에 데이터를 씁니다:");

            // BufferedOutputStream은 내부 버퍼에 데이터를 채운 후, 한 번에 파일에 씁니다.
            bos.write(dataToWrite);

            System.out.println("바이트 데이터가 파일에 성공적으로 작성되었습니다.");
            System.out.println("File created: " + FILE_NAME);
            System.out.println("File size: " + dataToWrite.length + " bytes");
        } catch (IOException e) {
            System.err.println("파일 쓰기 중 오류가 발생했습니다: " + e.getMessage());
        }

        System.out.println("------------------------------------");

        // --- 2. BufferedInputStream을 이용해 파일에서 읽기 (Read) ---
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(FILE_NAME))) {
            System.out.println("버퍼 스트림을 이용해 파일에서 데이터를 읽습니다:");
            
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead; // read() 메소드가 읽어온 바이트를 저장할 변수
            long fileSize = 0;

            // read(byte[] buffer)를 이용해 버퍼에 데이터를 읽어오고, 읽어온 바이트 수를 fileSize에 누적합니다.
            while ((bytesRead = bis.read(buffer)) != -1) {
                fileSize += bytesRead;
            }

            System.out.println("File name: " + FILE_NAME);
            System.out.println("File size: " + fileSize + " bytes");
        } catch (IOException e) {
            System.err.println("파일 읽기 중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

Buffered 스트림을 사용하면 코드가 좀 더 가벼워진다. 그러나 Buffered 스트림에는 동기화가 걸려있기 때문에 직접 버퍼를 다뤘을 때보다는 성능이 저하된다.

참고로 InputStream, OutputStream은 메인 스트림이고, Buffered 스트림은 보조 스트림이다. 따라서 혼자서 쓰일 수는 없다. 반드시 메인 스트림을 받아줘야 한다.

한 번에 다 입출력

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class SimpleByteStreamIOExample {
    private static final String FILE_NAME = "data.bin";

    public static void main(String[] args) {
        // 파일에 쓸 내용 (바이트로 변환)
        String content = "for 루프를 사용해 버퍼에 담은 후, 한 번에 입출력하는 예제입니다.";
        byte[] dataToWrite = content.getBytes();

        // --- 1. FileOutputStream을 이용해 버퍼를 채워 한 번에 쓰기 (Write) ---
        try (FileOutputStream fos = new FileOutputStream(FILE_NAME)) {
            System.out.println("for 루프를 이용해 버퍼를 채운 뒤 한 번에 데이터를 씁니다:");

            // dataToWrite 배열과 동일한 크기의 버퍼 생성
            byte[] buffer = new byte[dataToWrite.length];
            
            // for 루프를 사용해 데이터를 버퍼에 저장
            for (int i = 0; i < dataToWrite.length; i++) {
                buffer[i] = dataToWrite[i];
            }
            
            // 버퍼에 담긴 전체 데이터를 한 번에 파일에 씁니다.
            fos.write(buffer);

            System.out.println("바이트 데이터가 파일에 성공적으로 작성되었습니다.");
            System.out.println("File created: " + FILE_NAME);
            System.out.println("File size: " + dataToWrite.length + " bytes");
        } catch (IOException e) {
            System.err.println("파일 쓰기 중 오류가 발생했습니다: " + e.getMessage());
        }

        System.out.println("------------------------------------");

        // --- 2. FileInputStream을 이용해 한 번에 파일에서 읽기 (Read) ---
        try (FileInputStream fis = new FileInputStream(FILE_NAME)) {
            System.out.println("파일의 모든 내용을 한 번에 읽어옵니다:");
            
            // Java 9 이상에서 사용 가능한 readAllBytes()를 사용해 파일의 전체 내용을 한 번에 읽어옵니다.
            byte[] readBytes = fis.readAllBytes();

            System.out.println("File name: " + FILE_NAME);
            System.out.println("File size: " + readBytes.length + " bytes");
            
            // 읽어온 바이트 배열을 다시 문자열로 변환하여 출력합니다.
            String readContent = new String(readBytes);
            System.out.println("\n읽어온 내용: \n" + readContent);
        } catch (IOException e) {
            System.err.println("파일 읽기 중 오류가 발생했습니다: " + e.getMessage());
        }
    }
}

버퍼를 사용해서 한번에 입출력할 수도 있다. 이러면 성능이 좋아진다. 단점은 큰 파일을 한번에 가져올 경우 자원이 부족한 현상이 발생할 수도 있다.

정리

  • 파일 크기가 작고 성능이 중요할 경우 한 번에 입출력하면 된다. 물론 파일에서 조금씩 가져와서 처리해야할 경우에는 버퍼를 쓰거나 다른 방법을 써야한다.
  • 성능이 중요하고 큰 파일을 처리해야 한다면 직접 버퍼를 다루어 사용한다.
  • 성능이 중요하지 않고 버퍼 기능을 사용해야 한다면 Buffered 스트림을 사용한다.
profile
앱 개발 공부 중

0개의 댓글