본문 바로가기

카테고리 없음

자바에서의 입출력 - 1(InputStream/OutputStream)

728x90

 

이번 포스팅은 자바에서의 I/O (Input과 Output)에 대해서 작성했습니다.

 

자유로운 피드백은 환영입니다!!

 


스트림

자바에서 입출력을 수행하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 스트림(Stream)이라는 것이 필요합니다. 즉, 스트림이란 데이터를 운반하는 데 사용되는 연결통로라고 할 수 있습니다.

 


스트림 종류

스트림은 단방향 통신만 가능하므로, 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없습니다. 따라서, 입출력을 동시에 수행하려면 입력스트림(InputStream)과 출력스트림(OutputStream)이 모두 필요합니다.

 

바이트기반 스트림

아래와 같은 바이트 기반스트림은 바이트단위로 데이터를 전송하며, 입출력 대상에 따라 아래 종류의 입출력 스트림이 있습니다. 이들은 모두 InputStream 혹은 Outputstream의 자손들입니다.

 

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

 


보조 스트림

기반 스트림의 기능을 보완하기 위해 보조스트림이 제공됩니다. 실제 데이터를 주고받는 역할을 하지 않기 때문에, 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있습니다. 따라서, 보조스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해서 보조스트림을 생성해야 합니다.

 

보조스트림 사용법 예시
  1. 기반 스트림 생성 (filetest.txt)
  2.  보조 스트림 생성 (기반 스트림 이용)
FiledInputStream fis = new FileInputStream("test.txt"); // 기반 스트림
BufferedInputStream bis = new BufferedInputStream(fis); // 보조 스트림
bis.read(); // 데이터 읽기

보조스트림의 종류
  • FilterInputStreamFilterOutputStream: 필터를 이용한 입출력 처리
  • BufferedInputStreamBufferedOutputStream: 버퍼를 이용한 입출력 성능향상
  • DataInputStreamDataOutputStream: int, float와 같은 기본형(primitive type) 단위로 데이터 처리하는 기능
  • SequenceInputStream: 두 개의 스트림을 하나로 연결 (outputstream 없다.)
  • LineNumberInputStream: 읽어 온 데이터의 라인 번호를 카운트 (JDK1.1부터 LineNumberReader로 대체)
  • ObjectInputStreamObjectOutputStream: 데이터를 객체단위로 읽고 쓰는 데 사용 (주로 파일을 이용하며, 객체 직렬화와 관련 있음)
  • PrintStream: 버퍼를 이용하며, 추가적인 print과련 기능 (print, printf, println 메서드)
  • PushbackInputStream: 버퍼를 이용해서 읽어온 데이터를 다시 되돌리는 기능 (unread, push back to buffer)

 

그럼 본격적으로 기반 스트림부터 자세히 알아보도록 하겠습니다.

 

바이트기반 스트림

InputStream과 OutputStream

모두 바이트기반 스트림의 최고 조상이며 자주 사용되는 메서드는 아래와 같이 있습니다.

InputStream


public abstract class InputStream extends Object implements Closeable{
   
    // 자식 클래스들이 구현해야할 read 추상 메서드  
    // 바이트 하나를 읽어서 int로 반환, 더 이상 읽을 값이 없으면 -1을 리턴.
    public abstract int read() throws IOException;
   
    // len 바이트의 데이터를 읽어서 배열 b의 off 위치부터 집어넣기 (off는 배열 b의 index를 의미)
    // 읽은 바이트 개수를 반환, 더이상 읽을 값이 없으면 -1을 리턴
    public int read(byte[] b, int off, int len){
    	...
        for(int i=off; i < off + len; i++) {
        	b[i] = (byte)read(); // read()를 호출해서 데이터를 읽어서 배열에 삽입
        }
    }
   
    // byte b의 길이만큼 데이터를 InputStream으로부터 읽어들여 byte 배열 b에 삽입.
    // 읽은 바이트 개수를 반환, 더이상 읽을 값이 없으면 -1을 리턴
    public int read(byte[] b) throws IOException {
    	return read(b, 0, b.length); 
    }
    // InputStream을 닫는역할.
    public void close() throws IOException{
    	...
    }
    ...

}

 

위의 메소드 외에 다른 메서드들은 아래와 같습니다.


메서드명 설명
int available() 스트림으로부터 읽어 올 수 있는 데이터의 크기를 반환합니다.
void mark(int readlimit) 현재위치를 표시해 놓습니다. 후에 reset()에 의해서 표시해 놓은 위치로 돌아갈 수 있습니다. readlimit은 되돌아갈 수 있는 byte 수 입니다.
boolean markSupported() mark()와 reset()을 지원하는지를 알려줍니다. mark()와 reset()을 사용하기 전에 이 메서드를 호출해서 지원여부를 확인해야 합니다.
void reset() 스트림에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌립니다.
long skip(long n) 스트림에서 주어진 길이(n)만큼을 건너뜁니다.

 


OutputStream


public abstract class OutputStream extends Object implements Closeable, Flushable{

	// 자식들이 구현해야할 write(int b) 추상 메서드
	// 주어진 값 b를 노드에 write.
	public abstract void write(int b) throws IOException;
   
	// 배열 b에 있는 데이터 전부를 노드에 write
	public void write(byte[] b) throws IOException {
          write(b, 0, b.length);
   	}
   
	// 바이트 배열 b에 저장된 데이터 중 off위치부터 len개를 읽어서 노드에 write
   	public void write(byte[] b, int off, int len) throws IOException {
    	...
    	for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
   	}
	
   	// 출력 스트림에 있는 모든 데이터를 노드에 출력하고 버퍼를 비운다.
	public void flush() throws IOException {
    	...
	}
   
   	// OutputStream을 닫는다.
	public void close() throws IOException{
    	...
    }
	...
}

 


FileInputStream과 FileOutputStream

기반 스트림 중 가장 많이 사용되는 스트림 중 하나로, 파일에 입출력을 하기 위한 스트림입니다. 

FileInputStream

FileInputStream은 아래와 같이 3가지 방법으로 생성할 수 있습니다.


생성자 설명
FileInputStream(String name) 지정된 파일이름(name)을 가진 실제 파일과 연결된 FileInputStream을 생성합니다.
FileInputStream(File file) 파일의 이름이 String이 아닌 File 인스턴스로 지정하여 FileInputStream을 생성합니다.
FileInputStream(FileDescriptor fdObj) 파일 디스크립터(fdObj)로 FileInputStream을 생성합니다.

예시


FileInputStream을 이용하여 존재하는 파일을 읽어 들이는 코드를 작성해 보았습니다.

 

- abc.txt 파일

hello!
안녕하세요

 

 

abc.txt 위치


소스코드

public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("abc.txt");
        int data=0;
        while((data=fis.read())!=-1){
            System.out.print((char)data);
        }
        fis.close();
    }
}

출력결과

 

출력 결과, 아래와 같이 영어는 잘 출력이 되지만, 한글은 깨져서 나오게 됩니다.

 

출력 결과

 

 

이렇게 결과가 출력된 이유는, 위에서도 설명했듯이, read() 함수가 1byte씩 글자를 읽기 때문입니다. 1byte는 범위가 0~255까지이며, 알파벳 대/소문자의 아스키 값은 모두 이 범위에 들어가기 때문에 영어는 정상적으로 출력이 됩니다. 하지만, 한글은 2byte가 필요하기 때문에 바이트 기반의 stream에서는 깨지게 되는 것입니다. 

 

한글로 구성된 파일을 읽고 쓰려면 캐릭터 기반 스트림인 Reader/Writer를 사용해야합니다. 이에 관한 내용은 아래 포스팅에 작성하였으니 참고하세요!     

 

https://silver-programmer.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C%EC%9D%98-%EC%9E%85%EC%B6%9C%EB%A0%A5-2-ReaderWriter

 

자바에서의 입출력 - 2 (Reader/Writer)

저번 포스팅(자바의 입출력 -1)에서는 바이트 기반의 스트림들을 알아보았습니다. 바이트기반 스트림은 입출력의 단위가 1byte입니다. 하지만 한글과 같은 2 byte형 문자를 표현하기에는 바이트기

silver-programmer.tistory.com

 


아래 예시는 read(byte[ ] b) 메서드를 활용해 파일을 읽는 방식입니다.    


public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("abc.txt");
        byte[] b = new byte[6];
        
        fis.read(b); // b 배열 크기만큼 읽어와서 b 배열에 저장
        
        for (byte a : b) {
            System.out.print((char) a);
        }
        System.out.println();
        System.out.print(new String(b, StandardCharsets.UTF_8)); //byte[] -> String
        
        fis.close();
    }

}

출력 결과, 배열 크기인 6개 문자 (hello!)를 잘 읽어오는 것을 확인할 수 있습니다.

 

출력 결과


FileOutputStream

FileOutputStream은 아래와 같이 4가지 방법으로 생성할 수 있습니다.


생성자 설명
FileOutputStream(String name) 지정된 파일이름(name)을 가진 실제 파일과의 연결된 FileOutPutStream을 생성합니다.
FileOutputStream(String name, boolean append) 위의 생성자와 동일한 방법으로 FileOutputStream을 생성합니다. append 인자에 true를 넣으면, 출력 시 기존의 파일내용 뒤에 이어서 쓰게 됩니다. false를 넣으면, 기존 파일 내용을 덮어쓰게 됩니다.
FileOutputStream(File file) 파일의 이름을 String이 아닌 File인스턴스로 지정하여 FileOutputStream을 생성합니다.
FileOutputStream(File file, boolean append) 바로 위의 생성자와 동일한 방법으로 FileOutputStream을 생성합니다. append 인자에 true를 넣으면, 출력 시 기존의 파일내용 뒤에 이어서 쓰게 됩니다. false를 넣으면, 기존 파일 내용을 덮어쓰게 됩니다.
FileOutputStream(FileDescriptor fdObj) 파일 디스크립터(fdObj)로 FileOutputStream을 생성합니다.)

예시 코드

 

FileOutputStream 을 이용하여 파일을 작성하는 코드를 작성해 보았습니다.


public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("abc.txt");
        FileOutputStream fos = new FileOutputStream("hello.txt");

        int data = 0;
        while((data=fis.read())!=-1){
            fos.write(data);
        }

        fis.close();
        fos.close();
    }
}

아래와 같이 hello.txt 파일이 생성됨을 확인할 수 있습니다.

 

hello.txt 파일 위치

 

hello.txt


그런데, 여기서 궁금한 점이 생겼습니다. read()는 1byte 단위로 읽어서 한글이 제대로 파일에 출력이 안될 것이라 생각했는데 왜 제대로 출력이 되었는지 의문이 들었습니다. 그래서 찾아보니, 한글을 표현하는 2byte 크기의 이진수에서, 하위 1byte만 나미고 상위 1byte는 버리는 것이 아니라, 2byte 크기의 한글을 그냥 1byte/1byte로 나누어서 저장하는 것이었기 때문에 정보의 손실은 없다고 합니다.  


ByteArrayInputStream과 ByteArrayOutputStream

InputStream/OutputStream의 자손으로 바이트배열에 데이터를 입출력 하는데 사용되는 스트림입니다. 주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는 데 사용됩니다. 사용방법이 FileInputStream/FileOutputStream과 같기 때문에 동일한 방법으로 사용하면 되겠습니다.


바이트기반의 보조스트림

BufferedInputStream과 BufferedOutputStream

위의 바이트기반 스트림을 사용하여 입출력하는 것은 기본적으로 한 바이트 씩 입출력하기 때문에 시간이 오래 걸리게 됩니다. 따라서 대표적으로 버퍼를 이용하여 속도를 빠르게 할 수 있습니다. 그 역할을 해주는 것이 BufferedInputStream과 BufferedOutputStream입니다. 대부분의 입출력 작업에 사용되기 때문에 꼭 숙지하고 있어야 합니다.


생성자 및 핵심 메서드


생성자 및 메서드 설명
BufferedInputStream(InputStream in) 주어진 InputStream 객체를 입력소스로 하며 버퍼의 크기를 지정하지 않으므로 기본적으로 8192byte 크기의 BufferedInputStream 객체를 생성합니다.
BufferedInputStream(InputStream in, int size) 주어진 InputStream 객체를 입력소스로 하며 지정된 크기의 버퍼를 갖는 BufferedInputStream 객체를 생성합니다.
BufferedOutputStream(OutputStream out) 주어진 OutputStream 객체를 출력소스로 하며 버퍼의 크기를 지정하지 않으므로 기본적으로 8192byte 크기의 BufferedOutputStream 객체를 생성합니다. 
BufferedOutputStream(OutputStream out, int size) 주어진 OutputStream 객체를 출력소스로 하며  지정된 크기의 버퍼를 갖는 BufferedOutputStream 객체를 생성합니다.
flush() 버퍼의 모든 내용을출력소스에 출력한 후, 버퍼를 비웁니다.  
close() flush()를 호출해서 버퍼의모든 내용을 출력소스에 출력하고, BufferedOutputStream 객체가 사용하던 모든 자원을 출력합니다.  

 

BufferedInputStream을 이용하여 코드를 작성해 보았습니다. 또한 BufferedInputStream 사용 유무의 속도 차이를 비교하기 위해 시간을 측정하였습니다.

 

기존 FileInputStream/FileOutputStream만 사용하던 코드

public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("abc.txt");
        FileOutputStream fos = new FileOutputStream("hello.txt");

        int data = 0;
        long start = System.currentTimeMillis();
        while((data=fis.read())!=-1){
            fos.write(data);
        }
        long end = System.currentTimeMillis();

        fis.close();
        fos.close();

        System.out.println(end-start + "ms");
    }
}

출력결과

 

 

출력결과


 

BufferedInputStream/BufferedOutputStream을 사용하는 코드

public class Main {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("abc.txt");
        BufferedInputStream bis = new BufferedInputStream(fis);

        FileOutputStream fos = new FileOutputStream("hello.txt");
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        int data = 0;
        long start = System.currentTimeMillis();
        while((data=bis.read())!=-1){
            bos.write(data);
        }
        long end = System.currentTimeMillis();

        bis.close();
        bos.close();

        System.out.println(end-start + "ms");
    }
}

출력결과

 

 

출력결과

 

확실히 속도가 개선되었음을 확인할 수 있습니다. 이 예제에서 사용되는 파일은 내용이 짧기 때문에 6ms 정도 차이가 났지만, 파일의 내용이 많아질수록 속도 차이는 엄청나게 커질 것 같네요..!!


그 외 보조 스트림

1. FilterInputStream과 FilterOutputStream

  • InputStream/OutputStream의 자손이면서 모든 보조스트림의 조상입니다.
  • FilterInputStream/FilterOutputStream의 모든 메서드는 단순히 기반스트림의 메서드를 그대로 호출하기 때문에, 이들 자체로는 아무런 일을 하지 않습니다. 따라서, 이들을 상속하여 원하는 작업을 수행하기 위해 자손 클래스에서 메서드를 오버라이딩해야 합니다. 
생성자
  • protected FilterInputStream(InputStream in): protected 이기때문에 FilterInputStream 객체를 외부에서 직접 생성할 수 없고, 상속을 통해서 오버라이딩 되어야 합니다. 
  • public FilterOutputStream(OutputStream out)
  • FilterInputStream/FilterOutputStream을 상속받아서 기반스트림에 보조기능을 추가한 보조스트림 클래스는 아래와 같습니다.
    • BufferedInputStream, DataInputStream, PushbackInputStream 등
    • BufferedOutputStream, DataOutputStream, PrintStream 등

2. DataInputStream과 DataOutputStream

  • 데이터를 읽고 쓸 때, byte 단위가 아닌, 8가지 기본 자료형(primitive type)의 단위로 읽고 쓸 수 있어 편리합니다. (데이터를 변환할 필요도 없고, 자리수를 세어서 따지지 않아도 되므로 편리하고 빠르게 데이터를 저장하고 읽을 수 있습니다.)     
  • DataOutputStream은 각 기본 자료형 값을 4byte의 16진수로 표현하여 저장하기 때문에, 출력한 데이터를 다시 읽어올때는 출력했을 때의 순서를 염두에 두고 읽어야 합니다.
대표 메서드
  • DataInputStream
    • boolean readBoolean(), byte readByte(), char readChar(), short readShort(), int readInt(), long readLong(), float readFloat(), double readDouble(), int readUnsignedByte(), int readUnsignedShort(): 각 자료형에 맞게 값을 읽습니다. 더 이상 읽을 값이 없으면 EOFException이 발생합니다.  
    • void readFully(byte[]readFully(byte [] b), void readFully(byte [] b, int off, int len): 지정된 배열의 크기만큼 혹은 지정된 위치에서 len만큼 데이터를 읽어옵니다. 파일의 끝에 도달하면 EOFException이 발생합니다. 
    • String readUTF(): UTF-8형식으로 쓰인 문자를 읽습니다. 더 이상 읽을 값이 없으면 EOFException이 발생합니다.  
    • int skipBytes(int n): 현재 읽고 있는 위치에서 지정된 숫자(n)만큼을 건너뜁니다.
  • DataOutputStream
    • void writeBoolean(), void  writeByte(), void  writeChar(), void  writeShort(), void writeInt(), void  writeLong(), void  writeFloat(), void  writeDouble(), void  writeUnsignedByte(), void  writeUnsignedShort():
    • void writeUTF(String s): UTF형식으로 문자를 출력합니다.
    • void writeChars(String s): 주어진 문자열을 출력합니다. writeChar(int c)를 여러 번 호출한 결과와 같습니다. 
    • int size(): 지금까지 DataOutputStream에 쓰여진 byte의 수를 나타냅니다. 

3. SequenceInputStream

  • 여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 해 줍니다.
  • 큰 파일을 여러 개의 작은 파일로 나누고, 하나의 파일로 합치는 것과 같은 작업을 수행할 때 유용합니다.
  • 다른 보조 스트림과 달리 FilterInputStream이 아닌 InputStream을 바로 상속받습니다.
생성자
  • SequenceInputStream(Enumeration e): Enumeration에 저장된 순서대로 입력스트림을 하나의 스트림으로 연결합니다.
  • SequenceInputStream(InputStream s1, InputStream s2): 두 개의 입력스트림을 하나로 연결합니다. 

4. PrintStream

  • 데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버로딩하여 제공합니다.  
  • 우리가 지금까지 알게 모르게 많이 사용한 System.out.println()과 같은 메서드들이 PrintStream에서 제공하여 주는 메서드들입니다.
    • System.out, System.err가 PrintStream 입니다.
  • print()나 println()는 예외를 던지지 않고 내부에서 처리하도록 정의하였는데, 그 이유는 이 메서드들이 빈번하게 사용되기 때문입니다. 만약 예외를 내부에서 처리하지 않았다면 우리가 이 메서드들을 사용할 때마다 try-catch문이나 예외를 throw 해주는 식으로 처리해야 했을 것입니다.

 

[참고자료]

남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016

Java 파일 I/O 정리 (velog.io)

 

Java 파일 I/O 정리

java의 입출력 I/O에 대해 정리합니다. 자바에서 입출력를 수행하려면 두 대상을 연결하려는 무엇인가가 필요하고 이를 스트림(Stream)이라고 한다. 이때 스트림은 단방향으로 통신이 가능하며 하

velog.io

https://cocosy.tistory.com/19 

 

Java의 InputStream/OutputStream과 한글 깨짐 현상

오늘의 궁금증 토픽: Java의 InputStream/OutputStream과 한글 깨짐 현상 틀린 정보가 있다면 댓글로 날려주시면 감사하겠습니다.. 궁금한 점 1: 한글은 2바이트인데 왜 바이트 단위로 읽고 쓰는 FileInputStr

cocosy.tistory.com

 

728x90
반응형