바이트 단위 IO 클래스

바이트 단위 IO 클래스

바이트 단위 IO 클래스는 가장 기본이 되는 IO 클래스이다. 컴퓨터에 존재하는 모든 데이터는 바이트 단위로 구성되어 있기 때문이다. 바이트 스트림 클래스는 모두 추상 클래스인 InputStream과 OutputStream의 자식 클래스다. 이는 입력과 관련된 모든 바이트 스트림은 InputStream에 있는 메소드를 포함하며 출력과 관련된 모든 바이트 스트림은 OutputStream에 있는 메소드를 포함한다는 의미이다.

inputstream outputstream에 대한 이미지 검색결과 InputStream과 OutputStream은 Object를 상속한다.

1. InputStream과 OutputStream

바이트 관련 Input과 Output의 최상위 클래스인 InputStream과 OutputStream을 모른다면 나머지 바이트 스트림 관련 클래스도 모른다는 의미이다. 그러므로 이 두개의 클래스를 이해해야 이를 상속받는 하위 클래스도 이해할 수 있다.

InputStream과 OutputStream은 앞서 추상클래스라고 했다. 이는 InputStream과 OutputStream은 new 연산자를 이용해서 객체화시킬 수 없다는 것을 의미한다.

InputStream in = new InputStream();

따라서 InputStream 클래스는 다음과 같은 형태로만 사용할 수 있다.

InputStream in = new InputStream 클래스를 상속받은 클래스의 생성자();

System.in을 이용해서 키보드로 부터 입력받기

System.in은 InputStream의 형식이다. 이는 InputStream에 있는 모든 메소드를 사용할수 있다는 의미다. 또한 System.in은 표준 입력을 받는다. 하지만 키보드에서 문자를 누른다고 해서 곧장 read() 메소드를 통해 읽어들이는 것은 아니다.

키보드를 눌렀을 경우 눌려진 값은 운영체제가 관리하는 키보드 버퍼에 쌓이고 JVM에게는 값이 전달되지 않기 때문이다. JVM에게 값을 전달하려면 키보드의 엔터키를 입력해야 한다. 엔터키를 입력하게 되면 키보드 버퍼에 있던 값과 엔터 키 값이 한꺼번에 JVM에게 전달되는 것이다.

이렇게 전달된 문자열은 read() 메소드를 통해 차례대로 한 바이트씩 읽어 들일 수 있다.

2. FileInputStream과 FileOutputStream

FileInputStream과 FileOutputStream은 각각 InputStream과 OutputStream을 상속받으며, 파일로부터 바이트 단위로 입력받고, 대상 파일에 바이트 단위로 출력할 수 있는 클래스다.

이 두개의 클래스의 생성자는 파일명이나 파일 정보가 있는 File 클래스에 대한 객체를 전달받는다. 그리고 FileInputStream의 경우 생성자에 전달한 파일명이 실제로 존재하지 않는 파일명이면 java.io.FileNotFoundException을 발생시킨다.

System.in, System.out, System.err의 세가지 스트림을 제외하고, 앞으로 IO 클래스를 사용할 때는 다음 내용을 반드시 지켜야 한다.

  • try문에서 사용할 IO 클래스를 선언한다. 보통 null 값을 할당한다.
  • try 블록 안에서 IO 클래스의 객체를 생성한다.
  • finally 블록 안에서 IO 클래스의 close() 메소드를 호출한다.
import java.io.*;

public class FileInputStream {
    public static void main(String[] args){
        if(args.length != 1){
            System.out.println("use :java fileview file name");
            System.exit(0);
        }

        FileInputStream fis = null;
        try
        {
            fis = new FileInputStream(args[0]);
            int i = 0;
            while((i = fis.read()) != -1)
            {
                System.out.write(i);
            }
        }
        catch(Exception ex)
        {
                System.out.println(ex);
        }
        finally
        {
                try
                {
                    fis.close();
                }catch(IOException e)
                {

                }
        }
    }
}

파일을 읽어 출력하는 프로그램의 개선 문제

평범한 파일 입출력 프로그램의 대부분은 효율이 떨어진다. 이 문제를 개선하기 위해서는 운영체제에 대한 기본 지식이 필요하다. 우리가 사용하는 운영체제는 하드웨어를 제어하는 기능이 있다. 자바에서 파일을 읽어들이라고 명령을 내리면, 내부적으로는 운영체제에게 부탁해서 물리적 장치인 하드디스크로부터 읽어오게 되는 것이다. 그런데 이 읽어오는 부분에서 몇 가지 중요한 점을 고려해야 한다.

  • 자바 프로그래밍으로 1byte를 읽어오라고 실행하면, 운영체제는 실제로 1byte를 읽지 않고 보통 인접한 256byte나 512byte를 읽게 된다. 이는 1000byte 파일을 1바이트씩 읽어오라고 실행하면, 내부적으로는 512byte씩 1000번 읽어온다는 것을 의미한다.
  • 이런 문제를 해결하려면 1byte씩 읽어 들이는 것이 아니라, 운영체제가 실제로 읽어 들이는 단위로 읽어 들여야 한다.
  • 즉 1000바이트 파일이라면 2번만 디스크를 읽어 들이면 되기 때문에 상당히 효율적으로 프로그램이 동작하게 되는것이다.
  • 이 특징은 작은 크기의 파일을 읽어 들일때는 차이를 느낄 수가 없으며 파일의 크기가 커질수록 효과를 실감하게 된다.
import java.io.*;

public class ImprovedFileInputStream
{
    public static void main(String[] args)
    {
        if(args.length != 1)
        {
            System.out.println("Use : Java File Name");
            System.exit(0);
        }

        FileInputStream fis = null;
        try
        {
            fis = new FileInputStream(args[0]);
            int readCount = 0;
            byte[] buffer = new byte[512];
            while( ( readCount = fis.read(buffer) ) != -1)
            {
                System.out.write(buffer, 0, readCount);
            }
        }
        catch (IOException ex)
        {
            System.out.println(ex);
        }
        finally
        {
            try
            {
                fis.close();
            }
            catch(IOException e){}
        }
    }
}

이 코드의 핵심은 이부분 이다.

int readCount = 0;
byte[] buffer = new byte[512];
while( ( readCount = fis.read(buffer) ) != -1)
{
    System.out.write(buffer, 0, readCount);
}

512byte의 배열을 전달함으로써 파일으로부터 읽어 들인 내뇽이 바이트 배열인 buffer에 저장된다. 저장된 바이트 배열은 write() 메소드로 화면에 출력되게 된다.

3. DataInputStream과 DataOutputStream

DataInputStream과 DataOutputStream은 자바의 primitive type 데이터인 int, float, double, boolean, short, byte 등의 정보를 입력하고 출력하는데 알맞은 클래스이다.

DataInputStream은 생성자에서 InputStream을 받아들이며, DataOutputStream은 생성자에서 OutputStream을 받아들인다. 거듭 강조하지만 InputStream을 인자로 받아들인다는 것은 InputStream의 자식이나 자손 클래스를 받아들인다는 것이다.

귀찮아서 DataOutputStream은 생략한다. DataInputStream과 구성은 비슷하나 출력 메소드가 주를 이룬다.

DataInputStream과 DataOutputStream을 이용한 파일의 저장과 읽기

둘다 인자가 없는 생성자, 기본 생성자가 없다. InputStream, OutputStream을 인자로 받는 생성자만 있다. 따라서 Input과 Output은 파일을 읽어들이고 출력하는 객체를 인자로 전달해야 한다.

public class DataOutputStreamTest
{
    public static void main(String[] args)
    {
        FileOutputStream fos = null;
        DataOutputStream dos = null;
        try
        {
            fos = new FileOutputStream("data.bin");
            dos = new DataOutputStream(fos);
            dos.writeBoolean(true);
            dos.wirteByte((byte)5);
            dos.wirteInt(100);
            dos.wirteDouble(200.5);
            dos.wirteUTF("Hello World");
            System.out.println("Saved");
        }
        catch(Exception ex)
        {
            System.out.println(ex);
        }
        finally
        {
            try
            {
                fos.close();
            }catch(IOException e){}
            try
            {
                dos.close();
            }catch(IOException e){}
        }
    }
}
ByteArrayInputStream과 ByteArrayOutputStream

이 두 클래스는 말 그대로 바이트 배열을 차례대로 읽어 들이기 위한 클래스다. ByteArrayOutputStream은 내부적으로 저장 공간이 있어서 ByteArrayOutputStream에 있는 메소드를 이용해서 출력하게 되면 출력되는 모든 내용들이 내부적인 저장 공간에 쌓이게 된다. 그 후에 ByteArrayOutputStream에 있는 toByteArray()를 실행하면 저장된 모든 내용이 바이트 배열로 반환된다.

fis = new FileInputStream(args[0]);
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[512];
int readCount = 0;
// 파일로부터 읽어 들인 바이트 배열을 ByteArrayOutputStream에 쓴다.
while( (readCount = fis.read(buffer)) != -1 )
{
    baos.write(buffer, 0, readCount);
}

파일이나 배열의 내용을 읽어 들여 출력하는 클래스 작성

public static void print (InputStream in)
{
    byte[] buffer = new byte[512];
    int readCount = 0;
    try
    {
        while( (eadCount = in.read(buffer)) != -1 )
        {
            System.out.write(buffer, 0, readCount);
        }
        catch(IOException e)
        {
            System.out.println(e);
        }
    }
}

static한 print() 메소드는 InputStream을 인자로 받아들인다. 이는 InputStream의 자식 클래스나 자손 클래스의 객체를 모두 받아들일 수 있다는 것을 의미한다. 즉, FileInputStream이나 ByteArrayInputStream 등의 객체에 대한 참조를 전달받을 수 있는 것이다. 전달받은 후에는 InputStream에 있는 read() 메소드를 이용해서 읽어 들인 후, 표준 출력 장치로 출력하고 있다.

if(args[0].equals("file"))
{
    FileInputStream fis = null;
    try
    {
        fis = new FileInputStream("file.dat");
        //Static 메소드 print () 호출
        print(fis);
    }
    catch(Exception ex)
    {
        System.out.println(ex);
    }
}

출처 : 김성박 송지훈 공저, 『 자바 IO & NIO 네트워크 프로그래밍』, 한빛미디어(2004.9.30), 4장 인용.