JAVA

자바 - IO 기본 (InputStream, OutputStream)

경딩 2025. 1. 28. 16:28

자바에서 데이터를 파일이나 네트워크 등을 통해 주고 받을 때 어떤식으로 통신하는지 알아보자

 

 

데이터의 통신 과정은 스트림으로 통신한다

 

 

자바가 가진 데이터를 hello.dat 라는  파일에 저장하려면 어떻게 해야할까?

자바 프로세스가 가지고 있는 데이터를 밖으로 보내려면 출력 스트림을 사용하면 되고, 반대로 외부 데이터를 자바 프로세스 안으로 가져오려면 입력 스트림을 사용하면 된다. 참고로 스프트림을 단방향으로 흐른다.

 

스트림 시작 

스트림 시작 - 예제1

package io.start;

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

public class StreamStartMain1_1 {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        fos.write(65);
        fos.write(66);
        fos.write(67);
        fos.close();

    }
}

 

실행 후 파일 확인

우리가 사용하는 개발 툴은 UTF-8 또는 MS949 문자 집합을 사용해서 byte 단위의 데이터를 문자로 디코딩해서 보여준다.

따라서 65, 66, 67 byte를 ASCII 문자인 A, B, C로 인식해서 출력한 것이다

package io.start;

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

public class StreamStartMain1_1 {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        fos.write(65);
        fos.write(66);
        fos.write(67);
        fos.close();

        FileInputStream fis = new FileInputStream("temp/hello.dat");
        System.out.println(fis.read()); // read() 한 바이트씩 읽어드림
        System.out.println(fis.read());
        System.out.println(fis.read());
        System.out.println(fis.read());
        fis.close();
    }
}
  • 실행결과

-1 은 파일의 끝을 의미한다.

 

new FileOutputStream("temp/hello.dat")

  • 파일에 데이터를 출력하는 스트림이다.
  • 파일이 없으면 파일을 자동으로 만들고, 데이터를 해당 파일에 저장한다
  • 폴더를 만들지는 않기 때문에 폴더는 미리 만들어두어야 한다

 

wirte()

  • byte 단위로 값을 출력한다. 여기서 65, 66, 67 을  출력했다.
  • 참고로 ASCII 코드 집합에서 65은 A, 66은 B, 67는 C이다.

new FileInputStream("temp/hello.dat")

  • 파일에서 데이터를 읽어오는 스트림이다.

 

read()

  • 파일에서 데이터를 byte 단위로 하나씩 읽어온다
  • 순서대로 65,66,67을 읽어온다.
  • 파일의 끝에 도달해서 더는 읽을 내용이 없다면 -1 을 반환한다.
    • 파일의 끝(EOF, End of File)

close()

  • 파일에 접근하는 것은 자바 입장에서 외부 자원을 사용하는 것이다. 자바에서 내부 객체는  자동으로 GC 가 되지만 외부 자원은 사용후 반드시 닫아주어야한다.

 

스트림 시작 - 예제2

package io.start;

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

public class StreamStartMain1_1 {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        fos.write(65);
        fos.write(66);
        fos.write(67);
        fos.close();

        FileInputStream fis = new FileInputStream("temp/hello.dat");
        int data;
        while ((data = fis.read()) != -1) {
            System.out.println(data);
        }
        fis.close();
    }
}

입력 스트림의 read() 메서드는 파일의 끝에 도달하면 -1 을 반환한다. 따라서 -1 을 반환할 때 까지 반복문을 사용하면 파일의 데이터를 모두 읽을 수 있다.

 

스트림 시작2

스트림 시작 - 예제3

이번에는 byte 를 하나씩 다루는 것이 아니라, byte[] 을 사용해서 데이터를 원하는 크기만큼 더 편리하게 저장하고 읽는 방법을 알아보자!

package io.start;

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

public class StreamStartMain1_1 {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        byte[] input = {65, 66, 67};
        fos.write(input);
        fos.close();

        FileInputStream fis = new FileInputStream("temp/hello.dat");
        byte[] buffer = new byte[10];
        int readCount = fis.read(buffer, 0, 10);
        System.out.println("readCount: " + readCount);
        System.out.println(Arrays.toString(buffer));
        fis.close();
    }
}

 

출력 스트림

wirte(byte[]) : byte[] 에 원하는 데이터를 담고 write() 에 전달하면 해당 데이터를 한번에 출력할 수 있따.

 

입력 스트림

  • read(byte[], offset, length  ) : byte[] 을 미리 만들어두고, 만들어둔 byte[] 에 한번에 데이터를 읽어올 수 있다.
  • byte[] : 데이터가 읽혀지는 버퍼
  • offset : 데이터 기록되는 byte[] 의 인덱스 시작 위치
  • length :  읽어올 byte 의 최대 길이
  • 반환값 : 버퍼에 읽은 총 바이트 수 
  • 여기서는 3byte  를 읽었으므로 3이 반환된다. 스트림의 끝에 도달하여 더 이상 데이터가 없는 경우 -1 반환

read(byte[]) 

  • 참고로 ` offset` , ` length` 를 생략한 read(byte[])`메서드도 있다.
  • 이 메서드는 다음 값을 가진다.
    • offset : 0 
    • length : byte[].length
package io.start;

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

public class StreamStartMain4_1 {

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        byte[] input = {65, 66, 67};
        fos.write(input);
        fos.close();

        FileInputStream fis = new FileInputStream("temp/hello.dat");
        byte[] readBytes = fis.readAllBytes();
        System.out.println(Arrays.toString(readBytes));
        fis.close();
    }
}
  • readAllBytes() 를 사용하면 스트림이 끝날 때 까지 (파일의 끝에 도달할 떄 까지) 모든 데이터를 한번에 읽어올 수 있다.

 

부분으로 나누어 읽기 vs 전체 읽기

  • read(byte[], offset, lentgh)
    • 스트림의 내용을 부분적으로 읽거나, 읽은 내용을 처리하면서 스트림을 계속 읽어야 할 경우에 적합하다.
    • 메모리 사용량을 제어할 수 있다.
    • 예시) 파일이나 스트림에서 일정한 크기의 데이터를 반복적으로 읽어야 할 때 유용하다. 예를 들어. 대용량 파일을 처리할 때, 한 번에 메모리에 로드하기보다는 이 메서드를 사용하여 파일을 조각조각 읽어들일 수 있다.
    • 100M 의 파일을 1M 단위로 나누어 읽고 처리하는 방식을 사용하면 한 번에 최대 1M 의 메모리만 사용한다.
  • readAllByte()
    • 한번에 호출로 모든 데이터를 읽을 수 있어 편리하다.
    • 작은 파일이나 메모리에 모든 내용을 올려서 처리해야 하는 경우에 적합하다.
    • 메모리 사용량을 제어할 수 없다.
    • 큰 파일의 경우 OutOfMemroyError 가 발생할 수 있다.

 

InputStream, OutputStream

 

현재의 컴퓨터는 대부분 byte 단위로 데이터를 주고 받는다. 참고로 bit 단위는 너무 작기 때문에 byte 단위를 기본으로 한다.

이렇게 데이터를 주고 받는 것을 Input/Outout(I/O) 라 한다.

자바 내부에 있는 데이터를 외부에 있는 파일에 저장하거나, 네트워크를 통해 전송하거나 콘솔에 출력할 때 모두 byte 단위로 데이터를 주고 받는다.

만약 파일, 네트워크, 콘솔 각각 데이터를 주고 받는 방식이 다르다면 상당히 불편할 것이다.

또한 파일에 저장하던 내용을 네트워크에 전달하거나 콘솔에 출력하도록 변경할 때 너무 많은 코드를 변경해야 할 수 있다.

이런 문제를 해결하기 위해 자바는 InputStream, OutputStream 이라는 기본 추상 클래스를 제공한다.

  • InpueStream 과 상속 클래스
  • read() , read(byte[]) , readAllBytes()  제공

  • OutputStream 과 상속 클래스
  • write(int) , write(byte[]) 제공

스트림을 사용하면 파일을 사용하든, 소켓을 통해 네트워크에 사용하든 모두 일관된 방식으로 데이터를 주고 받을 수 있다. 그리고 수 많은 기본 구현 클래스들도 제공한다.

물론 각각의 구현 클래스들은 자신에게 맞는 추가 기능도 제공한다.

 

파일에 사용하는 FileInputStream ,  FileOutputStream  은 앞서 알아보았다. 네트워크 관련 스트림은 이후에 네트워크를 다룰때 알아보자. 여기서는 메모리와 콘솔에 사용하는 스트림을 사용해보자.

 

메모리 스트림

package io.start;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class ByteArrayStreamMain_1 {

    public static void main(String[] args) throws IOException {
        byte[] input = {1, 2, 3};

        // 메모리에 쓰기
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(input);

        // 메모리에 읽기
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        byte[] bytes = bais.readAllBytes();
        System.out.println(Arrays.toString(bytes));
    }
}

 

 

ByteArrayOutputStream, ByteArrayInputStream 을 사용하면 메모리에 스트림을 쓰고 읽을 수 있다. 이 클래스들은 OutputStream, InputStream 을 상속받았기 때문에 부모의 기능을 모두 사용할 수 있다.

코드를 보면 파일 입출력과 매우 비슷한 것을 확인할 수 있다.

 

참고로 메모리에 어떤 데이터를 저장하고 읽을 때는 컬렉션이나 배열을 사용하며 되기 때문에, 이 기능은 잘 사용하지 않는다. 주로 스트림을 간단하게 테스트하거나 스트림의 데이터를 확인하는 용도로 사용한다.

 

콘솔 스트림

package io.start;

import java.io.IOException;
import java.io.PrintStream;

import static java.nio.charset.StandardCharsets.UTF_8;

public class PrintStreamMain_1 {

    public static void main(String[] args) throws IOException {
        PrintStream printStream = System.out;

        byte[] bytes = "Hello!\n".getBytes(UTF_8);
        printStream.write(bytes);
        printStream.println("Print");
    }
}

 

우리가 자주 사용하던 System.out 이 사실은 PrintStream 이다. 이 스트림은 OutputStream 를 상속받는다.

이 스트림은 자바가 시작될 때 자동으로 만들어진다. 따라서 우리가 직접 생성하지는 않는다.

  • write(byte[]) : OutputStream 의 부모 클래스가 제공하는 기능이다.
  • println(String) :PrintStream 이 자체적으로 제공하는 추가 기능이다.

 

정리 

InputStream 과 OutputStream 이 다양한 스트림들을 추상화하고 기본 기능에 대한 표준을 잡아둔 덕분에 개발자는 편리하게 입출력 작업을 수행할 수 있다. 이러한 추상화의 장점은 다음과 같다.

  • 일관성 : 모든 종류의 입출력 작업에 대해 동일한 인터페이스(여기서는 부모의 메서드) 를 사용할 수 있어, 코드의 일관성이 유지된다.
  • 유연성 : 실제 데이터 소스나 목적지가 무엇인지에 관계 없이 동일한 코드를 작성할 수 있다. 예를 들어, 파일, 네트워크, 메모리 등 다양한 소스에 대해 동일한 메서드를 사용할 수 있다.
  • 확장성 : 새로운 유형의 입출력 스트림에 쉽게 추가할 수 있다.
  • 재사용성: 다양한 스트림 클래스들을 조합하여 복잡한 입출력 작업을 수행할 수 있다. 예를 들어 BufferedInputStream 을 사용하여 성능을 향상시키거나, DataInputStream 을 사용하여 기본 데이터 타입을 쉽게 읽을 수 있다. 
  • 에러 처리 : 표준화된 예외 처리 메커니즘을 통해 일관된 방식으로 오류를 처리할 수 있다.

 

참고로 InputStream ` , ` OutputStream ` ` 은 추상 클래스이다. 자바 1.0부터 제공되고, 일부 작동하는 코드도 들어있기 때문에 인터페이스가 아니라 추상 클래스로 제공된다

 

 

'JAVA' 카테고리의 다른 글

네트워크 - 프로그램1  (1) 2025.01.29
자바 - IO 기본 (buffer)  (1) 2025.01.28
지연 연산 vs 즉시 연산: Kotlin과 Java Stream을 통한 최적화 전략 이해하기  (0) 2025.01.17
Runnable과 Callable  (0) 2025.01.12
ReentrantLock  (0) 2025.01.07