CodeGym /행동 /JAVA 25 SELF /Future, CompletionHandler: 작업 완료 처리

Future, CompletionHandler: 작업 완료 처리

JAVA 25 SELF
레벨 56 , 레슨 1
사용 가능

1. Future: “준비될 때까지 기다리기”

Java의 비동기 작업은 편지를 우편으로 보내는 것과 비슷합니다. 우체통 앞에서 답장을 기다리지 않고 다른 일을 하러 가죠. 편지가 도착하면 알림만 받으면 됩니다. 코드에서는 작업이 끝났는지 판단하고 결과나 오류를 알아내야 합니다. Java에는 FutureCompletionHandler라는 두 가지 접근 방식이 있습니다.

Future는 나중에 결과를 주겠다는 “영수증”을 받는 것과 같습니다. 때때로 준비가 됐는지 확인할 수도 있고, get()을 호출해 결과를 기다릴 수도 있습니다.

CompletionHandler를 쓰면 아예 기다릴 필요가 없습니다. 미리 핸들러를 지정해 두면, 성공이든 실패든 작업이 끝날 때 자동으로 호출됩니다.

어떻게 동작할까?

AsynchronousFileChannel의 비동기 메서드인 read()write()를 호출할 때, Future<Integer> 타입의 객체를 받을 수 있습니다. 마치 전자 대기표처럼 작업이 시작되고, 아무 때나 “이제 끝났나요?”라고 물어볼 수 있습니다.

메서드 시그니처

Future<Integer> read(ByteBuffer dst, long position)

또는

Future<Integer> write(ByteBuffer src, long position)
  • dst — 데이터를 읽어 올 버퍼.
  • src — 데이터를 쓸 버퍼.
  • position — 파일에서 읽기/쓰기 위치.

예제: Future로 비동기 읽기

파일의 처음 1024바이트를 비동기로 읽고, 명시적으로 결과를 기다리는 코드:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.io.IOException;

public class FutureReadExample {
    public static void main(String[] args) {
        Path path = Path.of("example.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 비동기 읽기 시작
            Future<Integer> future = channel.read(buffer, 0);

            // ... 파일을 읽는 동안 여기서 다른 일을 할 수 있습니다

            // 작업 완료를 기다립니다(블로킹 호출!)
            int bytesRead = future.get(); // InterruptedException, ExecutionException이 발생할 수 있습니다

            System.out.println("읽은 바이트 수: " + bytesRead);

            // 버퍼에서 읽기로 전환
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
        } catch (Exception e) {
            System.err.println("파일 읽기 오류: " + e);
        }
    }
}
  • future.get()은 작업이 끝날 때까지 스레드를 블로킹합니다 — 마치 전화 올 때까지 “전화기 앞에서 기다리는” 것과 같습니다.
  • future.isDone()으로 작업이 끝났는지 확인한 다음에 get()을 호출할 수도 있습니다.
  • 작업이 오류로 끝나면 get()ExecutionException을 던집니다(원인은 그 안에 포함).

언제 Future가 유용할까?

Future는 작업을 시작해 두고, 필요할 때 결과를 기다리고 싶을 때 좋습니다. 예를 들어 파일 읽기, 요청 보내기, 백그라운드 계산 등. 여러 작업을 병렬로 실행해 나중에 결과를 모을 때도 편리합니다.

하지만 진짜 논블로킹 방식으로 자동 완료 알림이 필요하다면 CompletionHandler를 고려하세요.

2. CompletionHandler: “끝나면 불러줘”

CompletionHandler는 여러분이 구현해서 read()write() 메서드에 전달하는 인터페이스입니다. 작업이 끝나거나(또는 오류가 발생하면) Java가 알아서 여러분의 핸들러를 호출합니다. “준비되면 전화 주세요”와 같습니다.

메서드 시그니처

void read(ByteBuffer dst,
          long position,
          A attachment,
          CompletionHandler<Integer, ? super A> handler)
  • dst — 읽기용 버퍼.
  • position — 파일 내 위치.
  • attachment — 핸들러로 전달할 임의의 객체(null 가능, 예: 파일명).
  • handler — 여러분의 핸들러.

CompletionHandler 인터페이스

public interface CompletionHandler<V, A> {
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}
  • completed는 성공 시 호출됩니다. result — 바이트 수, attachment — 여러분이 전달한 객체.
  • failed는 오류 시 호출됩니다. exc — 예외.

예제: CompletionHandler로 비동기 읽기

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;

public class CompletionHandlerReadExample {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("example.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        channel.read(buffer, 0, "example.txt", new CompletionHandler<Integer, String>() {
            @Override
            public void completed(Integer result, String attachment) {
                System.out.println("파일 " + attachment + " 읽기 완료, 바이트 수: " + result);
                buffer.flip();
                while (buffer.hasRemaining()) {
                    System.out.print((char) buffer.get());
                }
                closeChannel();
            }

            @Override
            public void failed(Throwable exc, String attachment) {
                System.err.println("파일 읽기 오류 " + attachment + ": " + exc);
                closeChannel();
            }

            private void closeChannel() {
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("채널 닫기 오류: " + e);
                }
            }
        });

        // 중요: 스레드를 "hold"하지 않으면 main 메서드가 읽기 완료 전에 종료될 수 있습니다!
        // 실제 애플리케이션에서는 보통 스레드가 바로 종료되지 않습니다(예: 서버, UI).
        try {
            Thread.sleep(500); // 비동기 작업에 시간을 조금 줍니다(예제용!)
        } catch (InterruptedException ignored) {}
    }
}

read()에 익명 핸들러를 전달해 완료 후에 무엇을 할지 정의했습니다. completed에서 결과를 받아 처리하고, failed에서는 오류에 대응합니다. 열린 리소스를 남기지 않도록 핸들러 안에서 채널을 닫는 것을 잊지 마세요.

주의할 점: main 메서드가 비동기 작업이 끝나기 전에 종료될 수 있어 예제에는 Thread.sleep(500)을 넣었습니다 — 단지 결과를 보기 위한 것입니다. 실제 애플리케이션에서는 보통 이런 트릭이 필요 없습니다.

언제 CompletionHandler가 더 적합할까?

CompletionHandler는 대기와 블로킹 없이 진정한 비동기가 필요할 때 좋습니다. 작업을 시작하고 완료 시에 무엇을 할지 기술하면 됩니다. UI(JavaFX, Swing)에서 인터페이스가 멈춰 보이지 않도록 하는 데 필수이고, 서버에서도 유용합니다 — 스레드가 대기하지 않고 할 일이 있을 때만 사용됩니다.

4. Future와 CompletionHandler 비교

접근 방식 스레드를 블로킹하나? 언제 사용할까? 예시 시나리오
Future 예( get() 호출 시) 단순 순차 처리 파일 복사, 보고서 생성
CompletionHandler 아니요 진정한 비동기, UI, 서버, 병렬 작업 서버, GUI, 대량 IO
  • Future는 이해하기 쉽지만, 결과를 얻으려면 블로킹이 필요합니다.
  • CompletionHandler는 약간 더 복잡하지만 진정한 비동기를 제공하며 스레드를 블로킹하지 않습니다.

5. 실습: CompletionHandler로 파일에 비동기 쓰기

문자열을 파일에 비동기적으로 기록하고 결과를 알려 주는 예제:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CompletionHandlerWriteExample {
    public static void main(String[] args) throws IOException {
        String text = "안녕, 비동기 세계!";
        ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        Path path = Path.of("async_output.txt");

        AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
            @Override
            public void completed(Integer result, Path attachment) {
                System.out.println("파일 " + attachment + "에 " + result + "바이트를 성공적으로 기록했습니다");
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("채널 닫기 오류: " + e);
                }
            }

            @Override
            public void failed(Throwable exc, Path attachment) {
                System.err.println("파일 쓰기 오류 " + attachment + ": " + exc);
                try {
                    channel.close();
                } catch (IOException e) {
                    System.err.println("채널 닫기 오류: " + e);
                }
            }
        });

        // 결과를 보기 위해 main을 잠깐 붙잡아 둡니다(예제용)
        try {
            Thread.sleep(500);
        } catch (InterruptedException ignored) {}
    }
}
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION