Java 길찾기/Java의 정석

[Java] 입출력 I/O - RandomAccessFile

Kindbeeeear_ 2022. 7. 28. 19:06

자바에서는 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있는데, RandomAccessFile만은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어 있다. InputStream이나 OutputStream으로부터 상속받지 않고, DataInput인터페이스와 DataOutput인터페이스를 모두 구현했기 때문에 읽기와 쓰기가 모두 가능하다.

 

사실 DataInputStream은 DataInput인터페이스를, DataOutputStream은 DataOutput인터페이스를 구현했다. 이 두 클래스의 기본 자료형(primitive data type)을 읽고 쓰기 위한 메서드들은 모두 이 2개의 인터페이스에 정의되어 있는 것들이다.

따라서, RandomAccessFile클래스도 DataInputStream과 DataOutputStream 처럼, 기본 자료형 다위로 데이터를 읽고 쓸 수 있다.

그래도 역시 RandomAccessFile클래스의 가장 큰 장점은 파일의 어느 위치에나 읽기/쓰기 가 가능하다는 것이다. 다른 입출력 클래스들은 입출력소스에 순차적으로 읽기/쓰기 를 하기 때문에 읽기와 쓰기가 제한적인데 반해서 RandomAccessFile클래스는 파일에 읽고 쓰는 위치에 제한이 없다.

 

이것을 가능하게 하기 위해 내부적으로 파일 포인터를 사용하는데, 입출력 시에 작업이 수행되는 곳이 바로 파일 포인터가 위치한 곳이 된다.

파일 포인터의 위치는 파일의 제일 첫 부분(0부터 시작)이며, 읽기 또는 쓰기를 수행할 때마다 작업이 수행된 다음 위치로 이동하게 된다. 순차적으로 읽기나 쓰기를 한다면, 파일 포인터를 이동시키기 위해 별도의 작업이 필요하지 않지만, 파일의 임의의 위치에 있는 내용에 대해서 작업하고자 한다면 먼저 파일 포인터를 원하는 위치로 옮긴 다음 작업을 해야 한다.

 

현재 작업 중인 파일에서 파일 포인터의 위치를 알고 싶을 때는 getFilePointer()를 사용하면 되고, 파일 포인터의 위치를 옮기기 위해서는 seek(long pos)나 skipBytes(int n)를 사용하면 된다.

|참고| 사실 모든 입출력에 사용되는 클래스들은 입출력시 다음 작업이 이루어질 위치를 저장하고 있는 포인터를 내부적으로 갖고 있다. 다만 내부적으로만 사용될 수 있기 때문에 작업자가 포인터의 위치를 마음대로 변경할 수 없다는 것이 RandomAccessFile과 다른 점이다.

생성자 / 메서드 설명
RandomAccessFile(File file, String mode)
RandomAccessFile(String fileName, String mode)
주어진 file에 읽기 또는 읽기와 쓰기를 하기 위한 RandomAccessFile인스턴스를 생성한다. mode의 값은 "r", "rw", "rws", "rwd"가 지정가능하다.
"r" : 파일로부터 읽기만을 수행할 때
"rw" : 파일에 읽기와 쓰기
"rws"와 "rwd"는 기본적으로 "rw"와 같은데, 출력내용이 파일에 지연 없이 바로 쓰이게 한다. "rwd"는 파일내용만, "rws"는 파일의 메타정보도 포함
FileChannel getChannel() 파일의 파일 채널을 반환한다.
FileDescriptor getFD() 파일의 파일 디스크립터를 반환
long getFilePointer() 파일 포인터의 위치를 알려준다.
long length() 파일의 크기를 얻을 수 있다.(byte 단위)
void seek(long pos) 파일 포인터의 위치를 변경한다. 위치는 파일의 첫 부분부터 pos크기만큼 떨어진 곳이다.(byte 단위)
void setLength(long newLength) 파일의 크기를 지정된 길이로 변경한다.(byte 단위)
int skipBytes(int n) 지정된 수만큼의 byte를 건너뛴다.

 

import java.io.*;

class RandomAccessFileEx1 {
    public static void main(String args[]) {
        try {
            RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
            System.out.println("파일 포인터의 위치 : " + raf.getFilePointer());
            raf.writeInt(100);
            System.out.println("파일 포인터의 위치 : " + raf.getFilePointer());
            raf.writeLong(100L);
            System.out.println("파일 포인터의 위치 : " + raf.getFilePointer());
        } catch(IOException e) {
            e.printStackTrace();
        }
    } 
}

 

 이 예제는 파일에 출력작업이 수행되었을 때 파일 포인터의 위치가 어떻게 달라지는지에 대해서 보여 준다. int가 4byte이기 때문에 writeInt()를 호출한 다음 파일 포인터의 위치가 0에서 4로 바뀐 것을 알 수 있다. 마찬가지로 8 byte인 long을 출력하는 writeLong()을 호출한 후에는 파일 포인터의 위치가 4에서 12로 변경된 것을 알 수 있다.

 

import java.io.*;

class RandomAccessFileEx2 {
    public static void main(String args[]) {
//                   번호, 국어, 영어, 수학
        int[] score = {1,  100,   90,   90,
                       2,   70,   80,  100,
                       3,  100,  100,  100,
                       4,   70,   60,   80,
                       5,   70,   90,  100 }; 
        
        try {
            RandomAccessFile raf = new RandomAccessFile("score2.dat", "rw");
            for(int i=0; i<score.length; i++){
                raf.writeInt(score[i]);
            }
            while(true) {
                System.out.println(raf.readInt());
            }
        } catch(EOFException eof) {
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

 

이 예제는 int배열 score에 저장된 데이터를 score2.dat에 저장한 다음, 저장된 내용을 readInt()로 읽어서 출력하도록 한 것 이다. 그러나 score2.dat파일은 생성되지만 화면에는 아무것도 출력되지 않는다.

그 이유는 writeInt()를 수행하면서 파일 포인터의 위치가 파일의 마지막으로 이동되었기 때문이다. 그 다음에 readInt()를 호출했으므로 파일의 앞부분이 아닌 마지막부분부터 읽기 시작하기 때문에 아무것도 읽지 못하고 EOFException이 발생해서 무한반복문을 벗어나게 된다.

그래서 다음과 같이 seek(long pos)를 이용해서 파일포인터의 위치를 다시 처음으로 이동시킨 다음에 readInt()를 호출하도록 해야한다.

// 변경 전
while(true) {
    System.out.println(raf.readInt());
}

// 변경 후
raf.seek(0);
while(true) {
    System.out.println(raf.readInt());
}

이처럼 randomAccessFile을 'rw(읽기쓰기)모드'로 생성해서 작업할 때는 이러한 점을 염두에 두어야한다.

 

import java.io.*;

class RandomAccessFileEx2 {
    public static void main(String args[]) {
        int sum = 0;

        try {
            RandomAccessFile raf = new RandomAccessFile("score2.dat", "r");
            int i=4;
            while(true) {
                raf.seek(i);
                sum += raf.readInt();
                i += 16;
            }
        } catch(EOFException eof) {
            System.out.println("sum : " + sum);
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

이전 예제에서 데이터를 저장한 score2.dat파일에서 국어과목의 점수만을 합계를 내는 예제이다. 한 학생의 데이터가 번호와 3과목의 점수로 모두 4개의 int값(4 * 4 = 16 byte)으로 되어 있기 때문에 i+=16과 같이 포인터의 값을 16씩 증가시키면서 readInt()를 호출했다.