[0621] Future / 선택과제

ㅇㅇㅈ·2025년 6월 21일

원래는 어제 했었어야 했는데...
너무 이해가 안돼서 하루 밀렸다.

(비)동기 프로그래밍

동기 프로그래밍
하나의 작업이 완료될 때까지 대기 후 다음 작업을 실행

  • 작업이 오래 걸릴 경우 전체가 지연됨
  • CPU 효율이 낮음 (대기 시간 발생)
  • 연속된 계산, 단순 로직에 적절함.

비동기 프로그래밍
여러 작업을 동시에 실행하고, 결과가 필요할 때만 기다림

  • 빠른 응답 가능
  • CPU 리소스를 최적으로 활용할 수 있음
  • 네트워크 요청이나 데이터베이스 조회에 적절

 

Future

Future<T>는 비동기 연산의 결과를 나중에 받을 수 있도록 하는 기능.

Future<Integer>futureResult = executor.submit(() -> {
			Thread.sleep(2000);
            return 42;
});

Future<Integer> futureResult

  • 이후에 Future에 Integer가 담길 것이라는 뜻

executor.submit(...)

  • 작업(코드 블록)을 스레드풀에 맡기는 메서드
    (이후 Future로 돌려줌)

() -> { ... }

  • 람다식
    새로운 스레드가 이 안에 들어있는 코드만 따로 실행

 

그러니까...

멀티 스레드스레드풀Future
은행 창구은행원 교대 업무은행 대기표
여러 개의 창구에서
손님들을 관리
일에 따라
스레드들을
활성화/비활성화
은행에서 번호표 받고
다른 일 하다가
돌아오는 것

대충 이런 걸까 싶다.

 


Future의 주요 메소드

get() : 결과를 반환 (작업이 끝날 때까지 블로킹)

isDone() : 작업 완료 여부 확인

cancel(true/false) : 작업 취소

예제

public class FutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(2000);
            return 42;
        });

        System.out.println("✅ 작업 수행 중...");
        System.out.println("🍀 결과: " + future.get());
        executor.shutdown();
    }
}

Futuredml 한계

CompletableFuture가 생긴 이유

아 방금 배웠는데 왜 한계가 있는 거야😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁😁

  1. get() 메소드가 블로킹
    비동기 작업의 결과를 가져오기 전에 get()을 호출하면, 작업이 끝날 때까지 기다려야 함

  2. Future는 콜백 callback 체이닝을 지원하지 않음

    • FuturethenApply()와 같은 체이닝 기능이 없음
    • 작업 완료 후 다른 작업을 연결하려면 복잡한 코드가 필요
  3. 여러 개의 Future를 조합하기 어려움
    Future는 병렬 실행된 여러 개의 작업을 조합하는 기능이 부족

 

 


CompletableFuture

Future의 확장 버전, 비동기 작업을 더 쉽게 다룰 수 있도록 개선된 클래스

  • 콜백 기능으로 체이닝 기능
  • 비동기 실행이 완료되면 자동으로 결과 변환
  • 블로킹 없이 작업 완료 후 추가 연산 가능

Future의 경우에는 단순한 비동기 실행(나중에 결과 주는 것)만 가능하지만, CompletableFuture는 체이닝 및 예외 처리까지 가능하다.

CompletableFuture 주요 메소드

Future의 메소드를 공유하고, 이후 추가 작업하는 메소드

 
supplyAsync : 새로운 비동기 작업 실행 후 결과 반환

thenApply : 이전 결과를 반환

thenAccept : 이전 결과를 사용하지만 반환값 없음

thenCombine : 두 개의 CompletableFuture를 조합

exceptionally : 예외 발생 시 대체값 반환

 


예제

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
                    System.out.println("🚀 비동기 작업: " + Thread.currentThread().getName());
                    try { Thread.sleep(2000); } catch (InterruptedException e) {}
                    return 42;
                })
                .thenApply(result -> result * 2)
                .thenAccept(result -> System.out.println("🍀 최종 결과: " + result + " / 스레드: " + Thread.currentThread().getName()));

        System.out.println("🧵 메인 스레드: " + Thread.currentThread().getName());
        try { Thread.sleep(3000); } catch (InterruptedException e) {}

    }
}

기존의 예제로는 비동기 작업이 잘 보이지 않아서 스레드의 이름을 바꿔보았다.
이를 실행할 경우,

🚀 비동기 작업: ForkJoinPool.commonPool-worker-1
🧵 메인 스레드: main
🍀 최종 결과: 84 / 스레드: ForkJoinPool.commonPool-worker-1

CompletableFuture.supplyAsync(() -> { ... }에 묶여있던 작업들은 ForkJoinPool.commonPool-worker-1 스레드로 출력되고,
메인 스레드에서의 작업은 main으로 출력되는 것을 볼 수 있었다.

 


예외 발생 시 처리 예제

public class CompletableFutureExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("❌ 오류 발생!");
        })
        .exceptionally(e -> {
            System.out.println("▲ 예외 발생: " + e.getMessage());
            return -1; // 에러가 났을 때 대신 돌려주는 값!
        })
        .thenAccept(result -> System.out.println("✅ 최종 결과: " + result));
    }
}

솔직히 이해는 못했지만 일단 적어놨다.
손이 외우길 빌어야겠다.


선택과제1 (로또)

import java.util.*;

public class Lotto {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);

        System.out.println("[로또 당첨 프로그램]");
        System.out.print("로또 개수를 입력해 주세요.(숫자 1 ~ 10): ");
        int ticketCount = scan.nextInt();

        List<Set<Integer>> tickets = new ArrayList<>();
        for (int i = 0; i < ticketCount; i++) {
            tickets.add(makeLotto());
        }

        char mark = 'A';
        for (Set<Integer> ticket : tickets) {
            System.out.print(mark++ + " ");
            print(ticket);
        }

        Set<Integer> winNums = makeLotto();
        System.out.println("\n[로또 발표]");
        print(winNums);

        System.out.println("\n[내 로또 결과]");
        mark = 'A';
        for (Set<Integer> ticket : tickets) {
            int match = 0;
            for (int n : ticket) {
                if (winNums.contains(n)) match++;
            }
            System.out.print(mark++ + " ");
            print(ticket);
            System.out.println(" => " + match + "개 일치");
        }

        scan.close();
    }

    private static Set<Integer> makeLotto() {
        Set<Integer> nums = new TreeSet<>();
        Random r = new Random();
        while (nums.size() < 6) {
            nums.add(r.nextInt(45) + 1);
        }
        return nums;
    }

    private static void print(Set<Integer> nums) {
        int cnt = 0;
        for (int n : nums) {
            System.out.print(n);
            if (++cnt < nums.size()) System.out.print(",");
        }
        System.out.println();
    }
}

선택과제2 (과세 금액)

import java.util.Scanner;

public class Tax {
    // 구간 경계, 세율, 누진공제 배열(각 index가 동일 구간)
    static int[] section = {12_000_000, 46_000_000, 88_000_000, 150_000_000, 300_000_000, 500_000_000, 1_000_000_000};
    static double[] rate = {0.06, 0.15, 0.24, 0.35, 0.38, 0.40, 0.42, 0.45};
    static int[] minus = {0, 1_080_000, 5_220_000, 14_900_000, 19_400_000, 25_400_000, 35_400_000, 65_400_000};

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("[과세금액 계산 프로그램]");
        System.out.print("연소득을 입력해 주세요.:");
        int income = scan.nextInt();

        int[] sectionAmount = new int[8];
        double[] taxEach = new double[8];
        int left = income;
        int start = 0;

        // 구간별 계산
        for (int i = 0; i < section.length; i++) {
            int thisRange = section[i] - start;
            if (left > thisRange) {
                sectionAmount[i] = thisRange;
                left -= thisRange;
                start = section[i];
            } else {
                sectionAmount[i] = left;
                left = 0;
                break;
            }
        }
        if (left > 0) sectionAmount[section.length] = left;

        // 각 구간 세금 및 총 세금
        double totalTax = 0;
        for (int i = 0; i < sectionAmount.length; i++) {
            taxEach[i] = sectionAmount[i] * rate[i];
            if (sectionAmount[i] > 0)
                System.out.printf("%d * %.0f%% = %.0f\n", sectionAmount[i], rate[i] * 100, taxEach[i]);
            totalTax += taxEach[i];
        }
        System.out.printf("\n[세율에 의한 세금]:\t\t%.0f\n", totalTax);

        // 누진공제 계산
        int idx = 0;
        int[] cut = {12_000_000, 46_000_000, 88_000_000, 150_000_000, 300_000_000, 500_000_000, 1_000_000_000};
        while (idx < cut.length && income > cut[idx]) idx++;
        double minusTax = income * rate[idx] - minus[idx];
        System.out.printf("[누진공제 계산에 의한 세금]:\t%.0f\n", minusTax);
        scan.close();
    }
}

일단 메모만...

0개의 댓글