CodeGym /행동 /JAVA 25 SELF /비동기 I/O에서 오류 처리, 작업 취소

비동기 I/O에서 오류 처리, 작업 취소

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

1. 비동기 작업에서의 오류 처리

동기 코드에서는 간단합니다. 파일을 찾을 수 없거나 접근 권한이 없으면 바로 try-catch에서 예외를 잡으면 됩니다. 하지만 비동기 코드, 특히 콜백(CompletionHandler)을 사용할 때는 오류가 여러분의 메서드가 끝난 뒤 — 스레드 풀의 어딘가에서 — 발생할 수 있습니다. 이를 올바르게 처리하지 않으면 프로그램이 예측 불가능하게 동작할 수 있습니다. 데이터가 “조용히” 유실되거나 애플리케이션 전체가 다운될 수도 있습니다.

오류는 CompletionHandler에 어떻게 전달될까요?

인터페이스 CompletionHandler<V, A>에는 두 가지 메서드가 있습니다:

  • completed(V result, A attachment) — 작업이 성공적으로 완료되면 호출됩니다.
  • failed(Throwable exc, A attachment) — 오류가 발생하면 호출됩니다.

사용 예:

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

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("성공적으로 " + result + " 바이트를 읽었습니다");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("파일 읽기 오류: " + exc.getMessage());
                    // 로그를 남기거나, 사용자에게 알리거나, 예외를 상위로 전달할 수 있습니다
                }
            });
        } catch (IOException ex) {
            System.out.println("파일 열기 오류: " + ex.getMessage());
        }

        // 비동기 작업이 끝날 시간을 잠시 줍니다 (실제 애플리케이션에서는 CountDownLatch 등 다른 메커니즘을 사용하세요)
        Thread.sleep(500);
    }
}

여기서 무슨 일이 일어나나요?

  • 파일이 존재하지 않으면 failed 메서드가 해당 예외(NoSuchFileException)와 함께 호출됩니다.
  • 작업이 성공적으로 끝나면 completed가 호출됩니다.

자주 발생하는 오류 예

  • 파일을 찾을 수 없음: NoSuchFileException
  • 권한 없음: AccessDeniedException
  • 읽기/쓰기 오류: IOException의 다양한 하위 클래스
  • 버퍼 관련 문제: BufferOverflowException, BufferUnderflowException

로깅과 사용자 알림

비동기 콜백에서의 오류는 당황할 일이 아니지만, 그렇다고 아무 일도 없던 것처럼 넘어가서도 안 됩니다. 좋은 습관은 오류를 로깅(예: Logger 사용)하고, 사용자에게 중요하다면 메시지를 보여주거나 UI에서 핸들러를 호출하는 것입니다.

로깅 예:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("비동기 작업 오류: " + exc);
    exc.printStackTrace();
}

프로덕션 코드에서는 System.err 대신 java.util.logging이나 Log4j 같은 제대로 된 로거를 사용하세요.

2. 비동기 작업 취소

언제 작업 취소가 필요할까요?

때로는 비동기 작업을 진행 중에 바로 멈춰야 할 때가 있습니다. 예를 들어, 사용자가 파일 다운로드 중에 “취소”를 눌렀을 수 있습니다. 또는 프로그램 창이 닫혀 해당 작업이 더 이상 의미가 없을 수도 있습니다. 애플리케이션을 종료할 때 리소스를 깔끔하게 해제해야 할 때도 있습니다.

이런 경우 Java의 비동기 I/O는 Future 인터페이스를 통해 취소를 지원합니다. 이를 사용하면 언제든 실행 중인 작업을 중단하여 불필요한 리소스 소모를 막을 수 있습니다.

Future로 작업을 어떻게 취소하나요?

AsynchronousFileChannel의 read 또는 write 메서드는 Future<Integer> 객체를 반환합니다. 이 객체에는 cancel(boolean mayInterruptIfRunning) 메서드가 있습니다.

예: 비동기 읽기 취소

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

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // 잠시 기다렸다가 작업을 취소합니다
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("읽기 작업이 취소되었습니다!");
            } else {
                System.out.println("작업을 취소하지 못했습니다(이미 완료되었을 수 있음)");
            }
        }
    }
}

중요한 포인트:

  • 취소는 아직 완료되지 않은 작업에만 동작합니다.
  • 작업이 이미 끝났다면 — 더 이상 취소할 수 없습니다.
  • 취소된 후 해당 Future에 대해 get()을 호출하면 CancellationException이 발생합니다.

언제 더 이상 작업을 취소할 수 없나요?

작업이 이미 완료되었다면 — 성공이든 오류든 — 더 이상 멈출 수 없습니다. 이미 기차는 떠난 셈입니다.

또한, 모든 구현이 운영체제 수준에서 실제로 작업을 중단시킬 수 있는 것은 아닙니다. 예를 들어 일부 파일 시스템에서는 “취소”가 형식적일 뿐이며, 작업은 계속 진행되고 결과만 무시될 수 있습니다.

3. 실전: 오류 처리와 취소

예제 1: 존재하지 않는 파일 읽기 시 오류 처리

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("작업이 성공적으로 완료되었습니다");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("파일 읽기 중 오류: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("파일 열기 중 오류: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

콘솔에서 무엇을 보게 될까요?

파일 열기 중 오류: no_such_file.txt

혹은, 열기가 아니라 실제 읽기 중 오류가 발생했다면:

파일 읽기 중 오류: NoSuchFileException - no_such_file.txt

예제 2: 긴 작업 취소 및 올바른 종료

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

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // 50ms 후 작업을 취소합니다(실험용)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("읽기 작업이 취소되었습니다!");
            } else {
                System.out.println("작업을 취소하지 못했습니다(아마 이미 완료되었을 것입니다)");
            }

            try {
                // 결과를 가져와 보겠습니다(CancellationException이 발생함)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("CancellationException 포착: 작업이 실제로 취소되었습니다.");
            }
        }
    }
}

4. Best practices: 올바르게 하는 방법

오류가 나도 리소스를 해제하세요

try-with-resources를 사용해 채널을 자동으로 닫으세요:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

CompletionHandler를 사용한다면, 모든 작업이 완료되었을 때 채널을 닫는 것을 잊지 마세요. 연속으로 여러 비동기 작업을 수행할 때 특히 중요합니다.

UI/메인 스레드를 막지 마세요

비동기 작업의 목적은 메인 스레드를 차단하지 않는 것입니다. UI 스레드에서 future.get()을 호출하지 마세요 — 그러면 비동기의 의미가 사라집니다.

모든 오류를 로깅하세요

CompletionHandler에서는 반드시 failed 메서드를 구현하고 모든 예외를 로깅(또는 상위로 전달)하세요.

프로그램 종료 전에 모든 작업의 완료를 확인하세요

프로그램이 작업 종료 전에 끝나면 결과가 유실될 수 있습니다. 콘솔 데모에서는 때로 Thread.sleep(500)을 쓰기도 하지만, 실제 애플리케이션에서는 CountDownLatch, CompletableFuture 등 동기화 메커니즘을 사용하세요.

취소를 잊지 마세요

작업이 더 이상 필요 없다면(예: 사용자가 창을 닫음) Future.cancel로 취소하세요. 리소스를 절약하고 애플리케이션 응답성을 높일 수 있습니다.

5. async I/O에서 오류 처리와 취소 시 자주 하는 실수

실수 №1: 메서드 failed 를 CompletionHandler에서 무시함.
오류 처리를 구현하지 않으면 애플리케이션이 예측 불가능하게 동작할 수 있습니다. 오류가 “사라지고”, 사용자는 왜 아무 일도 일어나지 않는지 모른 채로 남게 됩니다.

실수 №2: 작업이 끝난 뒤 채널을 닫지 않음.
AsynchronousFileChannel을 닫지 않으면 리소스 누수나 OS에서 파일이 잠기는 문제가 발생할 수 있습니다.

실수 №3: 메인 스레드에서 비동기 작업의 결과를 기다림.
future.get()을 UI 스레드에서 호출하면 — UI가 멈추고, 비동기의 의미가 사라집니다.

실수 №4: 이미 끝난 작업을 취소하려고 시도함.
cancel() 호출이 너무 늦으면 — 작업이 이미 완료되어 취소가 동작하지 않습니다. 치명적이진 않지만 디버깅 시 혼란을 줄 수 있습니다.

실수 №5: 취소 결과를 확인하지 않음.
cancel()을 호출하고 반환값을 확인하지 않거나 CancellationException 없이 get()을 호출하면 — 프로그램이 예외로 종료되거나 이상하게 동작할 수 있습니다.

실수 №6: 오류 또는 취소 시 리소스를 해제하지 않음.
오류나 취소 이후 채널을 닫지 않으면 리소스 누수나 파일 잠금이 발생할 수 있습니다.

1
과제
JAVA 25 SELF, 레벨 56, 레슨 3
잠금
채널 전환: 비디오 다운로드 취소 🎬
채널 전환: 비디오 다운로드 취소 🎬
1
과제
JAVA 25 SELF, 레벨 56, 레슨 3
잠금
시스템 로그 모니터링: 병렬 검사 및 긴급 중단 🚨
시스템 로그 모니터링: 병렬 검사 및 긴급 중단 🚨
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION