[Java] 콜백 패턴과 적용

2025. 1. 13. 17:38·JAVA

java에서의 콜백에 대해 알게되면서 동기, 비동기를 차치하고도 작업의 참조를 전달(위임)한다는 부분이 인상적깊었다.

 

기본적으로 콜백 패턴의 정의와 원리에 대해 알아보자.

 

콜백은 한마디로 피호출자 (Callee)가 호출자 (Caller)를 다시 호출하는 것을 의미한다.

특정 이벤트가 발생하거나 특정 작업이 완료되면 특정 메서드를 실행하는 방법인데,
비동기 처리나, 작업완료 시나리오에서 유용하게 사용된다.

 

간단한 웹 어플리케이션을 예시로 들어보자.
프런트 서버가 작업 서버에 일정 시간이 소요되는 작업을 요청했을 때, 사용자가 그동안 가만히 있을 순 없기 때문에
작업 완료 시간을 하염없이 기다리기보단 작업 완료 신호가 오기 전까지 다른 일을 하면서 작업 완료 여부를 알고 싶을 것이다.

 

그렇다면 프런트 서버가 작업 완료 여부를 알기 위해서는 어떻게 해야할까?

핵심적인 내용은 아니지만 작업 서버에서 직접 프런트 서버로 작업 완료 여부를 보내는건 말이 안되고,

프런트 서버에서 작업이 완료되었는지 확인하기 위해 계속 확인 요청을 보내기도 그렇다.

 

흔히 알겠지만 이런 경우 자바스크립트의 비동기 모델을 활용해 콜백을 구현하는데, 간단한 예시를 보면

function fetchData(callback) {
    console.log("Fetching data from server...");
    setTimeout(() => {
        const data = "Server response";
        callback(data); // 작업 완료 후 콜백 실행
    }, 2000);
}

function processData(data) {
    console.log("Processing:", data);
}

fetchData(processData);

이런 식으로 미리 콜백 함수를 지정해놓는 식으로 구현할 수 있다.

 

자바 입장에서의 콜백 패턴도 비슷한 것 같다.

(한 웹 어플리케이션 내에서 프런트서버의 콜백과 작업서버의 콜백은 독립적이지만, 작업 서버에서의 지연이 충분히 영향을 미칠 수 있다)

만약 서버가 파일 다운로드 요청을 받았다고 해보자.

//파일 다운로드 클래스
public class FileDownloader {

    private final ExecutorService executor = Executors.newSingleThreadExecutor(); // 비동기 작업을 위한 스레드 풀
    private final DownloadCallback callback;

    public FileDownloader(DownloadCallback callback) {
        this.callback = callback;
    }

    public void downloadFile(String fileUrl) {
        executor.submit(() -> {
            try {
                // 다운로드 진행 시 progress 업데이트
                for (int i = 1; i <= 100; i++) {
                    Thread.sleep(50); // 다운로드 시뮬레이션
                    callback.onProgress(i); // 진행률 업데이트
                }

                // 다운로드 완료
                String fileData = "Downloaded content from " + fileUrl; // 파일 데이터 시뮬레이션
                callback.onComplete(fileData);

            } catch (Exception e) {
                // 에러 발생 시 콜백 호출
                callback.onError("Failed to download file: " + e.getMessage());
            }
        });
    }

    public void shutdown() {
        executor.shutdown();
    }
}

//사용 클래스
public class App {
    public static void main(String[] args) {
        DownloadCallback callback = new DownloadCallback() {
            @Override
            public void onProgress(int progress) {
                System.out.println("Progress: " + progress + "%");
            }

            @Override
            public void onComplete(String fileData) {
                System.out.println("Download Complete! File data: " + fileData);
            }

            @Override
            public void onError(String errorMessage) {
                System.out.println("Error: " + errorMessage);
            }
        };

        FileDownloader downloader = new FileDownloader(callback);

        System.out.println("Starting file download...");
        downloader.downloadFile("http://example.com/file.txt");

        // 메인 스레드가 블로킹되지 않음
        System.out.println("You can perform other tasks while downloading...");
    }
}

이 경우도 역시 파일 다운로드가 어떻게 진행되고있는지 일일이 확인할 수 없기 때문에, 다운로드 작업을 별도의 스레드에서 실행시킨다.

사용 클래스에서는 파일 다운로드 작업의 결과를 미리 지정한 콜백 인터페이스의 구현체 DownloadCallback 의 인스턴스를 FileDownLoader클래스로 넘기기만 하면 된다.

 

이후에는 FileDownLoader는 사용자에 대해 알 필요 없이 단지 자신의 상황에 맞는 콜백 메서드를 호출하기만 하면 된다.

눈여겨봐야할 점은 결국 겉으로 보기에는 사용자가 FileDownLoader에게 진행상황을 물어보는 것 같지만,

FileDownLoader는 DownLoaderCallback 객체를 전달받아 실제로는 사용자가 구현한 메서드들을 호출한다는 것이다.

 

여기서 볼 수 있는 콜백의 핵심은 사용자가 만든 DownloadCallback은 FileDownLoader로 넘겨도 똑같은 주소를 같는 똑같은 놈이라는 것이다. 자바에서는 이를 인터페이스를 이용해 구현하였다.

따라서 FileDownLoader에서 아무리 콜백 메서드를 실행해도 결국 사용자 클래스의 메서드를 실행하는 꼴이 되고,

사용자는 이 DownloadCallback 객체를 통해 사용자 클래스에서 파일 다운로드 상태를 확인하며 후속 작업을 유연하게 작성할 수 있게 된다.

 

결론적으로 보면, 파일 다운로드라는 오래걸리는 작업은 새 스레드에서 비동기적으로 실행되지만,
DownloadCallback 인스턴스를 통해 작업의 진행상황을 사용자가 전달받을 수 있게 된다.

또한 파일 다운로드 책임과, 파일 다운로드 상태를 확인하며 추가적인 작업을 하는 책임이 분리되어 유연한 코드 작성이 가능해질 것이다.

 

 

다음은 내가 적용해본 사례이다.

InputView로부터 입력값을 받고 처리하는 과정에서 Error가 발생시 메시지를 출력하고 해당 부분부터 다시 입력받는 상황이다.

public interface InputHandlerCallback {
    void execute() throws IllegalArgumentException;
}

콜백 사용을 위한 인터페이스를 정의한다.

작업을 담당할 (여기서는 inputValidator) 클래스에 내부인터페이스로 정의해도 무방하다.

 

public class InputValidator {

    public static void repeatUntilValid(InputHandlerCallback callback) {
        while (true) {
            try {
                callback.execute();
                break;
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

Validator는 콜백을 실행할 수 있는 환경만을 제공한다.
실질적인 작업은 이 작업을 호출한 부분에서 실행된다.

리턴타입이 없기 때문에, 정상적으로 execute가 완료된 경우 명시적으로 break를 해주었다.

 

    public void purchaseLotto(int price) {
        this.quantity = Quantity.of(price);
        this.lottos = lottoGenerator.generate(quantity.getQuantity());
    }
    
    
        public static Quantity of(int price) {
        if (price % 1000 != 0 || price <= 0) {
            throw new IllegalArgumentException("[ERROR] 1000원 단위만 입력 가능합니다.");
        }
        return new Quantity(price / 1000);
    }
    
    //내부적으로 lottoGenerator.generate도 내부 도메인 클래스에서 IllegalArgumentException을 throw한다.
    private void purchaseLotto() {
        InputValidator.repeatUntilValid(() -> {
            int price = Integer.parseInt(InputView.getPrice());
            lottoService.purchaseLotto(price);
        });
        OutputView.showPurchaseList(lottoService.getLottos());
    }

 

위임할 작업을 설정한다. purchaseLotto는 Controller단에 위치했다.

(사실  int price = Integer.parseInt(InputView.getPrice()); 부분이 예외를 던지지는 않지만
구현 조건상 내부에서 IllegalArgumentException 발생 시 반복해야하는 작업의 단위가 입력받는 부분부터이다.)

 

상기했듯이 Validator 내부에서 callback.execute 시 실제 purchaseLotto에서 작업이 실행된다.

Validator가 직접 작업을 실행하는 것이 아니다. Validator는 작업의 참조를 위임받아 호출할 뿐이지,

모든 결과물은 결국 호출받아 실제 작업을 실행하는 Controller단에서 나온다.

 

 

 

이 방식을 적용한게 어떤 의미가 있는지 단순하게 while(true)만을 이용한 방식과의 차이점을 통해 알아보자.

private void purchaseLotto() {
    while (true) {
        try {
            int price = Integer.parseInt(InputView.getPrice());
            lottoService.purchaseLotto(price);                 
            break;
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
    OutputView.showPurchaseList(lottoService.getLottos());
}

위 코드의 문제점은, purchaseLotto 메서드에 책임이 너무 많다. (클래스 단위로 보면 컨트롤러가 직접 예외처리하는 책임까지 지니게 됨)

이렇게 보니 반복, 예외검증하는 책임을 분리하려고 하면 자연스럽게 콜백 패턴이 나오는 것 같기도 하다.

 

그냥 콜백 패턴을 이렇게도 적용해볼수 있다 정도이지 이 상황에서 콜백 패턴이 최선의 선택인지는 잘 모르겠다.

(콜백의 원리와 장점을 100% 활용했다고 하기엔 좀...)

 

그래도 동작 방식과, 왜 콜백 패턴이 유연한지를 java의 관점에서 느껴볼 수 있었던 것 같다.

요즘 비동기 프로그래밍에 관심이 많은데 콜백 방식보다 더 좋은 비동기 처리 방식이 많은 것 같다.
사이드프로젝트 하면서 적용하기에 더 좋은 사례가 있다면 다른 비동기 처리 방식과 함께 한번 리마인드해봐야겠다.

 

 

 

 

'JAVA' 카테고리의 다른 글

[Java] 자바 스레드와 운영체제 스레드의 관계  (1) 2025.01.03
'JAVA' 카테고리의 다른 글
  • [Java] 자바 스레드와 운영체제 스레드의 관계
폐프
폐프
  • 폐프
    폐프의삶
    폐프
  • 전체
    오늘
    어제
    • 분류 전체보기 (43)
      • 2023 하계 모각코 (12)
      • 2023-24 동계 모각코 (8)
      • 2024 SW ACADEMY (5)
      • Spring (1)
      • JPA (0)
      • JAVA (2)
      • Database (10)
      • OS (5)
      • Network (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
폐프
[Java] 콜백 패턴과 적용
상단으로

티스토리툴바