[TIL] 241212 배송 경로 생성 기능 구현

MONA·2024년 12월 12일

나혼공

목록 보기
45/92

개요

주문(Order): 업체가 요청한 주문
배송(Delivery): 주문에 대한 배송 정보
배송 상세 경로(DeliveryRecords): 배송이 경유지를 거치는 단계를 상세히 기록한 정보

일단 배송은 주문 생성 시점에 생성된다.
배송에는 다음과 같은 정보가 포함되어 있다.

public class Deliveries extends BaseEntity {

    @Id
    @GeneratedValue
    @UuidGenerator
    @Column(updatable = false, nullable = false)
    private UUID deliveryId;

    @Column(nullable = false)
    private UUID sourceHubId; // 출발 허브 id

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private DeliveryStatusEnum status = DeliveryStatusEnum.HUB_WAIT; // 배송 상태

    @Column(nullable = false)
    private String companyAddress; // 배송지 주소

    @Column(nullable = false)
    private String recipient; // 수령인

    @Column(nullable = false)
    private String recipientSlackAccount; // 수령인 slack 계정(id)

    @Column(nullable = false)
    private LocalDateTime dispatchDeadline; // 배송을 시작해야 하는 데드라인

    private Integer totalSequence; // 총 시퀀스 수(DeliveryRecords 수)

    private Integer currentSeq; // 현재 진행중 시퀀스 번호

    @Column(nullable = false)
    private UUID orderId;

    @Column(nullable = false)
    private UUID companyId;

    @OneToMany(mappedBy = "delivery", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<DeliveryRecords> deliveryRecords = new ArrayList<>(); // 배송 상세 경로
}

그리고 배송이 만들어지는 시점에 배송 상세 경로(DeliveryRecords)가 생성되어야 한다.
DeliveryRecords가 포함한 정보는 다음과 같다.

public class DeliveryRecords extends BaseEntity {

    @Id
    @UuidGenerator
    @GeneratedValue(generator = "UUID")
    @Column(updatable = false, nullable = false)
    private UUID deliveryRecordId;

    @Column(nullable = false)
    private UUID departures; // 출발 허브 id

    @Column(nullable = false)
    private UUID arrival; // 도착 허브/업체 id

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private DeliveryRecordsStatusEnum status = DeliveryRecordsStatusEnum.WAIT; // 진행 상태

    @Column(nullable = false)
    private Integer sequence; // 시퀀스 순서

    @Column(nullable = false)
    private Duration estimatedTime; // 예상 소요 시간

    @Column(nullable = false)
    private BigDecimal estimatedDist; // 예상 소요 거리

    private Duration actualTime; // 실제 소요 시간

    private BigDecimal actualDist; // 실제 소요 거리

    @ManyToOne
    @JoinColumn(name = "delivery_id", nullable = false)
    private Deliveries delivery;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "deliverer_id")
    private Deliverers deliverer; // 배송 담당자 id
}

배송이 만들어지는 시점에 출발 허브로부터 업체까지 예상되는 최단거리를 탐색해서 미리 저장해두고 각 시퀀스가 도래하는 시점에 배송 담당자를 배정할 것이다.

이 때, 허브-허브는 인접 허브끼리 연결된 곳도 있고 아닌 곳도 있다.
연결되지 않은 허브는 아무리 가까워도 바로 이동이 불가하다.

업체는 하나의 허브에 속해있고, 이 업체가 속해있는 허브를 마지막 종착 허브라고 생각하여 허브-허브간 이동 경로를 계산해야 한다.

허브-허브간 경로는 허브 서비스에서 레디스에 캐싱해두고, 배송 서비스에는 이 캐싱된 정보를 가져와서 최단경로를 계산해 배송 상세 경로와 배송을 생성하면 된다.

업체와 가장 가

구현

최단경로 계산하기

레디스에 캐싱될 정보는 다음과 같다.

package com.sparta.delivery.dto;

import java.time.Duration;
import java.util.UUID;

public record HubRoute(
        UUID hubRouteId,
        UUID departureHubId,
        UUID arrivalHubId,
        double estimatedDistance, // 예상 소요 거리
        Duration estimateTime // 에상 소요 시간
) {
}

먼저 레디스에 캐싱된 허브 간 경로를 가져온다.

public List<HubRoute> getHubRoutes() {
        try {
            String hubRoutesJson = redisTemplate.opsForValue().get("hub_routes");
            if (Objects.isNull(hubRoutesJson)) {
                return null;
            }
            return objectMapper.readValue(hubRoutesJson, new TypeReference<>() {});
        } catch (Exception e) {
            throw new RuntimeException("레디스 조회 실패", e);
        }
    }

그리고 경로를 바탕으로 그래프를 만든다.

private Map<UUID, List<HubRoute>> buildGraph(List<HubRoute> hubRoutes) {
        Map<UUID, List<HubRoute>> graph = new HashMap<>();
        for (HubRoute route : hubRoutes) {
            graph.computeIfAbsent(route.departureHubId(), k -> new ArrayList<>()).add(route);
        }
        return graph;
    }

만들어진 그래프로 다익스트라를 돌린다.


PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparing(Node::totalTime));
Map<UUID, Duration> shortestTimes = new HashMap<>();
Map<UUID, UUID> previousNodes = new HashMap<>();

우선순위 큐를 이용하여 현재까지 계산된 최단 시간을 기준으로 노드를 자동 정렬시킨다.
Node에는 현재 탐색 중인 hubId와 누적 시간(totalTime)이 저장된다.

shortestTimes

  • 각 허브 Id에 대해 현재까지 발견된 가장 짧은 시간을 저장한다.
  • 초기 값으로 startHubId에 0을, 나머지 허브는 Duration.ofDays(Long.MAX_VALUE)로 최대 시간을 설정해준다.

previousNodes

  • 각 허브로 가는 직전 허브를 저장한다.
queue.add(new Node(startHubId, Duration.ZERO));
shortestTimes.put(startHubId, Duration.ZERO);

시작 허브를 큐에 추가하고 누적 시간을 0으로 설정한다.

while (!queue.isEmpty()) {
    Node currentNode = queue.poll();
    UUID currentHubId = currentNode.hubId();

큐에서 가장 작은 누적 시간을 가진 노드를 꺼낸다.

if (currentHubId.equals(endHubId)) {
    return reconstructPath(previousNodes, endHubId);
}

현재 노드가 종착 허브(endHubId)와 같다면 탐색을 종료한다.
reconstructPath를 호출해 최단 경로를 복원하고 반환한다.

도달하지 못했다면 인접 노드를 탐색한다.

List<HubRoute> neighbors = graph.getOrDefault(currentHubId, Collections.emptyList());
for (HubRoute route : neighbors) {
    UUID neighborHubId = route.arrivalHubId();
    Duration newTime = currentNode.totalTime().plus(route.estimateTime());

현재 허브에 연결된 인접 노드를 가져온다.
각 경로에 대해서 누적 시간+현재 경로의 소요 시간을 더한다.

if (newTime.compareTo(shortestTimes.getOrDefault(neighborHubId, Duration.ofDays(Long.MAX_VALUE))) < 0) {
    shortestTimes.put(neighborHubId, newTime);
    previousNodes.put(neighborHubId, currentHubId);
    queue.add(new Node(neighborHubId, newTime));
}

현재 경로를 통해 인접 허브로 가는 시간이 최단 시간보다 짧다면

  • shortestTimes 갱신
  • previousNodes에 인접 허브의 이전 노드로 currentHubId를 저장
  • 인접 허브를 큐에 추가
return Collections.emptyList();

큐를 다 돌았는데도 도달하지 못하면 빈 리스트를 반환한다.
근데 어쨌든 모두 연결되어 있는 허브 간 경로라 이럴 경우는 없을 것이다.

reconstructPath의 내용은 다음과 같다.

private List<UUID> reconstructPath(Map<UUID, UUID> previousNodes, UUID endHubId) {
        List<UUID> path = new LinkedList<>();
        for (UUID arrival = endHubId; arrival != null; arrival = previousNodes.get(arrival)) {
            path.add(0, arrival);
        }
        return path;
    }

endHubId로부터 시작해 직전 노드를 따라가면서 리스트의 맨 앞에 추가한다.
previousNodes.get(at)를 반복적으로 호출하면서 이전 노드를 추적하고 null에 도달하면 경로 복원이 완료된다.

최종 코드

public class PathService {

    private final StringRedisTemplate redisTemplate;
    private final ObjectMapper objectMapper;

    // Redis에서 허브 경로 데이터 가져오기
    public List<HubRoute> getHubRoutes() {
        try {
            String hubRoutesJson = redisTemplate.opsForValue().get("hub_routes");
            if (Objects.isNull(hubRoutesJson)) {
                return null;
            }
            return objectMapper.readValue(hubRoutesJson, new TypeReference<>() {});
        } catch (Exception e) {
            throw new RuntimeException("레디스 조회 실패", e);
        }
    }

    // 시작 허브부터 종착 허브까지 최단 경로 찾기
    public List<UUID> findShortestPath(List<HubRoute> hubRoutes, UUID startHubId, UUID endHubId) {

        // 그래프 구성
        Map<UUID, List<HubRoute>> graph = buildGraph(hubRoutes);

        return dijkstra(graph, startHubId, endHubId);
    }

    private Map<UUID, List<HubRoute>> buildGraph(List<HubRoute> hubRoutes) {
        Map<UUID, List<HubRoute>> graph = new HashMap<>();
        for (HubRoute route : hubRoutes) {
            graph.computeIfAbsent(route.departureHubId(), k -> new ArrayList<>()).add(route);
        }
        return graph;
    }

    private List<UUID> dijkstra(Map<UUID, List<HubRoute>> graph, UUID startHubId, UUID endHubId) {
        // 우선순위 큐 (최단 시간 기준)
        PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparing(Node::totalTime));
        Map<UUID, Duration> shortestTimes = new HashMap<>();
        Map<UUID, UUID> previousNodes = new HashMap<>();

        // 초기화
        queue.add(new Node(startHubId, Duration.ZERO));
        shortestTimes.put(startHubId, Duration.ZERO);

        while (!queue.isEmpty()) {
            Node currentNode = queue.poll();
            UUID currentHubId = currentNode.hubId();

            // 종착 허브에 도달하면 경로 복원
            if (currentHubId.equals(endHubId)) {
                return reconstructPath(previousNodes, endHubId);
            }

            // 인접 허브 탐색
            List<HubRoute> neighbors = graph.getOrDefault(currentHubId, Collections.emptyList());
            for (HubRoute route : neighbors) {
                UUID neighborHubId = route.arrivalHubId();
                Duration newTime = currentNode.totalTime().plus(route.estimateTime());

                if (newTime.compareTo(shortestTimes.getOrDefault(neighborHubId, Duration.ofDays(Long.MAX_VALUE))) < 0) {
                    shortestTimes.put(neighborHubId, newTime);
                    previousNodes.put(neighborHubId, currentHubId);
                    queue.add(new Node(neighborHubId, newTime));
                }
            }
        }

        return Collections.emptyList();
    }

    private List<UUID> reconstructPath(Map<UUID, UUID> previousNodes, UUID endHubId) {
        List<UUID> path = new LinkedList<>();
        for (UUID arrival = endHubId; arrival != null; arrival = previousNodes.get(arrival)) {
            path.add(0, arrival);
        }
        return path;
    }

    private static class Node {
        private final UUID hubId;
        private final Duration totalTime;

        public Node(UUID hubId, Duration totalTime) {
            this.hubId = hubId;
            this.totalTime = totalTime;
        }

        public UUID hubId() {
            return hubId;
        }

        public Duration totalTime() {
            return totalTime;
        }
    }
}

배송 정보 생성

다른 서비스와 연동하는 부분은 추후에 구현할거고, 일단은 임시 데이터로 때워뒀다.

public ApiResponse createDelivery(CreateDeliveryRequest request) {
        // 사용자 권한 및 유효성 체크, 수신인 조회
        String recipient = "tempRecipient";
        String recipientSlackAccount = "tempRecipientSlackAccount";

        // 업체 조회
        String companyAddress = "tempCompanyAdress";
        Point companyLocation = new Point(BigDecimal.valueOf(127.1058342), BigDecimal.valueOf(37.359708));
        // 업체가 소속된 허브를 종착 허브로 설정

        // 배송 생성
        Deliveries delivery = Deliveries.create(
                request.orderId(),
                request.sourceHubId(),
                request.destinationId(),
                companyAddress,
                recipient,
                recipientSlackAccount
        );
        // Redis에서 허브 경로 데이터 가져오기
        List<HubRoute> hubRoutes = pathService.getHubRoutes();

//        if(hubRoutes == null) {
//            // 경로 데이터 없을 경우 DB에서 조회 필요
//        }
        // 배송 상세 경로 생성
        List<UUID> paths = pathService.findShortestPath(hubRoutes, request.sourceHubId(), request.destinationId());
        List<DeliveryRecords> deliveryRecordsList = new ArrayList<>();
        for (int i = 0; i < paths.size() - 1; i ++) {
            UUID departureHubId = paths.get(i);
            UUID arrivalHubId = paths.get(i + 1);

            HubRoute hubRoute = pathService.findHubRoute(hubRoutes, departureHubId, arrivalHubId);

            DeliveryRecords deliverRecord = DeliveryRecords.create(
                    departureHubId,
                    arrivalHubId,
                    i + 1,
                    hubRoute.estimateTime(),
                    BigDecimal.valueOf(hubRoute.estimatedDistance()),
                    delivery
            );
            deliveryRecordsList.add(deliverRecord);
        }
        // 마지막 허브 위치 조회
        Point lastHubLocation = new Point(BigDecimal.valueOf(126.977969), BigDecimal.valueOf(37.566535));

        // 업체까지의 배송 경로 레코드 생성
        KakaoRouteResponse response = kakaoMapService.getRoute(lastHubLocation, companyLocation);
        if(response != null && response.routes().length >0) {
            BigDecimal distance = response.routes()[0].summary().distance();
            BigDecimal duration = response.routes()[0].summary().duration();
            DeliveryRecords finalRecord = DeliveryRecords.create(
                    paths.get(paths.size() - 1),
                    request.destinationId(),
                    deliveryRecordsList.size() + 1,
                    Duration.ofMillis(duration.longValue()),
                    distance,
                    delivery
            );
            deliveryRecordsList.add(finalRecord);
        }

        // 시퀀스 업데이트
        Deliveries updatedDelivery = Deliveries.updateSequence(delivery, 0, deliveryRecordsList.size());

        // 배송 저장
        deliveryJpaRepository.save(updatedDelivery);

        // 배송 상세 경로 저장
        deliveryRecordsJpaRepository.saveAll(deliveryRecordsList);

        // 슬랙 메시지 전송

        // 반환
        return new ApiResponse<>(200, "배송 생성 성공", null);
    }

어질어질하다.

종착 허브-업체까지의 예상 소요 시간과 거리는 카카오 맵 API를 이용할 것이다.

public class KakaoMapService {

    @Value("${kakao.api.key}")
    private String KAKAO_API_KEY;
    private static final String DIRECTIONS_URL = "https://apis-navi.kakaomobility.com/v1/directions";

    public KakaoRouteResponse getRoute(Point origin, Point destination) {
        // 요청 URL 생성
        String url = UriComponentsBuilder.fromUriString(DIRECTIONS_URL)
                .queryParam("origin", origin.getLongitude() + "," + origin.getLatitude())
                .queryParam("destination", destination.getLongitude() + "," + destination.getLatitude())
                .queryParam("priority", "SHORTEST")
                .toUriString();

        // HTTP 헤더 설정
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "KakaoAK " + KAKAO_API_KEY);

        // API 호출
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<KakaoRouteResponse> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                new org.springframework.http.HttpEntity<>(headers),
                KakaoRouteResponse.class
        );

        return response.getBody();
    }

}

단순히 요청을 보내고 응답을 받는다..

수정

너무 길어서 잘라보려고 한다.

DeliveryService

public class DeliveryService {

    private final PathService pathService;
    private final KakaoMapService kakaoMapService;
    private final DeliveriesJpaRepository deliveryJpaRepository;
    private final DeliveryRecordsJpaRepository deliveryRecordsJpaRepository;

    private static final Logger logger = LoggerFactory.getLogger(DeliveryService.class);


    // 배송 생성
    public ApiResponse createDelivery(CreateDeliveryRequest request) {

        try {
            Deliveries delivery = createDeliveryEntity(request);

            // Redis에서 경로 데이터 가져오기
            List<HubRoute> hubRoutes = pathService.getHubRoutes();

            // 최단 경로 생성
            List<DeliveryRecords> deliveryRecordsList = createDeliveryRecords(request, hubRoutes, delivery);

            // 마지막 허브에서 업체까지의 경로 추가
            addFinalDeliveryRecord(deliveryRecordsList, delivery);

            // 배송 데이터 저장
            saveDelivery(delivery, deliveryRecordsList);

            return new ApiResponse<>(200, "배송 생성 성공", null);
        } catch (Exception e) {
            logger.error("Error occurred while creating delivery: {}", e.getMessage(), e);
            return new ApiResponse<>(500, "배송 생성 실패", null);
        }
    }

    private Deliveries createDeliveryEntity(CreateDeliveryRequest request) {
        return Deliveries.create(
                request.orderId(),
                request.sourceHubId(),
                request.destinationId(),
                "tempCompanyAddress",
                "tempRecipient",
                "tempRecipientSlackAccount"
        );
    }

    private List<DeliveryRecords> createDeliveryRecords(CreateDeliveryRequest request, List<HubRoute> hubRoutes, Deliveries delivery) {
        List<UUID> paths = pathService.findShortestPath(hubRoutes, request.sourceHubId(), request.destinationId());
        List<DeliveryRecords> deliveryRecordsList = new ArrayList<>();

        for (int i = 0; i < paths.size() - 1; i++) {
            UUID departureHubId = paths.get(i);
            UUID arrivalHubId = paths.get(i + 1);

            HubRoute hubRoute = pathService.findHubRoute(hubRoutes, departureHubId, arrivalHubId);

            DeliveryRecords record = DeliveryRecords.create(
                    departureHubId,
                    arrivalHubId,
                    i + 1,
                    hubRoute.estimateTime(),
                    BigDecimal.valueOf(hubRoute.estimatedDistance()),
                    delivery
            );
            deliveryRecordsList.add(record);
        }
        return deliveryRecordsList;
    }

    private void addFinalDeliveryRecord(List<DeliveryRecords> deliveryRecordsList, Deliveries delivery) {
        Point lastHubLocation = new Point(BigDecimal.valueOf(126.977969), BigDecimal.valueOf(37.566535));
        Point companyLocation = new Point(BigDecimal.valueOf(127.1058342), BigDecimal.valueOf(37.359708));

        kakaoMapService.getRouteAsync(lastHubLocation, companyLocation)
                .thenAccept(response -> {
                    if (response != null && response.routes().length > 0) {
                        BigDecimal distance = response.routes()[0].summary().distance();
                        BigDecimal duration = response.routes()[0].summary().duration();
                        DeliveryRecords finalRecord = DeliveryRecords.create(
                                deliveryRecordsList.get(deliveryRecordsList.size() - 1).getArrival(),
                                delivery.getCompanyId(),
                                deliveryRecordsList.size() + 1,
                                Duration.ofMillis(duration.longValue()),
                                distance,
                                delivery
                        );
                        deliveryRecordsList.add(finalRecord);
                    } else {
                        logger.warn("유효한 경로가 없음");
                    }
                }).exceptionally(e -> {
                    logger.error("addFinalDeliveryRecord failed: {}", e.getMessage(), e);
                    return null;
                });
    }

    private void saveDelivery(Deliveries delivery, List<DeliveryRecords> deliveryRecordsList) {
        delivery.setTotalSequence(deliveryRecordsList.size());
        delivery.setCurrentSeq(0);
        deliveryJpaRepository.save(delivery);
        deliveryRecordsJpaRepository.saveAll(deliveryRecordsList);
    }

}

카카오 맵에서 응답을 받는 방식을 비동기로 바꿔보았다.
비동기 방식을 적용하면 다음과 같은 이점이 있다.

  1. 병렬 처리로 응답 시간이 단축된다
  • 외부 API 호출을 기다리는 동안 CPU가 다른 작업을 할 수 있음
  1. 메인 스레드 블로킹 방지
  • 비동기 호출로 인해 메인 스레드가 블로킹되지 않아서 애플리케이션 응답성이 유지될 수 있음
  1. 높은 동시성 처리
  • 여러 호출을 효율적으로 처리할 수 있음
public class KakaoMapService {

    @Value("${kakao.api.key}")
    private String kakaoApiKey;

    @Async
    public CompletableFuture<KakaoRouteResponse> getRouteAsync(Point origin, Point destination) {
        try {
            String url = UriComponentsBuilder.newInstance()
                    .scheme("https")
                    .host("apis-navi.kakaomobility.com")
                    .path("/v1/directions")
                    .queryParam("origin", origin.getLongitude() + "," + origin.getLatitude())
                    .queryParam("destination", destination.getLongitude() + "," + destination.getLatitude())
                    .queryParam("priority", "SHORTEST")
                    .build()
                    .toUriString();

            HttpHeaders headers = new HttpHeaders();
            headers.set("Authorization", "KakaoAK " + kakaoApiKey);

            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<KakaoRouteResponse> response = restTemplate.exchange(
                    url,
                    HttpMethod.GET,
                    new HttpEntity<>(headers),
                    KakaoRouteResponse.class
            );

            return CompletableFuture.completedFuture(response.getBody());
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }

}

@Async

Spring의 비동기 작업을 위한 어노테이션
메서드 호출 시 새로운 스레드에서 작업을 실행하며, 호출자는 CompletableFuture 객체를 즉시 반환받음.

CompletableFuture

비동기 작업을 쉽게 관리하고 조합할 수 있도록 설계됨
비동기 방식으로 작업을 실행하고, 작업 완료 시의 후속 작업을 정의하거나 작업 완료 시 결과를 처리하는 방식을 제공함

주요 기능 및 특징

  1. 비동기 작업 실행

    • 스레드나 Executor를 명시적으로 관리하지 않고 비동기 작업을 수행할 수 있음
    CompletableFuture.supplyAsync(() -> {
      // 비동기 작업
      return "Result";
    });
  2. 후속 작업 연결

    • 작업이 완료된 후 실행할 작업을 정의할 수 있음
    • thenApply, thenAccept, thenRun 등의 메서드
    메서드설명
    supplyAsync결과를 반환하는 비동기 작업 실행
    runAsync결과를 반환하지 않는 비동기 작업 실행
    thenApply작업 완료 후 결과를 변환하는 작업 추가
    thenAccept작업 완료 후 결과를 소비하는 작업 추가
    thenRun작업 완료 후 결과와 무관하게 실행되는 작업 추가
    thenCombine두 개의 비동기 작업 결과를 조합
    exceptionally예외가 발생했을 때 기본값으로 대체
    whenComplete작업이 완료되면 성공/실패 여부와 관계없이 실행
    allOf여러 CompletableFuture 작업이 모두 완료될 때 실행
    anyOf여러 CompletableFuture 작업 중 하나라도 완료되면 실행
  3. 작업 조합

    • 여러 비동기 작업을 조합해 실행 순서를 정의하거나 결과를 조합할 수 있음
  4. 작업 완료 시 처리

    • whenComplete, exceptionally 메서드를 사용해 작업 성공과 실패의 경우 후속 처리를 할 수 있음
  5. 결과 대기

    • get() 또는 join() 메서드를 사용하여 작업이 완료될 때까지 대기하고 결과를 가져올 수 있음

장점

  • 비동기 작업을 간결하게 표현해 콜백 지옥을 피할 수 있음
  • 여러 작업의 결과를 쉽게 조합할 수 있음
  • 예외처리를 지원해 비동기 작업의 실패를 쉽게 컨트롤 할 수 있음

단점

  • 스레드 풀 관리 필요: ForkJoinPool을 사용하나, 대규모 비동기 작업에서는 커스텀 Executor 설정이 필요함
  • get()이나 join()을 잘못 사용하면 블로킹이 발생할 수 있음
    • get(): CompletableFuture의 결과를 반환하며, 작업이 완료될 때까지 현재 스레드를 블로킹함. 예외 발생 시 ExecutionException를 던짐.
    • join(): CompletableFuture의 결과를 반환하며, 작업이 완료될 때까지 현재 스레드를 블로킹함. 예외 발생 시 CompletionException를 던짐.
    • 비동기 후속 작업(thenApply, thenAccept, whenComplete)를 사용해 블로킹을 피하는 것도 하나의 방법이다.
  • 비동기 작업의 디버깅이 동기 작업보다 복잡할 수 있음
profile
고민고민고민

0개의 댓글