Java IO 성능 개선
내용 핵심
버퍼를 활용해 I/O를 최대한 줄이자
첫번째 블로그 내용 정리
Java의 입출력 성능 개선에 대한 1997년 글 요약
1997년 당시, Java의 입출력(I/O) 성능은 많은 애플리케이션에서 병목 현상을 일으켰습니다. 그 이유는 JDK 1.0.2의
java.io패키지가 잘못 설계되고 구현되었기 때문입니다. 특히 문제였던 부분은 버퍼링(buffering) 문제였습니다.java.io의 대부분의 클래스는 버퍼링이 되어 있지 않았습니다. 유일하게 버퍼링이 되어 있는 클래스는BufferedInputStream과BufferedOutputStream이었지만, 이 클래스들은 제공하는 메소드가 매우 제한적이었습니다. 예를 들어, 파일을 한 줄씩 읽어야 하는 경우, 유일하게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 을 사용하려면 자체 버퍼링을 설정하고 바이트에 대한 읽기 메서드를 직접 구현하는 속도가 빠르다RandomAccessFile 클래스는 파일에서 바이트 단위로 랜덤 접근 I/O를 수행할 수 있는 자바 클래스입니다. 이 클래스는 파일 포인터를 임의의 위치로 이동시키는 seek 메서드를 제공합니다. seek 메서드를 사용하면 특정 위치에서 바이트를 읽거나 쓸 수 있습니다. 랜덤 접근의 문제점은 비용이 크다는 것입니다. seek 메서드는 기본 런타임 시스템에 접근하므로 비용이 많이 듭니다. 대안으로, 랜덤 접근 파일 위에 자체적인 버퍼링을 설정하고 직접 바이트를 읽는 read 메서드를 구현하는 것이 더 저렴합니다.
캐싱 이용하기
ArrayList에 저장한 후 읽어오기
궁금
적정 버퍼 사이즈 찾는 방법은? 파일 블록 사이즈를 본다?
Last updated