-
BufferedOutputStream를 사용하면 성능이 좋아지는 이유 및 자세한 설명 with FileDescriptor자바웹프로그래밍/JAVA 2024. 7. 24. 14:37728x90반응형
Java에서 데이터를 디스크에 쓰는 과정은 여러 단계로 구성되어 있으며, 최종적으로는 운영 체제의 네이티브 파일 시스템 호출로 이어집니다. 여기서 BufferedOutputStream이 실제로 어떻게 작동하는지, 그리고 네이티브 코드에서 어떻게 구현되는지 살펴보겠습니다.
데이터가 디스크에 적재되는 과정
- Java 애플리케이션 코드: 애플리케이션은 FileOutputStream 또는 BufferedOutputStream을 사용하여 데이터를 파일에 씁니다.
- Java 클래스: FileOutputStream과 BufferedOutputStream 클래스는 Java 표준 라이브러리의 일부이며, 데이터를 디스크에 쓰기 위한 메서드를 제공합니다.
- 네이티브 메서드 호출: FileOutputStream의 write 메서드는 FileDescriptor의 네이티브 메서드 writeBytes를 호출합니다.
- JNI: 네이티브 메서드는 JNI를 통해 C/C++ 코드로 전달됩니다.
- 운영 체제 호출: 네이티브 코드에서 실제로 운영 체제의 파일 시스템 호출(write, open, close)을 사용하여 데이터를 디스크에 씁니다.
Java BufferedOutputStream를 사용하면 성능이 좋아지는 이유
과정
버퍼를 모아서 전달한다는 것은 데이터를 일정 크기의 조각으로 나누어 네이티브 메서드 writeBytes에 전달하는 것을 의미합니다. 예를 들어, 10MB 데이터를 2MB씩 버퍼에 모아서 저장하는 경우, writeBytes 메서드가 2MB의 데이터를 5번 호출하는 것입니다.
이를 통해 Java 코드가 어떻게 데이터를 네이티브 코드로 전달하는지, 그리고 어떻게 효율적으로 디스크에 쓰이는지 상세히 설명하겠습니다.
BufferedOutputStream 동작 방식
BufferedOutputStream은 데이터를 버퍼에 모아서 한 번에 디스크에 쓰는 방식으로 성능을 최적화합니다. 아래는 BufferedOutputStream의 내부 동작 방식입니다.
BufferedOutputStream의 내부 구현
public class BufferedOutputStream extends FilterOutputStream { protected byte buf[]; protected int count; public BufferedOutputStream(OutputStream out, int size) { super(out); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; } @Override public synchronized void write(int b) throws IOException { if (count >= buf.length) { flushBuffer(); } buf[count++] = (byte)b; } @Override public synchronized void write(byte[] b, int off, int len) throws IOException { if (len >= buf.length) { flushBuffer(); out.write(b, off, len); return; } if (len > buf.length - count) { flushBuffer(); } System.arraycopy(b, off, buf, count, len); count += len; } @Override public synchronized void flush() throws IOException { flushBuffer(); out.flush(); } private void flushBuffer() throws IOException { if (count > 0) { out.write(buf, 0, count); count = 0; } } }
예시 코드: 10MB 데이터를 2MB 버퍼로 나누어 쓰기
아래는 10MB 데이터를 2MB씩 나누어 버퍼링하고 파일에 쓰는 예제 코드입니다.
import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; public class BufferedWriteExample { public static void main(String[] args) { int dataSize = 10 * 1024 * 1024; // 10MB int bufferSize = 2 * 1024 * 1024; // 2MB byte[] data = new byte[dataSize]; // 10MB 데이터를 임의의 값으로 채웁니다. for (int i = 0; i < dataSize; i++) { data[i] = (byte) (i % 256); } long start = System.currentTimeMillis(); try (FileOutputStream fos = new FileOutputStream("output_buffered.txt"); BufferedOutputStream bos = new BufferedOutputStream(fos, bufferSize)) { bos.write(data); } catch (IOException e) { e.printStackTrace(); } long duration = System.currentTimeMillis() - start; System.out.println("BufferedOutputStream 걸린 시간: " + duration + " ms"); } }
데이터가 디스크에 적재되는 과정
- 데이터 준비: 10MB 데이터를 준비합니다.
- BufferedOutputStream 생성: 2MB 버퍼 크기로 BufferedOutputStream을 생성합니다.
- 데이터 쓰기: BufferedOutputStream을 통해 데이터를 씁니다. 내부적으로는 다음과 같이 동작합니다:
- 데이터가 버퍼에 채워집니다.
- 버퍼가 꽉 차면 flushBuffer 메서드가 호출됩니다.
- flushBuffer는 버퍼의 내용을 FileOutputStream의 write 메서드에 전달합니다.
- 네이티브 메서드 호출: FileOutputStream의 write 메서드는 네이티브 메서드 writeBytes를 호출하여 데이터를 디스크에 씁니다.
- 반복: 이 과정이 2MB 단위로 반복되며, 최종적으로 10MB 데이터를 5번의 네이티브 writeBytes 호출을 통해 디스크에 씁니다.
네이티브 메서드의 역할
네이티브 메서드 writeBytes는 실제로 데이터를 디스크에 쓰는 역할을 합니다. 아래는 네이티브 메서드의 간단한 예시입니다:
FileDescriptor 클래스의 네이티브 메서드
public final class FileDescriptor { private int fd; public native void writeBytes(byte[] b, int off, int len) throws IOException; public native void close() throws IOException; private native void open(String path, int flags) throws FileNotFoundException; static { System.loadLibrary("io"); } }
네이티브 코드 (io.c)
#include <jni.h> #include <fcntl.h> #include <unistd.h> #include "java_io_FileDescriptor.h" JNIEXPORT void JNICALL Java_java_io_FileDescriptor_writeBytes(JNIEnv *env, jobject this, jbyteArray b, jint off, jint len) { jbyte *bytes = (*env)->GetByteArrayElements(env, b, NULL); jint fd = (*env)->GetIntField(env, this, (*env)->GetFieldID(env, this, "fd", "I")); if (write(fd, bytes + off, len) < 0) { // Error handling } (*env)->ReleaseByteArrayElements(env, b, bytes, JNI_ABORT); } JNIEXPORT void JNICALL Java_java_io_FileDescriptor_close(JNIEnv *env, jobject this) { jint fd = (*env)->GetIntField(env, this, (*env)->GetFieldID(env, this, "fd", "I")); if (close(fd) < 0) { // Error handling } } JNIEXPORT void JNICALL Java_java_io_FileDescriptor_open(JNIEnv *env, jobject this, jstring path, jint flags) { const char *pathStr = (*env)->GetStringUTFChars(env, path, NULL); jint fd = open(pathStr, flags, 0666); (*env)->ReleaseStringUTFChars(env, path, pathStr); if (fd < 0) { // Error handling } (*env)->SetIntField(env, this, (*env)->GetFieldID(env, this, "fd", "I"), fd); }
결론
버퍼를 사용하여 데이터를 디스크에 쓰는 것은 효율성을 높이기 위한 방법입니다. BufferedOutputStream은 데이터를 일정 크기의 버퍼에 모아서 한 번에 디스크에 쓰고, 이는 여러 번 작은 데이터를 쓰는 것보다 효율적입니다. 네이티브 메서드는 이러한 버퍼된 데이터를 실제로 디스크에 쓰는 역할을 합니다. 10MB 데이터를 2MB씩 나누어 쓰는 경우, 네이티브 메서드 writeBytes는 총 5번 호출되어 데이터를 디스크에 저장합니다.
728x90반응형'자바웹프로그래밍 > JAVA' 카테고리의 다른 글
내부 스태틱 클래스(static nested class)를 사용하는 이유와 그 동작 방식 (0) 2024.07.25 InputStream 과 OutputStream에 차이 ! (0) 2024.07.24 org.springframework.core.io.Resource란 무엇인가 !!? (0) 2024.07.15 JVM의 메모리 영역에 대한 이해와 예시와 reflection의 이해 (0) 2024.07.11 jvm 클래스 로더 작동방식 (0) 2024.07.11