---
# System prepended metadata

title: Handling Large File with AsynchronousFileChannel
tags: [IO, NIO, AsynchronousFileChannel, Asynchronous, Async, 비동기, Channel, 채널, Stream, 스트림, Java, 성능, 자바, Performance]

---

# 대용량 파일을 AsynchronousFileChannel로 다뤄보기

Java 7 에는 비동기 방식의 File I/O를 지원하는 `AsynchronousFileChannel`이 추가되었다.
 
비동기 방식이므로 File I/O에 소요되는 시간 동안 다른 처리를 할 수 있다는 장점이 있다.
 
![](http://i.imgur.com/641EZTR.png)
 
특히 용량이 큰 파일일 수록 File I/O에 소요되는 시간이 클 수 있으므로, 비동기 방식의 장점을 더 살릴 수 있다.
 
# `AsynchronousFileChannel`을 사용하는 일반적인 방법
 
`AsynchronousFileChannel`로 파일을 읽으려면 아래의 메서드를 사용하면 된다.
 
```java
public abstract <A> void read(ByteBuffer dst,
           long position,
           A attachment,
           CompletionHandler<Integer,? super A> handler)
```
 
`CompletionHandler`를 사용하는 대신 `Future`를 반환하는 `read()` 메서드도 있는데, ~~while (!result.isDone()) { ... } 와 같은 식으로 계속 완료 여부를 polling하는 Future 방식보다는~~(`Future`도 `Future.get()`을 사용하면 굳이 polling 하지 않아도 된다.) `CompletionHandler` 방식이 더 간지나므로 `Future` 방식은 여기서는 다루지 않는다.
 
암튼 API를 보니 파일의 `position` 위치에서 부터 읽은 데이터를 `dst` 라는 ByteBuffer에 담고, 성공/실패 시 `CompletionHandler`에 구현된 callback 메서드를 호출하는구나.. 라고 이해되는데, `attachment`는 뭘까? API문서에서도 그냥 아래와 같이 뜬구름 잡는 소리만 있다.
 
>attachment - The object to attach to the I/O operation; can be null
 
`attachment`가 뭔지 궁금하지만, `null` 일 수도 있다고 하니 일단 그냥 `null`로 둬보자.
 
## 일반적인 코드
 
nio의 `Channel`은 데이터의 I/O에 `byte[]` 대신 `Buffer`를 사용한다. `AsynchronousFileChannel`도 마찬가지로 `ByteBuffer`를 사용하며, `AsynchronousFileChannel`를 이용해서 파일을 읽어들이는 코드는 다음과 같다.
 
```java
private void asyncFileChannelTest(Path sourceFilePath, boolean isWrite) throws IOException {
 
    try (
            AsynchronousFileChannel asyncFileChannel = AsynchronousFileChannel.open(
                sourceFilePath,
                StandardOpenOption.READ
            );
    ) {
        System.err.println("AsynchronousFileChannel 테스트 시작");
 
        long startTime = System.nanoTime();
 
        long fileSize = asyncFileChannel.size();
 
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)fileSize);
 
        System.err.println("AsynchronousFileChannel.read() 호출");
 
        asyncFileChannel.read(
            byteBuffer, 0, null,
            new CompletionHandler<Integer, Object>() {
 
                @Override
                public void completed(Integer result, Object object) {
                    if (result == -1) {
                        long endTime = System.nanoTime();
                        System.err.println("비정상 종료 : " + (endTime - startTime) + " ns elapsed.");
                        return;
                    }
 
                    byteBuffer.flip();
                    byteBuffer.mark();
                    if (isWrite) System.out.write(byteBuffer.array(), 0, result);
                    byteBuffer.reset();
 
                    long endTime = System.nanoTime();
                    System.err.println("AsynchronousFileChannel.read() 완료 : " + (endTime - startTime) + " ns elapsed.");
                }
 
                @Override
                public void failed(Throwable exc, Object object) {
                    exc.printStackTrace();
                }
            }
        );
 
        System.err.println("AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱");
        System.err.println("그동안 그리스에도 다녀오고");
        System.err.println("크로아티아에도 갔다오자");
    }
}
```
 
실행하면 다음과 같은 결과가 나온다.
 
```
AsynchronousFileChannel 테스트 시작
AsynchronousFileChannel.read() 호출
AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱
그동안 그리스에도 다녀오고
크로아티아에도 갔다오자
AsynchronousFileChannel.read() 완료 : 320668276 ns elapsed.
```
 
위에서 보는 것처럼 File I/O가 처리되는 시간 동안에도 메시지를 콘솔에 출력하는 것처럼 다른 작업을 수행할 수 있다.
 
한 가지 마음에 걸리는 것은 `ByteBuffer`의 크기를 파일 사이즈와 같게 한다는 점이다. 용량이 클수록 비동기 방식의 장점이 더 드러난다고는 하지만, 용량이 기가 단위로 정말 거대한 파일을 통으로 메모리에 모두 담아 처리하면 OutOfMemoryError를 유발할 수도 있다. 
 
![](http://i.imgur.com/26xMoli.png)
 
위의 그래프는 Java SDK에 번들로 제공되는 VisualVM을 사용해서 메모리 사용을 모니터링한 것인데, 160메가 정도의 파일로 테스트 해본 결과 위와 같이 메모리 사용이 쭈욱~ 올라간 것을 알 수 있다. 기가 단위의 파일이라면 역시나 OutOfMemoryError가 발생할 것 같다.
 
참고로 일반적인 `main()`으로 실행하면 금방 종료되어 위처럼 캡쳐 등이 불편해서, 편의상 간단하게 SpringBoot로 만든 웹 애플리케이션에서 테스트를 진행했다.
 
암튼 메모리 사용량을 적게 할 수 있도록 작은 크기의 `ByteBuffer`를 생성해서 이를 재활용하는 것이 좋겠다.
 
# 작은 크기의 ByteBuffer을 재사용해서 OutOfMemoryError 를 막는 방법
 
파일 크기 만큼의 `ByteBuffer`를 사용하는 대신 작은 크기의 `ByteBuffer`에 데이터를 담는 일을 반복하면 OutOfMemoryError 걱정없이도 파일 내용을 읽을 수 있다. 그런데 그냥 쉽게 되는 것은 아니고 손 봐줘야 할 곳이 몇 군데 있다. 
 
## 반복 문제
 
비동기 방식이라 Handler를 통해서 구현하므로 통상적인 방법처럼 단순하게 `while` 문으로는 해결할 수 없다.
 
그렇다고 엄청난 고도의 방법이 필요한 것은 아니다. `while`을 쓰는 대신 `CompletionHander`내에서 다시 `asynchronousFileChannel.read()`를 호출하게 하면 될 것 같다.
 
## 읽을 위치 지정 문제
 
`asynchronousFileChannel.read()`를 다시 호출할 때 주의해야할 점이 있다. 바로 `asynchronousFileChannel.read()`의 두번째 파라미터인 `position` 값의 적절한 설정이다. `ByteBuffer` 사이즈만큼 읽은 후에 다시 읽을 때는, 앞에서 읽고난 위치에서부터 다시 읽기 시작해야  중복 또는 누락 없이 정확하게 파일 내용을 읽을 수 있다. 
 
이 역시도 어려운 문제는 아니다. `ByteBuffer` 크기에 반복회수를 곱해준 값을 두번째 파라미터로 넘겨주면 되겠다. `long` 타입의 `iterations`라는 변수로 반복회수를 관리하자.
 
그런데, 한가지 문제가 있다. 반복회수는 익명 내부 클래스인`CompletionHandler` 내의 `completed()` 메서드 내에서 증가시켜야 하는데, 내부 클래스의 메서드에서는 바깥 클래스의 변수의 값을 직접 변경할 수 없다. 그래서 단순히 primitive 타입의 `iterations`을 사용하면 내부 클래스의 메서드에서 증가시킬 수 없으므로, primitive 타입인 `long` 대신에 객체인 `Long` 타입을 쓰면 값을 변경할 수 있다. 하지만 이 `Long` 타입의 변수를 `CompletionHandler`에게 어떻게 넘겨주지?
 
## 아하 attachment
 
그렇다. 우리에겐 미지의 파라미터인 `attachment`가 있었다.
 
Java API Doc에 뜬구름 잡는 설명만 있었던 `attachment`는 이럴 때 쓰라는 넘이었구나.. `asynchronousFileChannel.read()`의 세번째 파라미터에 `null` 대신 Long 타입의 iterations라는 변수를 넘겨주자.
 
그럼 대략 아래와 같이 코드가 바뀐다. 바뀐 부분에는 주석을 추가했다.
 
```java
private void asyncFileChannelTest(Path sourceFilePath, boolean isWrite) throws IOException {
 
    try (
            AsynchronousFileChannel asyncFileChannel = AsynchronousFileChannel.open(
                sourceFilePath,
                StandardOpenOption.READ
            );
    ) {
        System.err.println("AsynchronousFileChannel 테스트 시작");
 
        long startTime = System.nanoTime();
 
        long fileSize = asyncFileChannel.size();
 
        // ByteBuffer 크기를 8k로 축소
        ByteBuffer byteBuffer = ByteBuffer.allocate(8 * 1024);
 
        // 반복 회수 관리용 변수
        Long iterations = 0L;
 
        System.err.println("AsynchronousFileChannel.read() 호출");
 
        asyncFileChannel.read(
            byteBuffer, 0, iterations,    // null 대신 iterations 전달
            new CompletionHandler<Integer, Long>() {    // 타입 파라미터에 Object 대신 Long 전달
 
                @Override
                public void completed(Integer result, Long iterations) {    // 타입 파라미터에 Object 대신 Long
                    if (result == -1) {
                        long endTime = System.nanoTime();
                        System.err.println("비정상 종료 : " + (endTime - startTime) + " ns elapsed.");
                        return;
                    }
 
                    // 반복 회수 확인
                    System.err.println((iterations + 1) + "회차 반복");
 
                    byteBuffer.flip();
                    byteBuffer.mark();
                    if (isWrite) System.out.write(byteBuffer.array(), 0, result);
                    byteBuffer.reset();
 
                    // 읽어들인 바이트수가
                    // 파일사이즈와 같거나(버퍼 크기와 파일 크기가 같은 경우)
                    // 버퍼 사이즈보다 작다면 파일의 끝까지 읽은 것이므로 종료 처리
                    if (result == fileSize || result < byteBuffer.capacity()) {
                        long endTime = System.nanoTime();
                        System.err.println("AsynchronousFileChannel.read() 완료 : " + (endTime - startTime) + " ns elapsed.");
                        return;
                    }
                    // 읽을 내용이 남아있으므로 반복 회수를 증가 시키고 다시 읽는다.
                    iterations++;
                    asyncFileChannel.read(byteBuffer, result * iterations, iterations, this);
                }
 
                @Override
                public void failed(Throwable exc, Long iterations) {    // 타입 파라미터에 Object 대신 Long
                    exc.printStackTrace();
                }
            }
        );
 
        System.err.println("AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱");
        System.err.println("그동안 그리스에도 다녀오고");
        System.err.println("크로아티아에도 갔다오자");
    }
}
```
 
자 이제 실행해보면 메모리를 적게 쓰면서도 비동기 방식으로 파일 내용을 읽어올 것이다. 생각만해도 흐뭇하다. 얼른 실행해보자.
 
```
AsynchronousFileChannel 테스트 시작
AsynchronousFileChannel.read() 호출
AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱
그동안 그리스에도 다녀오고
크로아티아에도 갔다오자
1회차 반복
java.nio.channels.ClosedChannelException
	at sun.nio.ch.SimpleAsynchronousFileChannelImpl.implRead(SimpleAsynchronousFileChannelImpl.java:302)
	at sun.nio.ch.AsynchronousFileChannelImpl.read(AsynchronousFileChannelImpl.java:229)
	... 이하 생략 ...
```
 
ㅋㅋㅋ 한 방에 될리가.. 에러 메시지를 보니 채널이 이미 닫혀있다고 한다. 응? 나 채널 닫은 적 없는데..
 
## 채널이 닫힌 이유
 
어디서 닫혔을까 하고 코드를 보니 `AsynchronousFileChannel`을 가져올 때, 자원 해제의 편의를 위해 Java 7에 도입된 `try-with-resources` 구문을 사용했다. 채널이 닫힌 이유를 알 것 같다. 
 
파일 내용을 `ByteBuffer`에 성공적으로 한 번 읽어들인 후에 `CompletionHandler`내에서 다시 호출하는 `read()`는 `System.err.println("크로아티아에도 갔다오자");`가 실행되고 난 시점에 호출되는데, 이 시점에는 이미 `try-with-resources`를 빠져나온 후가 된다. 따라서 `try-with-resources`에 의해 자동으로 자원이 해제되면서 `AsynchronousFileChannel`가 닫히고, 그 다음에 이미 닫혀있는 `AsynchronousFileChannel`의 `read()`를 다시 호출하니까 채널이 이미 닫혀있다는 에러가 발생한 것이다.
 
원인은 알겠는데 해결은 또 어떻게 해야하나.
 
## try-catch 적용
 
일단 `AsynchronousFileChannel`를 닫는 처리를 프로그래머가 직접 제어할 수 있도록 `try-with-resources` 부터 걷어내는 것이 순서일 것 같다. 그대신 적절한 위치에서 명시적으로 `AsynchronousFileChannel.close()`를 호출해서 자원 해제 처리를 확실히 해주면, 채널이 닫히지 않은 채로 비동기 방식의 반복을 수행하고, 적절한 위치에서 채널을 닫는 처리가 가능할 것 같다. 
 
코드는 아래와 같이 바뀐다. 이번에 바뀐 내용은 `//// 2차 변경`와 같이 슬래쉬 4개로 주석처리했다.
 
```java
private void asyncFileChannelTest(Path sourceFilePath, boolean isWrite) throws IOException {
 
    //// try-with-resource 대신 try-catch-finally 적용
    try {
        System.err.println("AsynchronousFileChannel 테스트 시작");
 
        AsynchronousFileChannel asyncFileChannel = AsynchronousFileChannel.open(
                sourceFilePath,
                StandardOpenOption.READ
        );
 
        long startTime = System.nanoTime();
 
        long fileSize = asyncFileChannel.size();
 
        // ByteBuffer 크기를 8k로 축소
        ByteBuffer byteBuffer = ByteBuffer.allocate(8 * 1024);
 
        // 반복 회수 관리용 변수
        Long iterations = 0L;
 
        System.err.println("AsynchronousFileChannel.read() 호출");
 
        asyncFileChannel.read(
                byteBuffer, 0, iterations,    // null 대신 iterations 전달
                new CompletionHandler<Integer, Long>() {    // 타입 파라미터에 Object 대신 Long 전달
 
                    @Override
                    public void completed(Integer result, Long iterations) {    // 타입 파라미터에 Object 대신 Long
                        if (result == -1) {
                            long endTime = System.nanoTime();
                            System.err.println("비정상 종료 : " + (endTime - startTime) + " ns elapsed.");
 
                            //// asyncFileChannel 닫기
                            closeAsyncFileChannel(asyncFileChannel);
 
                            return;
                        }
 
                        // 반복 회수 확인
                        System.err.println((iterations + 1) + "회차 반복");
 
                        byteBuffer.flip();
                        byteBuffer.mark();
                        if (isWrite) System.out.write(byteBuffer.array(), 0, result);
                        byteBuffer.reset();
 
                        // 읽어들인 바이트수가
                        // 파일사이즈와 같거나(버퍼 크기와 파일 크기가 같은 경우)
                        // 버퍼 사이즈보다 작다면 파일의 끝까지 읽은 것이므로 종료 처리
                        if (result == fileSize || result < byteBuffer.capacity()) {
                            long endTime = System.nanoTime();
                            System.err.println("AsynchronousFileChannel.read() 완료 : " + (endTime - startTime) + " ns elapsed.");
 
                            //// asyncFileChannel 닫기
                            closeAsyncFileChannel(asyncFileChannel);
 
                            return;
                        }
                        // 읽을 내용이 남아있으므로 반복 회수를 증가 시키고 다시 읽는다.
                        iterations++;
                        asyncFileChannel.read(byteBuffer, result * iterations, iterations, this);
                    }
 
                    @Override
                    public void failed(Throwable exc, Long iterations) {    // 타입 파라미터에 Object 대신 Long
                        exc.printStackTrace();
 
                        //// asyncFileChannel 닫기
                        closeAsyncFileChannel(asyncFileChannel);
                    }
                }
        );
 
        System.err.println("AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱");
        System.err.println("그동안 그리스에도 다녀오고");
        System.err.println("크로아티아에도 갔다오자");
 
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException(e);    //// 상황에 맞는 예외 처리 필요
    }
}
 
//// asyncFileChannel 닫기
private void closeAsyncFileChannel(AsynchronousFileChannel asyncFileChannel) {
 
    if (asyncFileChannel != null && asyncFileChannel.isOpen()) {
 
        try {
            asyncFileChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);    //// 상황에 맞는 예외 처리 필요
        }
    }
}
```
 
실행하면 드디어!! 다음과 같이 정상적으로 표시된다.
 
```
AsynchronousFileChannel 테스트 시작
AsynchronousFileChannel.read() 호출
AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱
그동안 그리스에도 다녀오고
크로아티아에도 갔다오자
1회차 반복
2회차 반복
3회차 반복
...
19496회차 반복
19497회차 반복
AsynchronousFileChannel.read() 완료 : 844209043 ns elapsed.
```
 
메모리 사용량을 확인해보면 아래와 같이 거의 미미한 수준의 변화만 있을 뿐이다.
 
![](http://i.imgur.com/V5ub1z4.png)
 
## attachment의 활용
 
앞에서 단순히 `Long` 타입의 변수를 `attachment`로 활용해서 반복 회수를 관리했는데, `attachment`에 다른 객체를 사용하면 더 많은 정보를 관리할 수 있다. 예를 들어, 읽어들인 바이트수를 `attachment`에 사용된 객체에 계속 누적하면, 파일 읽기 완료 후 읽어들인 바이트수와 실제 파일 크기를 비교할 수도 있다.
 
코드는 다시 아래와 같이 바뀐다. 이번에 바뀌는 부분은 슬래쉬 6개로 주석 처리했다.
 
```java
private void asyncFileChannelTest(Path sourceFilePath, boolean isWrite) throws IOException {
 
        //// try-with-resource 대신 try-catch-finally 적용
        try {
            System.err.println("AsynchronousFileChannel 테스트 시작");
 
            AsynchronousFileChannel asyncFileChannel = AsynchronousFileChannel.open(
                    sourceFilePath,
                    StandardOpenOption.READ
            );
 
            long startTime = System.nanoTime();
 
            long fileSize = asyncFileChannel.size();
 
            // ByteBuffer 크기를 8k로 축소
            ByteBuffer byteBuffer = ByteBuffer.allocate(8 * 1024);
 
            ////// attachment 용 객체
            class AsyncIOResultInfo {
                long iterations = 0L;
                long totalBytesRead = 0L;
            }
            AsyncIOResultInfo asyncIOResultInfo = new AsyncIOResultInfo();
 
            System.err.println("AsynchronousFileChannel.read() 호출");
 
            asyncFileChannel.read(
                    byteBuffer, 0, asyncIOResultInfo,    ////// iterations 대신 asyncIOResultInfo 전달
                    new CompletionHandler<Integer, AsyncIOResultInfo>() {    ////// 타입 파라미터에 Long 대신 AsyncIOResultInfo 전달
 
                        @Override
                        public void completed(Integer result, AsyncIOResultInfo asyncIOResultInfo) {    ////// Long 대신 AsyncIOResultInfo 전달
                            if (result == -1) {
                                long endTime = System.nanoTime();
                                System.err.println("비정상 종료 : " + (endTime - startTime) + " ns elapsed.");
 
                                //// asyncFileChannel 닫기
                                closeAsyncFileChannel(asyncFileChannel);
 
                                return;
                            }
 
                            // 반복 회수 확인
                            System.err.println((asyncIOResultInfo.iterations + 1) + "회차 반복");    ////// iterations 대신 asyncIOResultInfo.iterations
 
                            ////// 읽어들인 바이트수 누적
                            asyncIOResultInfo.totalBytesRead += result;
 
                            byteBuffer.flip();
                            byteBuffer.mark();
                            if (isWrite) System.out.write(byteBuffer.array(), 0, result);
                            byteBuffer.reset();
 
                            // 읽어들인 바이트수가
                            // 파일사이즈와 같거나(버퍼 크기와 파일 크기가 같은 경우)
                            // 버퍼 사이즈보다 작다면 파일의 끝까지 읽은 것이므로 종료 처리
                            if (result == fileSize || result < byteBuffer.capacity()) {
                                long endTime = System.nanoTime();
                                System.err.println("AsynchronousFileChannel.read() 완료 : " + (endTime - startTime) + " ns elapsed.");
 
                                ////// 총 읽어들인 바이트수 비교
                                System.err.println("fileSize       : " + fileSize);
                                System.err.println("totalBytesRead : " + asyncIOResultInfo.totalBytesRead);
 
                                //// asyncFileChannel 닫기
                                closeAsyncFileChannel(asyncFileChannel);
 
                                return;
                            }
                            // 읽을 내용이 남아있으므로 반복 회수를 증가 시키고 다시 읽는다.
                            ////// iterations 대신 asyncIOResultInfo.iterations
                            asyncIOResultInfo.iterations++;
                            asyncFileChannel.read(byteBuffer, result * asyncIOResultInfo.iterations, asyncIOResultInfo, this);
                        }
 
                        @Override
                        public void failed(Throwable exc, AsyncIOResultInfo iterations) {    ////// Long 대신 AsyncIOResultInfo 전달
                            exc.printStackTrace();
 
                            //// asyncFileChannel 닫기
                            closeAsyncFileChannel(asyncFileChannel);
                        }
                    }
            );
 
            System.err.println("AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱");
            System.err.println("그동안 그리스에도 다녀오고");
            System.err.println("크로아티아에도 갔다오자");
 
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);    //// 상황에 맞는 예외 처리 필요
        }
    }
 
    //// asyncFileChannel 닫기
    private void closeAsyncFileChannel(AsynchronousFileChannel asyncFileChannel) {
 
        if (asyncFileChannel != null && asyncFileChannel.isOpen()) {
 
            try {
                asyncFileChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e);    //// 상황에 맞는 예외 처리 필요
            }
        }
    }
```
 
실행해보면 아래와 같이 전체 읽어들인 바이트수와 파일 크기도 함께 표시할 수 있다.
 
```
AsynchronousFileChannel 테스트 시작
AsynchronousFileChannel.read() 호출
AsyncFileChannel I/O 진행 중에는 다른 작업도 할 수 있지롱
그동안 그리스에도 다녀오고
크로아티아에도 갔다오자
1회차 반복
2회차 반복
3회차 반복
...
19496회차 반복
19497회차 반복
AsynchronousFileChannel.read() 완료 : 683431718 ns elapsed.
fileSize       : 159718093
totalBytesRead : 159718093
```
 
 
# 기존 방식과의 비교
 
## 속도
 
동기와 비동기는 그 작업 자체의 속도 비교보다는 대기 시간 동안 다른 작업 처리가 가능하냐 마냐가 중요하므로, 처리 속도가 중요한 것은 아니지만 그래도 궁금하니까 기존 방식이랑 한 번 비교해보자.
 
980메가 정도의 파일로 비교해봤다. 버퍼를 사용할 수 있는 부분에서는 8k 를 적용했다. 참고로 테스트용 대용량 텍스트 파일은 [여기](https://dumps.wikimedia.org/mediawikiwiki/20160720/)에서 내려받을 수 있다.
 
![](http://i.imgur.com/hNq09Zb.png)
 
`AsynchronousFileChannel`은 약 2.75초 정도 소요되었고, 가장 빠른 `FileChannel`과 `BufferedInputStream`은 0.5초 이내, `BufferedReader`와 `InputStreamReader`는 4\~5초 정도 소요되었다.

텍스트를 읽을 때 간편해서 가장 많이 사용하는 `BufferedReader.readLine()`이 아무래도 8k 보다는 현저히 바이트수가 작을 행 단위로 반복을 하므로 반복 회수가 가장 많을테고, 따라서 가장 느릴 거라고 예상은 했지만 이 정도 차이라면 상당히 놀랍다. 작은 파일은 관계없겠지만 대용량 파일에서는 `BufferedReader.readLine()`는 피하는 것이 좋을 것 같다.
 
## 자원 사용

아래 그림은 다음과 같이 6번의 테스트 수행 시  CPU와 메모리 변화 추이를 나타내고 있다.

1. AsynchronousFileChannel.read()
1. FileChannel.read()
1. BufferedInputStream.read()
1. BufferedReader.read()
1. InputStreamReader.read()
1. BufferedReader.readLine()

`BufferedReader.readLine()`를 실행할 때만 메모리 사용량이 급격히 증가한다. 처리 속도 뿐아니라 메모리 사용 측면에서도 `BufferedReader.readLine()`는 대용량 파일에서는 사용하지 않는 것이 좋을 것 같다.
 
![](http://i.imgur.com/86OemjJ.png)
 
# 마무리

:::info
- **대용량 파일도 `AsynchronousFileChannel`을 활용해서 비동기 방식으로 I/O를 처리할 수 있다.**
- 하지만 OutOfMemoryError가 발생하지 않도록 **적당한 크기의 `ByteBuffer`로 쪼개서 반복 처리**하는 것이 좋다.
- **반복 처리 방식, 파일 읽을 위치 지정, 채널 자원의 해제, attachment의 사용** 등 신경써야 할 부분이 있다.
- **대용량 파일에서는 `BufferedReader.readLine()`는 사용하지 않는 것이 좋다.**
:::

 
 
# 더 읽을 거리
 
- jenkov tutorials : http://tutorials.jenkov.com/java-nio/asynchronousfilechannel.html
- java NIO : http://www.slideshare.net/allnewangel/java-nio-23150417
- java nio intro : https://tutorials.techmytalk.com/2014/11/03/java-nio-introduction-2/
- java file I/O tutorials : https://docs.oracle.com/javase/tutorial/essential/io/fileio.html
- API docs
	- https://docs.oracle.com/javase/7/docs/api/java/nio/channels/AsynchronousFileChannel.html
	- https://docs.oracle.com/javase/7/docs/api/java/nio/channels/CompletionHandler.html
	- https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html
- Async IO 개념 정리 : http://djkeh.github.io/articles/Boost-application-performance-using-asynchronous-IO-kor/
 
# 더 해볼 거리
 
- `ByteBuffer`를 사용할 때마다 생성하지 말고 Pooling 하면 속도가 더 나아질 것 같다.
- `ByteBuffer`의 크기를 바꿔가면서 `FileChannel`, `BufferedInputStream`와 속도 비교를 해보는 것도 좋을 것 같다.
- `attachment`를 잘 활용하면 예외 발생 처리 시 처음부터 모두 다시 읽지 않고 성공적으로 읽기를 마친 위치부터 다시 읽도록 효율화도 할 수 있을 것 같다.
- `ByteBuffer` 대신 `Direct ByteBuffer`로 테스트 해보는 것도 재미있을 것 같다.