Java MMF(Memory-Mapped File)

방지환·2026년 1월 22일

Java

목록 보기
17/19

MMF(Memory-Mapped File)란?

Memory-Mapped File은 파일의 내용을 프로세스의 가상 메모리 주소 공간에 매핑하는 기술입니다. 운영체제가 파일을 메모리처럼 다룰 수 있게 해주어, 디스크 I/O 작업을 메모리 접근처럼 처리할 수 있습니다.

MMF의 장점

  1. 빠른 성능: 버퍼링 오버헤드가 줄어들고, 커널 영역과 사용자 영역 간의 데이터 복사가 불필요
  2. 대용량 파일 처리: JVM 힙 메모리 제한 없이 대용량 파일 처리 가능
  3. 메모리 효율성: OS의 페이지 캐시를 활용하여 메모리 효율적 사용
  4. 랜덤 액세스: 파일의 임의 위치에 빠르게 접근 가능

MMF의 단점

  1. 작은 파일에는 비효율적: 매핑 오버헤드로 인해 작은 파일에는 일반 I/O가 더 빠를 수 있음
  2. 플랫폼 의존성: 운영체제마다 동작 방식이 다를 수 있음
  3. 메모리 관리 복잡성: 명시적인 언맵(unmap) 처리가 필요할 수 있음

Java에서 MMF 사용하기

1. 기본 Java NIO (추천)

Java는 java.nio 패키지를 통해 기본적으로 MMF를 지원합니다. 별도의 라이브러리 설치가 필요 없습니다.

파일 읽기 예제

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MMFReadExample {
    public static void main(String[] args) {
        String filePath = "example.txt";
        
        try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
             FileChannel channel = file.getChannel()) {
            
            // 파일 전체를 메모리에 매핑 (읽기 전용)
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_ONLY, 
                0, 
                channel.size()
            );
            
            // 데이터 읽기
            byte[] data = new byte[(int) channel.size()];
            buffer.get(data);
            String content = new String(data, StandardCharsets.UTF_8);
            
            System.out.println("파일 내용: " + content);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

파일 쓰기 예제

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MMFWriteExample {
    public static void main(String[] args) {
        String filePath = "output.txt";
        String content = "Memory-Mapped File로 쓰는 내용입니다!";
        
        try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");
             FileChannel channel = file.getChannel()) {
            
            byte[] data = content.getBytes(StandardCharsets.UTF_8);
            
            // 파일을 데이터 크기만큼 메모리에 매핑 (읽기/쓰기)
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_WRITE, 
                0, 
                data.length
            );
            
            // 데이터 쓰기
            buffer.put(data);
            buffer.force(); // 강제로 디스크에 flush
            
            System.out.println("파일 쓰기 완료!");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

대용량 파일 처리 예제

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class LargeFileProcessing {
    private static final int CHUNK_SIZE = 100 * 1024 * 1024; // 100MB 청크
    
    public static void processLargeFile(String filePath) {
        try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
             FileChannel channel = file.getChannel()) {
            
            long fileSize = channel.size();
            long position = 0;
            
            while (position < fileSize) {
                long remaining = fileSize - position;
                long size = Math.min(CHUNK_SIZE, remaining);
                
                // 청크 단위로 메모리 매핑
                MappedByteBuffer buffer = channel.map(
                    FileChannel.MapMode.READ_ONLY,
                    position,
                    size
                );
                
                // 데이터 처리
                processChunk(buffer);
                
                position += size;
            }
            
            System.out.println("대용량 파일 처리 완료!");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static void processChunk(MappedByteBuffer buffer) {
        // 여기서 실제 데이터 처리 로직 구현
        // 예: 바이트 단위로 읽기
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            // 처리 로직...
        }
    }
}

2. Chronicle Map (권장 라이브러리)

고성능 Key-Value 저장이 필요할 때 사용하는 오픈소스 라이브러리입니다.

Maven 의존성

<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-map</artifactId>
    <version>3.24ea3</version>
</dependency>

Gradle 의존성

implementation 'net.openhft:chronicle-map:3.24ea3'

사용 예제

import net.openhft.chronicle.map.ChronicleMap;
import java.io.File;

public class ChronicleMapExample {
    public static void main(String[] args) {
        File file = new File("chronicle-map.dat");
        
        try (ChronicleMap<String, String> map = ChronicleMap
                .of(String.class, String.class)
                .name("user-map")
                .entries(1_000_000)
                .averageKeySize(10)
                .averageValueSize(100)
                .createPersistedTo(file)) {
            
            // 데이터 쓰기
            map.put("user1", "홍길동");
            map.put("user2", "김철수");
            
            // 데이터 읽기
            String value = map.get("user1");
            System.out.println("Value: " + value);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. Apache Arrow (대용량 데이터 처리)

컬럼 기반 인메모리 데이터 포맷으로, 대용량 데이터 분석에 최적화되어 있습니다.

Maven 의존성

<dependency>
    <groupId>org.apache.arrow</groupId>
    <artifactId>arrow-memory-netty</artifactId>
    <version>14.0.1</version>
</dependency>
<dependency>
    <groupId>org.apache.arrow</groupId>
    <artifactId>arrow-vector</artifactId>
    <version>14.0.1</version>
</dependency>

성능 비교

방식읽기 속도쓰기 속도사용 사례
일반 FileInputStream보통보통작은 파일, 순차 읽기
BufferedInputStream빠름빠름중간 크기 파일
Memory-Mapped File매우 빠름매우 빠름대용량 파일, 랜덤 액세스
Chronicle Map매우 빠름매우 빠름Key-Value 저장소

실전 팁

1. 적절한 MapMode 선택

// 읽기 전용: FileChannel.MapMode.READ_ONLY
// 읽기/쓰기: FileChannel.MapMode.READ_WRITE
// 쓰기 전용 (copy-on-write): FileChannel.MapMode.PRIVATE

2. 메모리 정리

// MappedByteBuffer 명시적 정리 (Java 9+)
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public static void unmap(MappedByteBuffer buffer) {
    try {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        unsafe.invokeCleaner(buffer);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3. 청크 단위 처리

대용량 파일은 한 번에 매핑하지 말고 청크 단위로 나눠서 처리하세요.

private static final int CHUNK_SIZE = 128 * 1024 * 1024; // 128MB

4. Force() 사용

데이터 무결성이 중요한 경우 force() 메서드로 즉시 디스크에 쓰기를 보장하세요.

buffer.force(); // 버퍼의 변경사항을 즉시 디스크에 반영

주의사항

  1. 파일 크기 제한: 32비트 JVM에서는 2GB 이상의 파일을 한 번에 매핑할 수 없습니다.
  2. 리소스 해제: FileChannelRandomAccessFile을 반드시 닫아야 합니다 (try-with-resources 권장).
  3. 동시성: 여러 스레드에서 동일한 MappedByteBuffer에 접근할 때는 동기화가 필요합니다.
  4. Windows 파일 잠금: Windows에서는 매핑된 파일을 삭제하거나 이름을 변경할 수 없습니다.

결론

  • 작은 파일 (<10MB): 일반 BufferedInputStream/OutputStream 사용
  • 중간~대용량 파일 (10MB~1GB): Java NIO의 MappedByteBuffer 사용
  • 대용량 파일 (1GB+): 청크 단위 Memory-Mapped File 처리
  • Key-Value 저장소: Chronicle Map 사용
  • 데이터 분석: Apache Arrow 사용

0개의 댓글