본문 바로가기
네이버클라우드/JAVA 웹 프로그래밍

JAVA 27일차 (2023-06-29) 자바 기초 DAY25_자바 프로그래밍_입출력에 버퍼 적용하는 이유

by prometedor 2023. 6. 29.

ex06

버퍼 사용하기

  ㄴ 버퍼 사용 전/후 성능 비교

 

포맷

물리섹터를 몇개의 단위로 묶을 것인가

정보를 어떻게 관리할 것인가

 

* 데이터를 읽는 시간이란 seek time 과  read time 을 합친 만큼을 말함

ex06

Exam0110.java

// 버퍼 사용 전 - 데이터 읽는데 걸린 시간 측정
package com.eomcs.io.ex06;

import java.io.FileInputStream;

public class Exam0110 {

  public static void main(String[] args) throws Exception {
    FileInputStream in = new FileInputStream("temp/jls17.pdf");

    int b;

    long startTime = System.currentTimeMillis(); // 밀리초

    int callCount = 0;
    while ((b = in.read()) != -1) {
      callCount++; // 파일을 끝까지 읽는다.
    }

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(callCount);

    in.close();
  }

}

 

Exam0120.java

// 버퍼 사용 후 - 데이터 읽는데 걸린 시간 측정
package com.eomcs.io.ex06;

import java.io.FileInputStream;

public class Exam0120 {

  public static void main(String[] args) throws Exception {
    FileInputStream in = new FileInputStream("temp/jls17.pdf");

    byte[] buf = new byte[8192]; // 보통 8KB 정도 메모리를 준비한다.
    int len = 0;

    long startTime = System.currentTimeMillis(); // 밀리초

    int callCount = 0;

    while ((len = in.read(buf)) != -1) {
      System.out.printf("읽은 바이트 수: %d\n", len);
      callCount++; // 파일을 끝까지 읽는다.
    }

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(callCount);
    in.close();
  }

}

ㄴ 한 번 읽을 때 8192 바이트씩 읽음

ㄴ 시간이 확연히 줄어들은 것을 알 수 있음

=> 데이터를 많이씩 읽는 것이 좋음

 

테스트

MyFileInputStream.java 

MyFileInputStream.java

 

MyFileInputStream.java

package com.eomcs.io.ex06;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MyFileInputStream extends FileInputStream {

  byte[] buf = new byte[8192]; // 8KB
  int length; // 버퍼에 저장한 바이트 수
  int pos; // 읽을 바이트의 인덱스

  public MyFileInputStream(String name) throws FileNotFoundException {
    super(name);
  }

  @Override
  public int read() throws IOException {
    if (pos == length) { // 버퍼의 데이터를 다 읽었다면,
      length = this.read(buf); // 버퍼 크기만큼 파일에서 데이터를 읽어 들인다.
      pos = 0; // 버퍼 시작부터 읽을 수 있도록 위치를 0으로 설정한다.
    }
    return buf[pos++];
  }
}

 

Exam0130.java

// BufferedFileInputStream 사용 전 - 데이터 읽는데 걸린 시간 측정
package com.eomcs.io.ex06;

public class Exam0130 {

  public static void main(String[] args) throws Exception {
    MyFileInputStream in = new MyFileInputStream("temp/jls17.pdf");

    int b;

    long startTime = System.currentTimeMillis(); // 밀리초

    int callCount = 0;
    while ((b = in.read()) != -1)
      callCount++; // 파일을 끝까지 읽는다.

    // => BufferedFileInputStream의 read() 메서드는
    // FileInputStream에서 상속 받은 메서드를 이용하여
    // 바이트 배열로 데이터를 왕창 가져온 다음
    // 그 배열에서 1바이트를 리턴한다.
    // => 그 이후에는 바이트 배열의 데이터가 떨어질 때까지
    /// 계속 바이트 배열에서 값을 꺼내 리턴해 준다.
    // => 바이트 배열의 값이 떨어지면
    // 다시 바이트 배열 단위로 데이터를 읽어 온다.
    // 그래서 1바이트 씩 읽더라도 그렇게 속도가 떨어지지 않는 것이다.

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(callCount);

    in.close();
  }

}

=>

MyFileInputStream.java

package com.eomcs.io.ex06;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MyFileInputStream extends FileInputStream {

  byte[] buf = new byte[8192]; // 8KB
  int length; // 버퍼에 저장한 바이트 수
  int pos; // 읽을 바이트의 인덱스

  public MyFileInputStream(String name) throws FileNotFoundException {
    super(name);
  }

  @Override
  public int read() throws IOException {
    if (pos == length) { // 버퍼의 데이터를 다 읽었다면,
      length = this.read(buf); // 버퍼 크기만큼 파일에서 데이터를 읽어 들인다.
      if (length == -1) {
        length = 0;
        return -1;
      }
      pos = 0; // 버퍼 시작부터 읽을 수 있도록 위치를 0으로 설정한다.
      System.out.println(length + " 바이트 읽었음!");
    }
    return buf[pos++] & 0x000000ff; // 4바이트 int값으로 바꿔줌
  }
}

 

Exam0130.java

// BufferedFileInputStream 사용 후 - 데이터 읽는데 걸린 시간 측정
package com.eomcs.io.ex06;

public class Exam0130 {

  public static void main(String[] args) throws Exception {
    BufferedFileInputStream in = new BufferedFileInputStream("temp/jls17.pdf");

    int b;

    long startTime = System.currentTimeMillis(); // 밀리초

    int callCount = 0;
    while ((b = in.read()) != -1)
      callCount++; // 파일을 끝까지 읽는다.

    // => BufferedFileInputStream의 read() 메서드는
    // FileInputStream에서 상속 받은 메서드를 이용하여
    // 바이트 배열로 데이터를 왕창 가져온 다음
    // 그 배열에서 1바이트를 리턴한다.
    // => 그 이후에는 바이트 배열의 데이터가 떨어질 때까지
    /// 계속 바이트 배열에서 값을 꺼내 리턴해 준다.
    // => 바이트 배열의 값이 떨어지면
    // 다시 바이트 배열 단위로 데이터를 읽어 온다.
    // 그래서 1바이트 씩 읽더라도 그렇게 속도가 떨어지지 않는 것이다.

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(callCount);

    in.close();
  }

}

=> 속도 향상됨

ㄴ BufferedFileInputStream.java 참고하자

 

BufferedFileInputStream.java

package com.eomcs.io.ex06;

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

public class BufferedFileInputStream extends FileInputStream {

  byte[] buf = new byte[8192];
  int size; // 배열에 저장되어 있는 바이트의 수
  int cursor; // 바이트 읽은 배열의 위치

  public BufferedFileInputStream(String filename) throws Exception {
    super(filename);
  }

  // 파일에서 버퍼로 왕창 읽어 온 횟수
  int readCount = 0;

  // 버퍼를 사용하는 서브 클래스의 특징에 맞춰서
  // 상속 받은 메서드를 재정의 한다.
  @Override
  public int read() throws IOException {
    if (size == -1 || cursor == size) { // 바이트 배열에 저장되어 있는 데이터를 모두 읽었다면,
      if ((size = super.read(buf)) == -1) { // 다시 파일에서 바이트 배열로 데이터를 왕창 읽어 온다.
        return -1;
      }
      readCount++;
      System.out.printf("==>버퍼로 왕창 읽었음! - %d 번째\n", readCount);
      cursor = 0;
    }

    return buf[cursor++] & 0x000000ff;

    // 위의 리턴 문장은 컴파일 할 때 아래의 문장으로 바뀐다.
    // int temp;
    // temp = buf[cursor];
    // cursor++;
    // return temp & 0x000000ff;

  }
}

 

 

Exam0210.java

package com.eomcs.io.ex06;

import java.io.FileOutputStream;

public class Exam0210 {
  public static void main(String[] args) throws Exception {
    FileOutputStream out = new FileOutputStream("temp/data.bin");

    System.out.println("데이터 쓰는 중...");

    long start = System.currentTimeMillis();

    for (int i = 0; i < 1000000; i++) {
      out.write(0x55);
    }

    long end = System.currentTimeMillis();

    out.close();

    System.out.println("출력 완료!");
    System.out.printf("경과된 시간: %d\n", end - start);
  }
}

 

Exam220.java

package com.eomcs.io.ex06;

import java.io.FileOutputStream;

public class Exam0220 {
  public static void main(String[] args) throws Exception {
    FileOutputStream out = new FileOutputStream("temp/data.bin");

    System.out.println("데이터 쓰는 중...");

    long start = System.currentTimeMillis();

    byte[] buf = new byte[8192];
    int size = 0;

    for (int i = 0; i < 1000000; i++) {
      // 일단 바이트 버퍼에 저장한다.
      buf[size++] = 0x55;

      if (size >= buf.length) {
        out.write(buf); // 버퍼가 꽉 차면 파일로 내보낸다.
        size = 0; // 다시 버퍼를 쓸 수 있도록 size를 0으로 초기화 한다.
      }
    }

    // 마지막으로 버퍼에 남아 있는 바이트를 출력한다.
    out.write(buf, 0, size);

    long end = System.currentTimeMillis();

    out.close();

    System.out.println("출력 완료!");
    System.out.printf("경과된 시간: %d\n", end - start);
  }
}

ㄴ 버퍼가 꽉차면 그제서야 왕창 바이트 배열을 출력함

 

Exam0230.java

package com.eomcs.io.ex06;

public class Exam0230 {
  public static void main(String[] args) throws Exception {
    BufferedFileOutputStream out = new BufferedFileOutputStream("temp/data.bin");

    System.out.println("데이터 쓰는 중...");

    long start = System.currentTimeMillis();

    for (int i = 0; i < 1000000; i++) {
      out.write(0x55);
      // BufferedFileOutputStream은
      // 내부적으로 1바이트를 파일로 바로 출력하지 않고
      // 일단 byte[] 배열에 저장한다.
      // 바이트 배열이 꽉 찼을 때 파일로 출력한다.
      // 그래서 1바이트씩 파일로 바로 출력하는 것 보다 더 빠르다.
    }

    // BufferedFileOutputStream은 바이트 배열이 꽉 찼을 때만 파일로 출력하기 때문에
    // 바이트 배열에 데이터가 남아 있을 경우 강제적으로 출력해야 한다.
    out.flush(); // 버퍼에 남아 있는 것을 방출한다.

    long end = System.currentTimeMillis();

    out.close();

    System.out.println("출력 완료!");
    System.out.printf("경과된 시간: %d\n", end - start);
  }
}

ㄴ BufferedFileOutputStream 이용

ㄴ flush() 이용하여 버퍼에 남아 있는 것을 방출

 

BufferedFileOutputStream.java

package com.eomcs.io.ex06;

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

public class BufferedFileOutputStream extends FileOutputStream {
  byte[] buf = new byte[8192];
  int cursor;

  public BufferedFileOutputStream(String filename) throws Exception {
    super(filename); 
  }

  // 오버라이딩: 상속 받은 메서드를 서브 클래스의 역할에 맞춰서 재정의 한다.
  // 즉 버퍼를 사용하는 특징에 맞춰서 데이터를 파일에 출력하도록 변경한다.
  @Override
  public void write(int b) throws IOException {
    if (cursor == buf.length) { // 버퍼가 다차면
      super.write(buf); // 버퍼에 들어있는 데이터를 한 번에 출력한다.(수퍼 클래스를 상속 받음을 강조하기 위해 쓴 것)
      cursor = 0; // 다시 커서를 초기화시킨다.
    }

    // 1바이트 출력하라고 하면 일단 버퍼에 저장할 것이다.
    buf[cursor++] = (byte) b;
  }

  // 버퍼를 사용할 때는 특히 주의해야 한다.
  // 버퍼가 꽉 찼을 때만 파일로 내보내기 때문에
  // 버퍼에 잔여 데이터가 남아 있을 수 있다.
  // 버퍼의 잔여 데이터를 강제로 출력하도록 상속 받은 다음 메서드를 재정의 한다.
  @Override
  public void flush() throws IOException {
    if (cursor > 0) {
      this.write(buf, 0, cursor);
      cursor = 0;
    }
  }

  // 항상 입출력 스트림을 사용한 다음에는 자원 해제를 위해 close()를 호출해야 한다.
  // close()가 호출될 때 버퍼의 잔여 데이터를 내보내도록 상속 받은 메서드를 재정의 한다.
  @Override
  public void close() throws IOException {
    this.flush();
    super.close();
  }

}

ㄴ 버퍼에 쌓아두다가 버퍼가 꽉차면 출력하는 것

ㄴ 버퍼의 잔여 데이터가 남아 있을 수 있으므로 상속받은 flush() 메서드를 통해 잔여 데이터를 강제로 출력하도록 함

=> close() 에도 flush() 메서드 이용됨

 

Exam0310.java

// 버퍼 사용 전 - 파일 복사 및 시간 측정
package com.eomcs.io.ex06;

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

public class Exam0310 {

  public static void main(String[] args) throws Exception {
    FileInputStream in = new FileInputStream("temp/jls17.pdf");
    FileOutputStream out = new FileOutputStream("temp/jls17_2.pdf");

    int b;

    long startTime = System.currentTimeMillis(); // 밀리초

    int count = 0;
    while ((b = in.read()) != -1) {
      out.write(b);
      count++;
    }

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(count);

    in.close();
    out.close();
  }

}

ㄴ 파일을 읽을 때 마다 쓴다면 시간이 너무 오래걸림

 

Exam0320.java

// 버퍼 사용 후 - 파일 복사 및 시간 측정
package com.eomcs.io.ex06;

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

public class Exam0320 {

  public static void main(String[] args) throws Exception {
    FileInputStream in = new FileInputStream("temp/jls17.pdf");
    FileOutputStream out = new FileOutputStream("temp/jls17_3.pdf");

    byte[] buf = new byte[8192]; // 보통 8KB 정도 메모리를 준비한다.
    int len = 0;

    long startTime = System.currentTimeMillis(); // 밀리초

    int count = 0;
    while ((len = in.read(buf)) != -1) {
      out.write(buf, 0, len);
      count++;
    }

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(count);

    in.close();
    out.close();
  }

}

// 데이터 읽기 시간 = average seek time + data transfer time
// 퀀텀 HDD 예)
// average seek time = 0.0105 초
// data trasfer time = 0.00000005 초 / 1 byte
//
// 1000 바이트를 읽을 때
// 1) 1바이트를 1000 번 읽는 경우
// 0.01050005초 * 1000 byte = 10.50005초
// 2) 1000바이트 1번 읽는 경우
// 0.0105 * 0.00000005초 * 1000 byte = 0.01055초
// 1바이트를 여러 번 읽을 경우 매번 바이트의 위치를 찾아야 하기 때문에
// 평균 탐색시간이 누적되어 한 번에 1000 바이트를 읽는 것 보다 시간이 오래 걸린다.
//
// 그러면 RAM에 배열의 크기를 크게 잡으면 좋지 않겠는가?
// => PC 처럼 소수의 프로그램이 동시에 실행될 때는 상관없지만,
// 서버처럼 데이터를 읽는 코드가 동시에 수천개에서 수십만개일 때는
// 아무리 작은 크기의 메모리라도 매우 많아지게 된다.
// 그래서 보통 8KB 정도의 버퍼 크기를 유지하고 있다.
// => 물론 프로그램의 용도나 목적에 따라 버퍼의 크기가 이보다 더 작아지거나
// 커질 수 있다.

ㄴ 8KB 메모리 버퍼 준비

ㄴ 버퍼 사용 후 속도 향상

 

Exam0330.java

// BufferedFileInputStream과 BufferedFileOutputStream을 사용하여 파일 복사 및 시간 측정
package com.eomcs.io.ex06;

public class Exam0330 {

  public static void main(String[] args) throws Exception {
    BufferedFileInputStream in = new BufferedFileInputStream("temp/jls17.pdf");
    BufferedFileOutputStream out = new BufferedFileOutputStream("temp/jls11_4.pdf");

    int b;

    long startTime = System.currentTimeMillis(); // 밀리초

    int count = 0;
    while ((b = in.read()) != -1) {
      out.write(b);
      count++;
    }

    // 아직 파일로 출력되지 않고 버퍼 남아 있는 데이터를
    // 마무리로 출력한다.
    // out.flush();

    long endTime = System.currentTimeMillis();

    System.out.println(endTime - startTime);
    System.out.println(count);

    in.close();
    out.close();
  }

}

ㄴ BufferedFileInputStream 사용시 속도 향상

 

=> 버퍼를 도입하자!