본문 바로가기
공부/java

IO 와 NIO 를 이용한 입출력 (1/2)

by 샤샤샤샤 2024. 5. 13.

자바에는 입출력을 위한 I/O API 존재한다.

I/O 클래스는 두개가 존재하는데, 기존 I/O 와 자바4에 등장해서 자바 7에 재정리된 버퍼를 사용하는 New I/O 다.

 

버퍼

버퍼에 대해 쉽게 이해하기 위해 먼저 마트에서 장을 본다고 가정해보자. 살 물건을 하나씩 계산대로 가지고 가기 보다는, 바구니에 담아서 한번에 가져가는 것이 이동시간도 적게 걸리고 힘도 덜 들 것이다. 버퍼는 바로 이 바구니와 비슷한 역할을 한다. 즉, 데이터를 다루기 전 일정 크기만큼 한번에 가져와 가지고 있는 저장소다. 덕분에 데이터를 사용하기 위해 매번 데이터를 찾으러 갈 필요가 없어 실행 시간이 빨라진다는 장점이 있다.

 

기본 I/O 클래스를 이용한 버퍼

코드로 살펴보기에 앞서 일부 세팅이 필요하다.

1. 더미 파일 생성

파워쉘에서 

fsutil fiel createnew [파일 이름] [용량]

명령어로 더미 파일을 생성하자.

필자는 10mb 의 파일을 생성했다.

 

2. 시간 측정을 위한 코드 작성

버퍼를 사용한 i/o 와 일반 i/o 사이의 시간 차이를 측정하기 위한 메서드를 작성하자.

   /**
     * 함수를 매개변수로 받아 실행 시간 측정하는 함수
     * */
    public static long measureRunTime(Runnable task){
        long startTime = System.currentTimeMillis();
        task.run();
        long endTime = System.currentTimeMillis();
        return endTime-startTime;
    }

메서드 참조 방식으로 함수를 매개변수로 넘겨주면 메서드 실행 시간을 구하는 함수다.

파일을 사용하기 쉽게 상수로 파일 경로를 지정해주자.

private static final String DUMMY_FILE_PATH = "C:\\Users\\hojun\\Desktop\\Git\\JavaStudy\\IOAndNIO\\dummyTest.txt";

 

 

이제 버퍼를 사용하지 않고 I/O를 하는 코드를 살펴보자

    public static void copyDummyWithIO() {
        try (InputStream in = new FileInputStream(DUMMY_FILE_PATH);) {

            while (true) {
                int byteDate = in.read();
                if (byteDate == -1) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

버퍼를 사용하지 않는 InputStream 을 사용하여 파일을 읽어오는 코드다.

 

그러면 이제 버퍼를 사용하는 i/o 코드를 살펴보자.

    public static void copyDummyWithBufferIO() {
        try (BufferedInputStream in = new BufferedInputStream( new FileInputStream(DUMMY_FILE_PATH), 1100);)) {
            // 처음 BufferedInputStream 에 파일을 읽어올때의 버퍼 크기 1100
            while (true) {
                int byteDate = in.read();
                if (byteDate == -1) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

InputStream 이 BufferedInputStream 으로 바뀐 것을 제외하면 똑같은 코드다.

이제 두개를 비교하는 코드를 살펴보자.

 

    public static void main(String[] args) {

        long time1 = measureRunTime(IOTimeTest::copyDummyWithIO);
        System.out.println("버퍼를 사용하지 않을 경우 처리 시간 : " +time1 + " milli seconds");
        long time2 = measureRunTime(IOTimeTest::copyDummyWithBufferIO);
        System.out.println("I/O 버퍼를 사용한 처리 시간 : " + time2 + " milli seconds");
    }

보면 거의 100배의 시간 차이가 나는 것을 확인할수 있다.

이유는 버퍼를 사용하면 일정 크기 만큼 데이터를 미리 버퍼 메모리에 저장해두기 때문이다. 이후 데이터가 필요할 때마다 버퍼에서 데이터를 가져오기 때문에 데이터 조회 속도가 훨씬 빠르다.

 

그런데 여기서 속도를 더 증가시킬수도 있다.

버퍼의 크기를 미리 실제 데이터 크기로 지정해두면 버퍼 메모리 조회 횟수가 줄어들어 실행 속도가 더 빨라진다.

    public static void copyDummyWithBufferIOWithBufferSize() {
        try (BufferedInputStream in = new BufferedInputStream( new FileInputStream(DUMMY_FILE_PATH));) {

            // 파일 크기를 버퍼 사이즈로 지정한다.
            int available = in.available();
            byte[] bufferSize = new byte[available];
            
            while (true) {
                int byteDate = in.read(bufferSize); // 버퍼 사이즈를 매개변수로 넣어줌
                if (byteDate == -1) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

참고로 이때 read() 에 지정된 버퍼 크기와 BufferedInputStream 을 생성할때 지정한 버퍼 사이즈는 서로 다른 것이다.

BufferedInputStream 에 지정된 버퍼 사이는 처음에 파일을 읽어와 저장하는 버퍼의 사이즈이고, read 의 버퍼는 스트림에서 한번에 얼만큼 읽어올지 지정하는 버퍼 사이즈다.

 

쉽게 표현하자면 BufferedInputStream 의 버퍼는 장바구니에 담을수 있는 양이고, read 의 버퍼는 계산대에 넘기기 위해 한손에 잡을수 있는 양이라고 보면 된다.

 

이제 코드를 실행해보자.

    public static void main(String[] args) {

        long time1 = measureRunTime(IOTimeTest::copyDummyWithIO);
        System.out.println("버퍼를 사용하지 않을 경우 처리 시간 : " +time1 + " milli seconds");
        long time2 = measureRunTime(IOTimeTest::copyDummyWithBufferIO);
        System.out.println("I/O 버퍼를 사용한 처리 시간 : " + time2 + " milli seconds");
        long time3 = measureRunTime(IOTimeTest::copyDummyWithBufferIOWithBufferSize);
        System.out.println("I/O 버퍼값을 정해준 처리 시간 : " + time3 + " milli seconds");

    }

시간이 더욱 줄어든 것을 확인할수 있다.

 

전체 코드

import java.io.*;

public class IOTimeTest {

    private static final String DUMMY_FILE_PATH = "C:\\Users\\hojun\\Desktop\\Git\\JavaStudy\\IOAndNIO\\dummyTest.txt";
    private static final String COPY_FILE_PATH = "C:\\Users\\hojun\\Desktop\\Git\\JavaStudy\\IOAndNIO\\CopyDummy.txt";

    public static void main(String[] args) {

        long time1 = measureRunTime(IOTimeTest::copyDummyWithIO);
        System.out.println("버퍼를 사용하지 않을 경우 처리 시간 : " +time1 + " milli seconds");
        long time2 = measureRunTime(IOTimeTest::copyDummyWithBufferIO);
        System.out.println("I/O 버퍼를 사용한 처리 시간 : " + time2 + " milli seconds");
        long time3 = measureRunTime(IOTimeTest::copyDummyWithBufferIOWithBufferSize);
        System.out.println("I/O 버퍼값을 정해준 처리 시간 : " + time3 + " milli seconds");

    }



    /**
     * 함수를 매개변수로 받아 실행 시간 측정하는 함수
     * */
    public static long measureRunTime(Runnable task){
        long startTime = System.currentTimeMillis();
        task.run();
        long endTime = System.currentTimeMillis();
        return endTime-startTime;
    }


    /**
     * i/o api 를 이용해서 10mb 의 더미 파일을 복사
     * flush() 를 사용하지는 않음으로 내용은 복사되지 않고 파일만 생성됨
     * */
    public static void copyDummyWithIO() {
        try (InputStream in = new FileInputStream(DUMMY_FILE_PATH);
             OutputStream out = new FileOutputStream(COPY_FILE_PATH)) {

            while (true) {
                int byteDate = in.read();
                if (byteDate == -1) {
                    break;
                }
//                out.write(byteDate);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void copyDummyWithBufferIO() {
        try (BufferedInputStream in = new BufferedInputStream( new FileInputStream(DUMMY_FILE_PATH), 1100);
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(COPY_FILE_PATH))) {

            while (true) {
                int byteDate = in.read();
                if (byteDate == -1) {
                    break;
                }
//                out.write(byteDate);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void copyDummyWithBufferIOWithBufferSize() {
        try (BufferedInputStream in = new BufferedInputStream( new FileInputStream(DUMMY_FILE_PATH));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(COPY_FILE_PATH))) {

            int available = in.available();
            byte[] bufferSize = new byte[available];
            while (true) {
                int byteDate = in.read(bufferSize);
                if (byteDate == -1) {
                    break;
                }
//                out.write(byteDate);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

outputStream 을 이용한 작성 코드는 주석처리 해서 읽기 시간만 측정했다.

 

 

결론

I/O 작업은 버퍼를 사용해야 훨씬 빠르고 버퍼 조회 횟수가 줄어들면 더욱 빨라질수 있다.