Java IO 성능 개선

내용 핵심

  • 버퍼를 활용해 I/O를 최대한 줄이자

첫번째 블로그 내용 정리

Java의 입출력 성능 개선에 대한 1997년 글 요약

  • 1997년 당시, Java의 입출력(I/O) 성능은 많은 애플리케이션에서 병목 현상을 일으켰습니다. 그 이유는 JDK 1.0.2java.io 패키지가 잘못 설계되고 구현되었기 때문입니다. 특히 문제였던 부분은 버퍼링(buffering) 문제였습니다. java.io의 대부분의 클래스는 버퍼링이 되어 있지 않았습니다. 유일하게 버퍼링이 되어 있는 클래스는 BufferedInputStreamBufferedOutputStream이었지만, 이 클래스들은 제공하는 메소드가 매우 제한적이었습니다. 예를 들어, 파일을 한 줄씩 읽어야 하는 경우, 유일하게 readLine 메소드를 제공하는 클래스는 DataInputStream이었습니다. 그러나 이 클래스는 내부 버퍼가 없어서 파일을 한 문자씩 읽어야 했습니다. 이는 큰 파일을 읽을 때 매우 비효율적이었습니다.

  • JDK 1.1에서는 Reader와 Writer 클래스를 추가하여 입출력 성능을 개선했습니다. BufferedReader의 readLine 메소드는 큰 파일을 읽을 때 DataInputStream의 readLine 메소드보다 최소 10~20배 빠릅니다. 하지만 JDK 1.1에서도 모든 성능 문제가 해결되지는 않았습니다. 예를 들어, RandomAccessFile 클래스는 큰 파일을 메모리에 모두 읽지 않고도 분석할 수 있어 매우 유용하지만, 여전히 버퍼링이 되어 있지 않았고, 이를 대체할 Reader 클래스도 제공되지 않았습니다.

  • 파일 입출력 문제해결하기 위해서는 버퍼링된 RandomAccessFile 클래스가 필요했습니다. 이를 위해 RandomAccessFile 클래스를 상속받아 새로운 Braf(BufferedRandomAccessFile) 클래스를 만들었습니다. 이 클래스는 RandomAccessFile의 모든 메소드를 재사용하며, 추가적인 버퍼 크기를 지정할 수 있는 새로운 생성자를 추가했습니다.

두번째 블로그 내용 정리

오라클의 Java I/O 성능 튜닝 기술 요약

이 글은 Java의 I/O 성능을 향상시키기 위한 다양한 기술을 논의하고 설명하는 내용을 담고 있습니다. 주로 디스크 파일 I/O를 튜닝하는 방법에 대해 다루고 있지만, 네트워크 I/O와 윈도우 출력에도 적용될 수 있는 기술들도 포함하고 있습니다. 이 글에서는 저수준 I/O 문제부터 압축, 포맷팅, 직렬화 등의 고수준 문제까지 다루고 있습니다.

Java I/O를 논할 때, Java 프로그래밍 언어가 두 가지 유형의 디스크 파일 조직을 가정한다는 점을 주목할 필요가 있습니다. 하나는 바이트 스트림을 기반으로 하고, 다른 하나는 문자 시퀀스를 기반으로 합니다. Java에서는 문자가 두 바이트로 표현되기 때문에, 파일에서 문자를 읽을 때 일부 번역이 필요합니다.

  • 저수준 I/O 문제

    • 디스크 접근 최소화: 디스크 접근을 줄이는 것이 I/O 속도를 높이는 기본 규칙입니다.

    • 운영 체제 접근 최소화: 기본 시스템 호출을 피합니다.

    • 메소드 호출 최소화: 메소드 호출을 줄입니다. 예를 들어, 직접 버퍼를 사용하면 BufferedInputStream 를 사용 했을때보다 메소드 호출이 줄어들어 성능적 이점을 갖는다 (아래 벤치마킹 참고).

    • 바이트와 문자를 개별적으로 처리하지 않기: 데이터 스트림에서 각 바이트나 문자를 하나씩 읽고 처리하는 대신, 큰 덩어리로 읽어 한 번에 처리하라는 뜻입니다. 이렇게 하면 성능이 크게 향상됩니다.

  • 고수준 I/O 문제

    • 버퍼링: 큰 버퍼를 사용하여 디스크 접근을 최소화합니다.

    • 압축: 파일 크기를 줄여 I/O 시간을 단축할 수 있습니다.

    • 캐싱: 자주 접근하는 데이터를 메모리에 저장해 I/O 성능을 향상시킵니다.

    • 포맷팅 비용: 데이터를 파일에 쓰는 것 외에도 포맷팅 비용이 크므로 이를 최적화해야 합니다.

버퍼가 크면 무조건 좋나? Java 버퍼의 길이는 일반적으로 기본적으로 1024 또는 2048바이트입니다. 이보다 큰 버퍼는 I/O 속도를 높이는 데 도움이 될 수 있지만 종종 5~10% 정도만 향상됩니다.

버퍼 사용시 속도 얼마나 빠를까?

1MB 파일 읽을때 속도 비교

  • 완전 기본 방식 (FileInputStream): 6.9초

  • 버퍼 스트림 객체 (BufferedInputStream): 0.9초

  • 직접 버퍼 (new byte[2048]) : 0.4초

메모리만 충분하다면야

파일의 크기를 미리 알고 메모리에 로드해서 사용하면 빠릅니다.

라인 버퍼링 비활성화

기본적으로 System.out은 라인 버퍼링이 되어 있어, 새로운 줄 문자를 만날 때마다 출력 버퍼가 플러시됩니다. 이 방식은 사용자 인터랙션에 중요하지만, 라인 버퍼링을 비활성화하면 성능을 더 높일 수 있습니다.

아래 예제는 1부터 100000까지의 정수를 출력하며, 라인 버퍼링이 활성화된 기본 출력보다 약 3배 더 빠릅니다.

텍스트 파일을 읽을때는 DataInputStream 보다는 BufferedReader 사용하기

문자를 파일에서 읽을 때 DataInputStream.readLine 사용시 메서드 호출 오버헤드가 문제가 될 수 있습니다 (1개 char 마다 read 호출 일어남). 6MB 텍스트 파일을 읽을때 두 번째 프로그램이 첫 번째 프로그램보다 약 20% 더 빠릅니다. 또한, DataInputStream 는 바이트에서 문자 변환 문제있습니다. 이 클래스는 ASCII 문자만 제대로 처리할 수 있고, 자바의 기본 문자 집합인 유니코드를 제대로 처리하지 못합니다.

"recall that the Java language uses the Unicode character set, not ASCII"

  • 자바는 기본적으로 유니코드를 사용하여 문자를 표현합니다. 자바의 char 타입은 16비트 유니코드 문자를 나타냅니다. 자바는 다양한 문자 인코딩을 지원하며, 주로 사용하는 인코딩 방식은 UTF-8입니다.

  • UTF-8, UTF-16 등 여러 인코딩 방식이 있으며, 각각의 인코딩 방식은 유니코드 코드 포인트를 바이트 시퀀스로 변환하는 방법을 정의합니다

  • ASCII:

    • 제한된 수의 문자(128개)만 표현 가능

    • 주로 단일 바이트(1바이트)로 표현

    • 파일에서 읽거나 쓸 때 특별한 인코딩이 필요하지 않음

    유니코드:

    • 전 세계의 모든 문자를 표현 가능

    • 다양한 인코딩 방식(UTF-8, UTF-16 등)을 통해 표현

    • 파일에서 읽거나 쓸 때 인코딩 방식을 지정해야 함

ASCII 파일 읽기 (바이트 스트림 사용):

유니코드 파일 읽기 (문자 스트림 사용):

보충 코드

유니코드 문자를 제대로 출력하지 못하는 프로그램:

유니코드 문자를 제대로 출력 가능:

RandomAccessFile 을 사용하려면 자체 버퍼링을 설정하고 바이트에 대한 읽기 메서드를 직접 구현하는 속도가 빠르다

RandomAccessFile 클래스는 파일에서 바이트 단위로 랜덤 접근 I/O를 수행할 수 있는 자바 클래스입니다. 이 클래스는 파일 포인터를 임의의 위치로 이동시키는 seek 메서드를 제공합니다. seek 메서드를 사용하면 특정 위치에서 바이트를 읽거나 쓸 수 있습니다. 랜덤 접근의 문제점은 비용이 크다는 것입니다. seek 메서드는 기본 런타임 시스템에 접근하므로 비용이 많이 듭니다. 대안으로, 랜덤 접근 파일 위에 자체적인 버퍼링을 설정하고 직접 바이트를 읽는 read 메서드를 구현하는 것이 더 저렴합니다.

캐싱 이용하기

ArrayList에 저장한 후 읽어오기

궁금

  • 적정 버퍼 사이즈 찾는 방법은? 파일 블록 사이즈를 본다?

Last updated