티스토리 뷰
버퍼 이란?
버퍼(Buffer) 메모리는 데이터를 일시적으로 저장하는 메모리 공간이다. 주로 데이터의 입출력 속도 차이를 조정하고, 효율적인 데이터 처리를 돕기 위해 사용된다.
버퍼 메모리가 필요한 이유?
- 속도 차이 조정: CPU, RAM, 저장 장치(SSD, HDD)의 데이터 처리 속도가 다르기 때문에 버퍼를 사용하여 빠른 장치와 느린 장치 간의 속도 차이를 완충할 수 있다.
- 데이터 손실 방지: 버퍼를 사용하면 데이터를 안정적으로 저장하고 순차적으로 전달할 수 있기 때문에 버퍼가 없을 경우 데이터가 빠르게 도착하거나 늦게 도착할 수 있어서 손실이 발생할 수 있는데 이를 방지한다.
- 일괄 처리 가능: 프린터의 예를 들자면 프린터는 한 글자씩 출력하는 것이 아니라, 일정량의 데이터를 버퍼에 저장한 후 한꺼번에 인쇄한다.
버퍼 예제
아래 예제는 버퍼를 사용한 것과 사용하지 않는 것으로 나뉘어서 FileInputStream과 FileOutputStream를 활용하여 파일을 읽고 씀으로써 버퍼가 필요한 이유를 알아보았다.
파일 작성 시 버퍼 사용 X
public class CreateFileBufferedNotUsing {
public static final String FILE_NAME = "temp/buffered.dat"; // 파일 경로
public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
for (int i = 0; i < FILE_SIZE; i++) {
fos.write(1);
}
fos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
위의 버퍼를 사용하지 않는 코드를 보면 buffered.dat를 생성한 후 10*1024*1024 1바이트씩 약 1천만 번을 write 하였다. (※ temp 디렉터리는 src 밑이 아닌 최상단 프로젝트 하단에 생성.)
실행 결과
File created: temp/buffered.dat
File size: 10MB
Time taken: 62254ms
위의 실행 결과를 보니 1분 이상의 시간이 소요되었다. (PC마다 차이가 있다.)
파일 읽을 시 버퍼 사용 X
public class ReadFileBufferedNotUsing {
public static final String FILE_NAME = "temp/buffered.dat"; // 파일 경로
public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
int fileSize = 0;
int data;
while ((data = fis.read()) != -1) {
fileSize++;
}
fis.close();
long endTime = System.currentTimeMillis();
System.out.println("File name: " + FILE_NAME);
System.out.println("File size: " + fileSize / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
위의 코드에서 FileInputStream을 통해 파일을 읽은 후 1바이트씩 파일을 읽었다. 대략 10*1024*1024 약 1천만 번을 read 하였다. (※ read 메서드는 파일 내용을 하나씩 읽고 마지막까지 다 읽은 후 마지막에 -1을 반환한다.)
실행 결과
File name: temp/buffered.dat
File size: 10MB
Time taken: 22296ms
위의 실행 결과를 보니 20초 이상의 시간이 소요되었다. (PC마다 차이가 있다.)
시간이 많이 소요되는 이유?
시간이 많이 소요되는 이유는 바로 파일 용량은 10MB인데 1바이트씩 읽고 쓰기를 반복했기 때문이다. 이것은 상대적으로 매우 속도가 느린 하드웨어 장비에 1바이트씩 읽고 쓰기를 반복한 것이다. 비록 운영체제나 하드웨어 쪽에서 최적화가 발생하지만 자바에서 1바이트씩 읽고 쓰기를 운영체제에게 반복하기 때문에 오버헤드를 유발하게 된다. 따라서 버퍼를 활용하여 이런 오버헤드를 줄여야 한다.
파일 작성 시 버퍼 사용 O
public class CreateFileBufferedUsing {
public static final String FILE_NAME = "temp/buffered.dat"; // 파일 경로
public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB
public static final int BUFFER_SIZE = 8192; // 8KB
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
byte[] buffer = new byte[BUFFER_SIZE];
int bufferIndex = 0;
for (int i = 0; i < FILE_SIZE; i++) {
buffer[bufferIndex++] = 1;
// 버퍼가 가득 차면 쓰고, 버퍼를 비운다.
if (bufferIndex == BUFFER_SIZE) {
fos.write(buffer);
bufferIndex = 0;
}
}
// 끝 부분에 오면 버퍼가 가득차지 않고, 남아있을 수 있다. 버퍼에 남은 부분 쓰기
if (bufferIndex > 0) {
fos.write(buffer, 0, bufferIndex);
}
fos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
위의 코드를 보면 1바이트씩 write 하지 않고 8KB를 버퍼 배열에 담은 후 버퍼가 다 차게 되면 버퍼 배열 자체를 write 하였고 그 후 다시 버퍼 배열을 인덱스 0부터 채운다. 그리고 만약 버퍼 배열이 남게 되면 남는 사이즈만큼 다시 write 한다.
실행 결과
File created: temp/buffered.dat
File size: 10MB
Time taken: 36ms
위의 실행 결과를 보니 60초에서 확 줄어든 0.036초 정도 걸린 것을 확인할 수 있다.
파일 읽을 시 버퍼 사용 O
public class ReadFileBufferedUsing {
public static final String FILE_NAME = "temp/buffered.dat"; // 파일 경로
public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB
public static final int BUFFER_SIZE = 8192; // 8KB
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
byte[] buffer = new byte[BUFFER_SIZE];
int fileSize = 0;
int size;
while ((size = fis.read(buffer)) != -1) {
fileSize += size;
}
fis.close();
long endTime = System.currentTimeMillis();
System.out.println("File name: " + FILE_NAME);
System.out.println("File size: " + fileSize / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
위의 코드를 보면 8KB씩 버퍼에 담는(읽음) 것을 알 수 있다. 그리고 read(buffer)의 반납 결과는 버퍼 크기만큼 읽은 사이즈를 나타낸다.
실행 결과
File name: temp/buffered.dat
File size: 10MB
Time taken: 6ms
위의 실행 결과를 보니 20초에서 확 줄어든 0.006초 정도 걸린 것을 확인할 수 있다.
버퍼를 사용한 결과
위의 두 개의 코드를 통해 버퍼를 사용할 경우 시간 단축과 운영체제의 오버헤드를 줄일 수 있는 이점을 얻을 수 있다. 그런데 만약 버퍼의 크기를 늘이면 어떻게 될까?
버퍼의 크기를 늘리면?
아래 예제는 버퍼의 크기를 늘린 후 파일 쓰기를 통해 알아보았다.
버퍼의 크기를 늘릴 때
public class CreateFileBufferedUsing {
public static final String FILE_NAME = "temp/buffered.dat"; // 파일 경로
public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB
public static final int BUFFER_SIZE = 51200; // 50KB
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
byte[] buffer = new byte[BUFFER_SIZE];
int bufferIndex = 0;
for (int i = 0; i < FILE_SIZE; i++) {
buffer[bufferIndex++] = 1;
// 버퍼가 가득 차면 쓰고, 버퍼를 비운다.
if (bufferIndex == BUFFER_SIZE) {
fos.write(buffer);
bufferIndex = 0;
}
}
// 끝 부분에 오면 버퍼가 가득차지 않고, 남아있을 수 있다. 버퍼에 남은 부분 쓰기
if (bufferIndex > 0) {
fos.write(buffer, 0, bufferIndex);
}
fos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
위의 코드를 보면 8KB에서 50KB으로 버퍼의 크기를 변경하였다.
실행 결과
File created: temp/buffered.dat
File size: 10MB
Time taken: 18ms
위의 실행 결과를 보니 0.036초에서 조금 줄어든 0.018초 정도 걸린 것을 확인할 수 있다.
버퍼의 크기가 클수록 좋을까?
물론 바로 위의 예제에서 시간이 줄어든 것을 알 수 있지만 그렇게 시간이 많이 줄어들지는 않았다. 그 이유는 PC내의 디스크나 파일 시스템에서 데이터를 읽고 쓰는 단위가 4KB 또는 8KB 정도이다. 그렇기 때문에 해당 단위로 저장하기 때문에 한계가 있고 괜히 JVM 내부에서 불필요한 데이터만 사용할 수 있다.
정리
버퍼를 사용하면 데이터의 입출력 속도 차이를 조정하고, 효율적인 데이터 처리를 도운다.
본 포스팅은 “김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션/인프런”를 학습한 내용을 정리한 것
'Java > Java' 카테고리의 다른 글
| <Java> 원자적 연산이란? (5) | 2025.08.09 |
|---|---|
| <Java> yield() 메서드란? (0) | 2025.05.10 |
| <Java> Object 객체의 wait(), notify(), notifyAll() 메서드란? (1) | 2025.01.25 |
| <Java> ReentrantLock이란? (0) | 2024.10.26 |
| <Java> sychronized이란? (1) | 2024.10.22 |
