본문 바로가기

orElse·orElseGet

값이 있는 Optional에서도 실행되는 orElse 인수를 확인하고 중첩 주소와 배송 상태를 map·flatMap·지연 대체 경로로 처리합니다.

orElse(value)의 인수는 일반 Java 인수라 Optional이 값이 있는 상태여도 먼저 계산됩니다.

orElseGet(supplier)는 빈 상태일 때만 Supplier를 호출합니다.

결과 문자열이 같아도 비용, 부수 효과, 예외 시점이 다르므로 대체 경로 생성 방식에 맞춰 선택합니다.


값 존재 시 불필요한 대체 조회

아래 Optional에는 이미 주소가 있지만 orElse(loadDefault())의 인수가 즉시 평가됩니다.

원문에서 도출되는 출력은 address=Seoul, wrong-queries=1입니다.

lab/OrElseEagerQueryBug.java
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public final class OrElseEagerQueryBug {
    public static void main(String[] args) {
        AtomicInteger queries = new AtomicInteger();
        String address = Optional.of("Seoul").orElse(loadDefaultAddress(queries));
        System.out.println("address=" + address + ", wrong-queries=" + queries.get());
    }

    private static String loadDefaultAddress(AtomicInteger queries) {
        queries.incrementAndGet();
        return "Default";
    }
}

대체 경로가 리터럴·상수·이미 계산된 지역 변수처럼 값싼 값이면 orElse가 간결합니다.

데이터베이스 조회, 객체 생성, 로깅처럼 불필요한 실행을 피해야 한다면 orElseGet을 씁니다.

“Optional API이므로 자동으로 지연된다”고 생각해서는 안 됩니다.


두 대체 경로 API의 평가 경로

값이 있는 상태에서 orElse는 인수를 계산한 뒤 결과를 버리지만 orElseGet은 공급자 본문을 호출하지 않습니다.

빈 상태에서는 둘 다 대체 경로의 값을 반환합니다.

다만 Supplier 객체 생성과 캡처 비용은 발생할 수 있습니다.

src/OrElseAndOrElseGetTrace.java
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public final class OrElseAndOrElseGetTrace {
    public static void main(String[] args) {
        AtomicInteger eager = new AtomicInteger();
        AtomicInteger lazy = new AtomicInteger();

        String first = Optional.of("value").orElse(make("eager", eager));
        String second = Optional.of("value").orElseGet(() -> make("lazy", lazy));
        String third = Optional.<String>empty().orElseGet(() -> make("needed", lazy));

        System.out.println(first + "," + second + "," + third);
        System.out.println("eager=" + eager.get() + ", lazy=" + lazy.get());
    }

    private static String make(String value, AtomicInteger calls) {
        calls.incrementAndGet();
        return value;
    }
}

세 Optional 호출에서 실제로 평가하는 식

값이 있는 orElse는 make를 먼저 호출하고 orElseGet은 생략하며 empty에서만 get을 호출합니다. 세 호출의 누적 eager=1/lazy=1을 실행 전후로 보여 줄 수 있습니다.

세 Optional 호출에서 실제로 평가하는 식

세 Optional 호출에서 실제로 평가하는 식

각 호출의 실행과 반환

세 Optional 호출에서 실제로 평가하는 식

값이 있는 orElse도 make를 실행합니다. orElseGet은 빈 상태인 세 번째 호출에서만 공급자 본문을 실행합니다.

Optional의 반환값과 실제 대체 식 실행을 따로 추적한다원main세호출을각입력상태→평가동작→반환의9노드6관계로펴서보인다. 각열desktop/각3노드mobile이한호출이고원실행순서는첫째둘째셋째다. 서로다른eager와lazycounter를합산하거나자동캐시로보지않으며원최종eager1/lazy1을보존한다.첫째: 값 있음orElse(make)인수 make 실행eager +1value 반환만든 대체 값 미사용둘째: 값 있음orElseGetSupplier 미호출lazy +0value 반환대체 계산 없음셋째: 빈 상태orElseGetSupplier get 호출lazy +1needed 반환최종 eager1 · lazy1
Optional의 반환값과 실제 대체 식 실행을 따로 추적한다원main세호출을각입력상태→평가동작→반환의9노드6관계로펴서보인다. 각열desktop/각3노드mobile이한호출이고원실행순서는첫째둘째셋째다. 서로다른eager와lazycounter를합산하거나자동캐시로보지않으며원최종eager1/lazy1을보존한다.첫째: 값 있음orElse(make)인수 make 실행eager +1value 반환만든 대체 값 미사용둘째: 값 있음orElseGetSupplier 미호출lazy +0value 반환대체 계산 없음셋째: 빈 상태orElseGetSupplier get 호출lazy +1needed 반환최종 eager1 · lazy1

첫째·값 있음

대체 계산
orElse 인수 make 실행; eager +1
반환
value

둘째·값 있음

대체 계산
orElseGet 공급자 미호출; lazy +0
반환
value

셋째·빈 상태

대체 계산
orElseGet 공급자 호출; lazy +1
반환
needed

OrElseAndOrElseGetTrace를 순서대로 읽으면 최종 카운터는 eager=1, lazy=1입니다. 이 횟수는 각 호출의 분기에서 도출한 것이며 다음 호출까지 값을 캐시한다는 뜻은 아닙니다.

이 예의 조건

  • 원 main의 세 호출 순서를 유지한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원 main의 세 호출을 각 입력 상태→평가 동작→반환의 9개 노드와 6개 관계로 펼쳐 보인다. desktop의 각 열/mobile의 각 3개 노드는 한 호출이고 원 실행 순서는 첫째, 둘째, 셋째다. 서로 다른 eager와 lazy counter를 합산하거나 자동 캐시로 보지 않으며 원 최종 eager 1/lazy 1을 보존한다.
20최종 eager 1 · lazy 1
원 도표의 설명과 주석
구분내용
설명원 main의 세 호출을 각 입력 상태→평가 동작→반환의 9개 노드와 6개 관계로 펼쳐 보인다. desktop의 각 열/mobile의 각 3개 노드는 한 호출이고 원 실행 순서는 첫째, 둘째, 셋째다. 서로 다른 eager와 lazy counter를 합산하거나 자동 캐시로 보지 않으며 원 최종 eager 1/lazy 1을 보존한다.
전체 읽기 정보세 Optional 호출에서 실제로 평가하는 식 세 Optional 호출에서 실제로 평가하는 식 세 Optional 호출에서 실제로 평가하는 식 각 호출의 실행과 반환 세 Optional 호출에서 실제로 평가하는 식 값이 있는 orElse도 make를 실행합니다. orElseGet은 빈 상태인 세 번째 호출에서만 공급자 본문을 실행합니다. 첫째·값 있음 대체 계산 orElse 인수 make 실행; eager +1 반환 value 둘째·값 있음 대체 계산 orElseGet 공급자 미호출; lazy +0 반환 value 셋째·빈 상태 대체 계산 orElseGet 공급자 호출; lazy +1 반환 needed OrElseAndOrElseGetTrace를 순서대로 읽으면 최종 카운터는 eager=1, lazy=1입니다. 이 횟수는 각 호출의 분기에서 도출한 것이며 다음 호출까지 값을 캐시한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
orElse 값 인수

값이 있어도 make(eager)를 먼저 계산합니다.

  • eager: 1
  • 반환: value
  • 인수 평가가 메서드 분기보다 먼저 일어난다.
orElseGet 값 있음

Supplier를 실행하지 않습니다.

  • lazy: 0
  • 반환: value
  • 값이 있으면 대체 계산을 생략한다.
orElseGet empty

Supplier.get으로 needed를 만듭니다.

  • lazy: 1
  • 반환: needed
  • 최종 카운터: eager 1, lazy 1
  • 빈 상태일 때에만 지연 계산이 실행된다.

세 Optional 호출에서 실제로 평가하는 식

값이 있는 orElse는 make를 먼저 호출하고 orElseGet은 생략하며 empty에서만 get을 호출합니다. 세 호출의 누적 eager=1/lazy=1을 실행 전후로 보여 줄 수 있습니다.

세 Optional 호출에서 실제로 평가하는 식

세 Optional 호출에서 실제로 평가하는 식

각 호출의 실행과 반환

세 Optional 호출에서 실제로 평가하는 식

값이 있는 orElse도 make를 실행합니다. orElseGet은 빈 상태인 세 번째 호출에서만 공급자 본문을 실행합니다.

Optional의 반환값과 실제 대체 식 실행을 따로 추적한다원main세호출을각입력상태→평가동작→반환의9노드6관계로펴서보인다. 각열desktop/각3노드mobile이한호출이고원실행순서는첫째둘째셋째다. 서로다른eager와lazycounter를합산하거나자동캐시로보지않으며원최종eager1/lazy1을보존한다.첫째: 값 있음orElse(make)인수 make 실행eager +1value 반환만든 대체 값 미사용둘째: 값 있음orElseGetSupplier 미호출lazy +0value 반환대체 계산 없음셋째: 빈 상태orElseGetSupplier get 호출lazy +1needed 반환최종 eager1 · lazy1
Optional의 반환값과 실제 대체 식 실행을 따로 추적한다원main세호출을각입력상태→평가동작→반환의9노드6관계로펴서보인다. 각열desktop/각3노드mobile이한호출이고원실행순서는첫째둘째셋째다. 서로다른eager와lazycounter를합산하거나자동캐시로보지않으며원최종eager1/lazy1을보존한다.첫째: 값 있음orElse(make)인수 make 실행eager +1value 반환만든 대체 값 미사용둘째: 값 있음orElseGetSupplier 미호출lazy +0value 반환대체 계산 없음셋째: 빈 상태orElseGetSupplier get 호출lazy +1needed 반환최종 eager1 · lazy1

첫째·값 있음

대체 계산
orElse 인수 make 실행; eager +1
반환
value

둘째·값 있음

대체 계산
orElseGet 공급자 미호출; lazy +0
반환
value

셋째·빈 상태

대체 계산
orElseGet 공급자 호출; lazy +1
반환
needed

OrElseAndOrElseGetTrace를 순서대로 읽으면 최종 카운터는 eager=1, lazy=1입니다. 이 횟수는 각 호출의 분기에서 도출한 것이며 다음 호출까지 값을 캐시한다는 뜻은 아닙니다.

이 예의 조건

  • 원 main의 세 호출 순서를 유지한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원 main의 세 호출을 각 입력 상태→평가 동작→반환의 9개 노드와 6개 관계로 펼쳐 보인다. desktop의 각 열/mobile의 각 3개 노드는 한 호출이고 원 실행 순서는 첫째, 둘째, 셋째다. 서로 다른 eager와 lazy counter를 합산하거나 자동 캐시로 보지 않으며 원 최종 eager 1/lazy 1을 보존한다.
20최종 eager 1 · lazy 1
원 도표의 설명과 주석
구분내용
설명원 main의 세 호출을 각 입력 상태→평가 동작→반환의 9개 노드와 6개 관계로 펼쳐 보인다. desktop의 각 열/mobile의 각 3개 노드는 한 호출이고 원 실행 순서는 첫째, 둘째, 셋째다. 서로 다른 eager와 lazy counter를 합산하거나 자동 캐시로 보지 않으며 원 최종 eager 1/lazy 1을 보존한다.
전체 읽기 정보세 Optional 호출에서 실제로 평가하는 식 세 Optional 호출에서 실제로 평가하는 식 세 Optional 호출에서 실제로 평가하는 식 각 호출의 실행과 반환 세 Optional 호출에서 실제로 평가하는 식 값이 있는 orElse도 make를 실행합니다. orElseGet은 빈 상태인 세 번째 호출에서만 공급자 본문을 실행합니다. 첫째·값 있음 대체 계산 orElse 인수 make 실행; eager +1 반환 value 둘째·값 있음 대체 계산 orElseGet 공급자 미호출; lazy +0 반환 value 셋째·빈 상태 대체 계산 orElseGet 공급자 호출; lazy +1 반환 needed OrElseAndOrElseGetTrace를 순서대로 읽으면 최종 카운터는 eager=1, lazy=1입니다. 이 횟수는 각 호출의 분기에서 도출한 것이며 다음 호출까지 값을 캐시한다는 뜻은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
orElse 값 인수

값이 있어도 make(eager)를 먼저 계산합니다.

  • eager: 1
  • 반환: value
  • 인수 평가가 메서드 분기보다 먼저 일어난다.
orElseGet 값 있음

Supplier를 실행하지 않습니다.

  • lazy: 0
  • 반환: value
  • 값이 있으면 대체 계산을 생략한다.
orElseGet empty

Supplier.get으로 needed를 만듭니다.

  • lazy: 1
  • 반환: needed
  • 최종 카운터: eager 1, lazy 1
  • 빈 상태일 때에만 지연 계산이 실행된다.

예시 첫 make(eager)는 value가 있어도 진입 전에 실행되고 만든 문자열은 반환에 쓰이지 않습니다. 둘째 make(lazy)는 참조만 전달돼 호출되지 않고, 셋째 make(needed)가 같은 lazy 카운터를 1로 만듭니다. 그래서 세 반환은 value,value,needed이고 카운터는 eager 1, lazy 1입니다. 반환 문자열만 검사하면 첫째의 불필요한 실행을 발견하지 못합니다.

독립 확인: Optional.of("value")와 make 함수를 유지하되 make가 카운터 증가 뒤 IllegalStateException을 던진다고 바꿉니다. orElse(make(...))와 orElseGet(() -> make(...))는 각각 정상 반환하나요? 별도로 Optional.empty()의 orElseGet은 어떤가요?

풀이 확인

값이 있는 orElse도 인수 계산 중 예외로 끝나 메서드 호출·정상 반환에 도달하지 않습니다. 값이 있는 orElseGet은 공급자 본문을 실행하지 않아 value를 반환하고 카운터도 늘지 않습니다. 별도 빈 Optional은 공급자를 실행해 카운터가 늘고 같은 예외가 전달됩니다. 대체 계산의 실패를 빈 결과로 처리한 것으로 읽으면 안 됩니다.


Optional 연쇄 탐색

User가 없을 수 있고 User.addressId도 null을 허용하는 레거시 필드라면 두 단계의 부재가 있습니다.

저장소의 Optional에 map을 적용했을 때 식별자가 null이면 Optional.map은 빈 상태로 바꿉니다.

주소 저장소가 반환한 Optional은 flatMap으로 연결합니다.

src/UserAddressLookup.java
import java.util.Map;
import java.util.Optional;

public final class UserAddressLookup {
    private record User(long id, Long addressId) {}

    private record Address(long id, String city) {}

    private static final Map<Long, User> USERS =
            Map.of(
                    1L, new User(1, 100L),
                    2L, new User(2, null));
    private static final Map<Long, Address> ADDRESSES = Map.of(100L, new Address(100, "Busan"));

    static Optional<Address> findAddress(long userId) {
        return Optional.ofNullable(USERS.get(userId))
                .map(User::addressId)
                .flatMap(addressId -> Optional.ofNullable(ADDRESSES.get(addressId)));
    }

    public static void main(String[] args) {
        System.out.println(findAddress(1).map(Address::city).orElse("unknown"));
        System.out.println(findAddress(2).map(Address::city).orElse("unknown"));
    }
}

원문은 첫 조회에서 Busan, 둘째 조회에서 문자열 unknown을 출력합니다.

이 예제는 메모리 Map을 조회하며 저장소 장애를 발생시키는 코드는 없습니다. 조회를 외부 저장소로 바꾼 경우에도 이 연쇄는 예외를 잡지 않으므로, 발생한 예외가 빈 상태로 바뀌지는 않습니다.

Optional 연쇄는 정상 부재만 표현합니다.


null 검사와 Optional 분기

명시적 null 검사는 각 단계별로 다른 대체 경로와 로깅이 필요할 때 읽기 좋습니다.

Optional 연쇄는 부재가 같은 방식으로 전파되고 최종 처리 하나면 충분할 때 간결합니다.

연쇄가 열 단계로 길어지면 도메인 메서드를 나눕니다.

Optional을 필드 그래프 순회의 만능 해결책으로 만들지 않습니다.

새 모델에서는 Address가 필수인지 선택인지 생성자와 관계 타입으로 표현할 수 있으므로 null 허용 식별자에 의존할 필요가 없습니다.

Optional은 저장소 조회 결과가 시스템에 들어오는 경계에서 가장 효과적입니다.


Order와 Delivery의 배송 조회

Order는 존재하지만 Delivery가 아직 생성되지 않을 수 있습니다.

배송이 없으면 대기 중 레이블을, 있으면 추적 번호를 반환합니다.

대기 중 값 생성이 단순 상수라면 orElse()도 안전하지만 로캘 조회 같은 비용이 있으면 orElseGet()을 선택합니다.

app/DeliveryStatusService.java
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public final class DeliveryStatusService {
    private record Order(long id, Long deliveryId) {}

    private record Delivery(long id, String trackingNumber) {}

    private final Map<Long, Order> orders = Map.of(1L, new Order(1, 10L), 2L, new Order(2, null));
    private final Map<Long, Delivery> deliveries = Map.of(10L, new Delivery(10, "T-100"));
    private final AtomicInteger fallbackLoads = new AtomicInteger();

    String trackingLabel(long orderId) {
        return Optional.ofNullable(orders.get(orderId))
                .map(Order::deliveryId)
                .flatMap(id -> Optional.ofNullable(deliveries.get(id)))
                .map(Delivery::trackingNumber)
                .orElseGet(this::pendingLabel);
    }

    private String pendingLabel() {
        fallbackLoads.incrementAndGet();
        return "PENDING";
    }

    public static void main(String[] args) {
        DeliveryStatusService service = new DeliveryStatusService();
        System.out.println(service.trackingLabel(1));
        System.out.println(service.trackingLabel(2));
        System.out.println("fallback-loads=" + service.fallbackLoads.get());
    }
}

추적 번호가 있는 첫 호출은 대체 경로를 건너뛰고 둘째 호출만 PENDING을 만들어 fallback-loads=1입니다.

예제의 연쇄에는 배송이 없을 때의 PENDING 값을 저장하는 캐시가 없습니다. 같은 주문 2를 다시 조회하면 다시 pendingLabel을 호출하여 누적 fallbackLoads는 2가 됩니다. 주문 1을 다시 조회하면 T-100을 얻고 횟수는 늘지 않습니다.

주문 자체가 없을 때도 PENDING으로 보일지 NOT_FOUND로 구분할지 제품 규칙을 정해야 합니다.


Optional.or의 지연 대체

값이 아니라 다른 Optional 조회를 대체 경로로 사용하려면 primary.or(() -> secondary())가 유용합니다.

기본값이 있으면 secondary 공급자를 호출하지 않고 결과도 Optional로 유지합니다.

orElseGet은 T를 반환하므로 Optional을 다시 감싸는 용도가 아닙니다.

캐시 조회 뒤 데이터베이스 조회처럼 원본의 우선순위가 있을 때 사용할 수 있습니다.

보조 조회의 실패를 빈 상태로 숨기지 말고 예외 정책을 유지합니다.


orElseThrow로 필수 관계 확인

배송 생성 명령처럼 Address가 반드시 있어야 작업을 시작할 수 있다면 orElseThrow(() -> new MissingAddressException(userId))로 불변식을 명시합니다.

단순 조회 화면에 “알 수 없음”을 표시하는 대체 경로와 명령 입력 검사의 예외는 요구 사항이 다릅니다.

Supplier를 사용하면 메시지 서식 처리와 예외 객체 생성도 값이 없는 경로에서만 수행됩니다.

예외 타입에는 호출자가 복구 여부를 판단할 수 있는 의미를 담습니다.


연습 문제

User.primaryEmail이 있으면 사용하고, 없으면 Profile 저장소에서 이메일을 찾으세요.

둘 다 없으면 [email protected]을 반환합니다.

대체 프로필 경로에 진입한 횟수를 세어 기본 이메일이 있을 때는 0인지 확인하세요.

정답과 Optional.or

기본 Optional 뒤의 or 공급자에서만 프로필 저장소를 조회합니다.

최종 기본값은 값싼 상수이므로 orElse를 씁니다.

exercise/LazyEmailFallbackSolution.java
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public final class LazyEmailFallbackSolution {
    private record User(Long profileId, String primaryEmail) {}

    private record Profile(long id, String email) {}

    private static String email(User user, Map<Long, Profile> profiles, AtomicInteger lookups) {
        return Optional.ofNullable(user.primaryEmail())
                .or(
                        () -> {
                            lookups.incrementAndGet();
                            return Optional.ofNullable(user.profileId())
                                    .map(profiles::get)
                                    .map(Profile::email);
                        })
                .orElse("[email protected]");
    }

    public static void main(String[] args) {
        Map<Long, Profile> profiles = Map.of(10L, new Profile(10, "[email protected]"));
        AtomicInteger lookups = new AtomicInteger();
        System.out.println(email(new User(10L, "[email protected]"), profiles, lookups));
        System.out.println(email(new User(10L, null), profiles, lookups));
        System.out.println("lookups=" + lookups.get());
    }
}

대체 경로 카운터와 실제 Map 조회는 다르다

primaryEmail이 있으면 공급자와 Map 조회를 건너뛰고 없을 때 lookups 증가 후 ID와 프로필을 조회합니다. 사용자별 분기와 두 카운터의 의미를 같은 역할로 추적할 수 있습니다.

대체 경로 카운터와 실제 Map 조회는 다르다

대체 경로 카운터와 실제 Map 조회는 다르다

경로 진입과 실제 조회

대체 경로 카운터와 실제 Map 조회는 다르다

lookups는 or 공급자 진입 횟수입니다. profileId가 없으면 카운터는 증가해도 profiles.get은 실행하지 않습니다.

or 공급자 진입과 저장소 조회의 경계가 서로 다르다원LazyEmailFallbackSolution의primary값분기는기본이메일반환이고부재분기는먼저lookups증가뒤profileId를확인한다. ID가없으면Map.get을생략하고기본anonymous주소를선택하며ID가있으면저장소조회후프로필이메일또는미적중anonymous이다. 7노드7관계로mobile동일. primary빈String은값있음이다. originalmain은두입력만호출하며다른경로는코드조건추론이다.primaryEmail 값 확인있으면 기본 이메일기본 이메일 반환lookups0 · get0or 공급자 진입lookups +1profileId 값 확인있으면 Map 조회Map.get → email프로필·이메일 찾기프로필 이메일 반환ID10 원 예제anonymous 기본 주소ID 없음 · 조회 미적중
or 공급자 진입과 저장소 조회의 경계가 서로 다르다원LazyEmailFallbackSolution의primary값분기는기본이메일반환이고부재분기는먼저lookups증가뒤profileId를확인한다. ID가없으면Map.get을생략하고기본anonymous주소를선택하며ID가있으면저장소조회후프로필이메일또는미적중anonymous이다. 7노드7관계로mobile동일. primary빈String은값있음이다. originalmain은두입력만호출하며다른경로는코드조건추론이다.primaryEmail 값 확인있으면 기본 이메일기본 이메일 반환lookups0 · get0or 공급자 진입lookups +1profileId 값 확인있으면 Map 조회Map.get → email프로필·이메일 찾기프로필 이메일 반환ID10 원 예제anonymous 기본 주소ID 없음 · 조회 미적중

기본 이메일 있음

lookups 증가 / Map.get 호출
0 / 0회
반환
기본 이메일

기본 없음·프로필 10

lookups 증가 / Map.get 호출
1 / 1회

기본 없음·프로필 ID 없음

lookups 증가 / Map.get 호출
1 / 0회

앞의 두 행은 main의 입력입니다. 마지막 행은 원문에 호출을 추가하지 않고 null profileId 분기를 읽은 비교입니다. lookups는 or 공급자 진입 횟수를 셉니다.

이 예의 조건

  • primary의 빈 String도 값 있음이며 원 main의 다른 경로는 코드 조건입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원 LazyEmailFallbackSolution의 primary 값 분기는 기본 이메일을 반환하고 부재 분기는 먼저 lookups를 증가시킨 뒤 profileId를 확인한다. ID가 없으면 Map.get을 생략하고 기본 anonymous 주소를 선택하며 ID가 있으면 저장소 조회 후 프로필 이메일 또는 미적중 시 anonymous를 반환한다. 7개 노드와 7개 관계는 mobile도 동일하다. primary의 빈 String도 값이 있는 상태이다. 원 main은 두 입력만 호출하며 다른 경로는 코드 조건에서 추론한 것이다.
14ID 10 원 예제
원 도표의 설명과 주석
구분내용
설명원 LazyEmailFallbackSolution의 primary 값 분기는 기본 이메일을 반환하고 부재 분기는 먼저 lookups를 증가시킨 뒤 profileId를 확인한다. ID가 없으면 Map.get을 생략하고 기본 anonymous 주소를 선택하며 ID가 있으면 저장소 조회 후 프로필 이메일 또는 미적중 시 anonymous를 반환한다. 7개 노드와 7개 관계는 mobile도 동일하다. primary의 빈 String도 값이 있는 상태이다. 원 main은 두 입력만 호출하며 다른 경로는 코드 조건에서 추론한 것이다.
전체 읽기 정보대체 경로 카운터와 실제 Map 조회는 다르다 대체 경로 카운터와 실제 Map 조회는 다르다 대체 경로 카운터와 실제 Map 조회는 다르다 경로 진입과 실제 조회 대체 경로 카운터와 실제 Map 조회는 다르다 lookups는 or 공급자 진입 횟수입니다. profileId가 없으면 카운터는 증가해도 profiles.get은 실행하지 않습니다. 기본 이메일 있음 lookups 증가 / Map.get 호출 0 / 0회 반환 기본 이메일 기본 없음·프로필 10 lookups 증가 / Map.get 호출 1 / 1회 반환 [email protected] 기본 없음·프로필 ID 없음 lookups 증가 / Map.get 호출 1 / 0회 반환 [email protected] 앞의 두 행은 main의 입력입니다. 마지막 행은 원문에 호출을 추가하지 않고 null profileId 분기를 읽은 비교입니다. lookups는 or 공급자 진입 횟수를 셉니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
primary 존재

값이 있으면 기본 이메일을 그대로 반환합니다.

  • lookups: 0
  • Map.get: 0
  • or 공급자에 진입하지 않습니다.
공급자 진입

primary 부재이면 lookups를 먼저 늘리고 ID를 확인합니다.

  • lookups: +1
  • ID 없음: Map.get 생략, anonymous
  • 공급자 호출 횟수는 저장소 조회 횟수와 다릅니다.
ID 조회

ID가 있으면 Map.get으로 프로필 이메일을 찾습니다.

  • ID 10: 프로필 이메일
  • 미적중: anonymous
  • ID가 있어도 조회 결과가 없을 때 기본값을 선택할 수 있습니다.

대체 경로 카운터와 실제 Map 조회는 다르다

primaryEmail이 있으면 공급자와 Map 조회를 건너뛰고 없을 때 lookups 증가 후 ID와 프로필을 조회합니다. 사용자별 분기와 두 카운터의 의미를 같은 역할로 추적할 수 있습니다.

대체 경로 카운터와 실제 Map 조회는 다르다

대체 경로 카운터와 실제 Map 조회는 다르다

경로 진입과 실제 조회

대체 경로 카운터와 실제 Map 조회는 다르다

lookups는 or 공급자 진입 횟수입니다. profileId가 없으면 카운터는 증가해도 profiles.get은 실행하지 않습니다.

or 공급자 진입과 저장소 조회의 경계가 서로 다르다원LazyEmailFallbackSolution의primary값분기는기본이메일반환이고부재분기는먼저lookups증가뒤profileId를확인한다. ID가없으면Map.get을생략하고기본anonymous주소를선택하며ID가있으면저장소조회후프로필이메일또는미적중anonymous이다. 7노드7관계로mobile동일. primary빈String은값있음이다. originalmain은두입력만호출하며다른경로는코드조건추론이다.primaryEmail 값 확인있으면 기본 이메일기본 이메일 반환lookups0 · get0or 공급자 진입lookups +1profileId 값 확인있으면 Map 조회Map.get → email프로필·이메일 찾기프로필 이메일 반환ID10 원 예제anonymous 기본 주소ID 없음 · 조회 미적중
or 공급자 진입과 저장소 조회의 경계가 서로 다르다원LazyEmailFallbackSolution의primary값분기는기본이메일반환이고부재분기는먼저lookups증가뒤profileId를확인한다. ID가없으면Map.get을생략하고기본anonymous주소를선택하며ID가있으면저장소조회후프로필이메일또는미적중anonymous이다. 7노드7관계로mobile동일. primary빈String은값있음이다. originalmain은두입력만호출하며다른경로는코드조건추론이다.primaryEmail 값 확인있으면 기본 이메일기본 이메일 반환lookups0 · get0or 공급자 진입lookups +1profileId 값 확인있으면 Map 조회Map.get → email프로필·이메일 찾기프로필 이메일 반환ID10 원 예제anonymous 기본 주소ID 없음 · 조회 미적중

기본 이메일 있음

lookups 증가 / Map.get 호출
0 / 0회
반환
기본 이메일

기본 없음·프로필 10

lookups 증가 / Map.get 호출
1 / 1회

기본 없음·프로필 ID 없음

lookups 증가 / Map.get 호출
1 / 0회

앞의 두 행은 main의 입력입니다. 마지막 행은 원문에 호출을 추가하지 않고 null profileId 분기를 읽은 비교입니다. lookups는 or 공급자 진입 횟수를 셉니다.

이 예의 조건

  • primary의 빈 String도 값 있음이며 원 main의 다른 경로는 코드 조건입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원 LazyEmailFallbackSolution의 primary 값 분기는 기본 이메일을 반환하고 부재 분기는 먼저 lookups를 증가시킨 뒤 profileId를 확인한다. ID가 없으면 Map.get을 생략하고 기본 anonymous 주소를 선택하며 ID가 있으면 저장소 조회 후 프로필 이메일 또는 미적중 시 anonymous를 반환한다. 7개 노드와 7개 관계는 mobile도 동일하다. primary의 빈 String도 값이 있는 상태이다. 원 main은 두 입력만 호출하며 다른 경로는 코드 조건에서 추론한 것이다.
14ID 10 원 예제
원 도표의 설명과 주석
구분내용
설명원 LazyEmailFallbackSolution의 primary 값 분기는 기본 이메일을 반환하고 부재 분기는 먼저 lookups를 증가시킨 뒤 profileId를 확인한다. ID가 없으면 Map.get을 생략하고 기본 anonymous 주소를 선택하며 ID가 있으면 저장소 조회 후 프로필 이메일 또는 미적중 시 anonymous를 반환한다. 7개 노드와 7개 관계는 mobile도 동일하다. primary의 빈 String도 값이 있는 상태이다. 원 main은 두 입력만 호출하며 다른 경로는 코드 조건에서 추론한 것이다.
전체 읽기 정보대체 경로 카운터와 실제 Map 조회는 다르다 대체 경로 카운터와 실제 Map 조회는 다르다 대체 경로 카운터와 실제 Map 조회는 다르다 경로 진입과 실제 조회 대체 경로 카운터와 실제 Map 조회는 다르다 lookups는 or 공급자 진입 횟수입니다. profileId가 없으면 카운터는 증가해도 profiles.get은 실행하지 않습니다. 기본 이메일 있음 lookups 증가 / Map.get 호출 0 / 0회 반환 기본 이메일 기본 없음·프로필 10 lookups 증가 / Map.get 호출 1 / 1회 반환 [email protected] 기본 없음·프로필 ID 없음 lookups 증가 / Map.get 호출 1 / 0회 반환 [email protected] 앞의 두 행은 main의 입력입니다. 마지막 행은 원문에 호출을 추가하지 않고 null profileId 분기를 읽은 비교입니다. lookups는 or 공급자 진입 횟수를 셉니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
primary 존재

값이 있으면 기본 이메일을 그대로 반환합니다.

  • lookups: 0
  • Map.get: 0
  • or 공급자에 진입하지 않습니다.
공급자 진입

primary 부재이면 lookups를 먼저 늘리고 ID를 확인합니다.

  • lookups: +1
  • ID 없음: Map.get 생략, anonymous
  • 공급자 호출 횟수는 저장소 조회 횟수와 다릅니다.
ID 조회

ID가 있으면 Map.get으로 프로필 이메일을 찾습니다.

  • ID 10: 프로필 이메일
  • 미적중: anonymous
  • ID가 있어도 조회 결과가 없을 때 기본값을 선택할 수 있습니다.

main의 Map.of는 null을 허용하지 않습니다. 다른 Map이 null 값을 반환하더라도 이 연쇄의 map은 이를 빈 상태로 처리하므로 미등록 프로필과 구분하지 않습니다.

독립 확인: 이메일 연습에서 같은 profiles와 처음 0인 lookups로 (profileId=null,primaryEmail=null), (profileId=99,primaryEmail=null), (profileId=null,primaryEmail="")를 순서대로 조회합니다. 각 반환 값·누적 lookups·실제 profiles.get의 호출 횟수를 구하세요.

풀이 확인

첫 조회는 [email protected], lookups 1이지만 ID가 비어 Map.get은 0회입니다. 다음 조회는 같은 기본 주소, lookups 2이며 ID 99로 Map.get을 한 번 실행합니다. 마지막 빈 문자열은 값이 있는 Optional이므로 빈 문자열을 그대로 반환하고 lookups 2·Map.get 누적 1회를 유지합니다. lookups는 저장소 함수가 아니라 or 공급자에 들어온 횟수이므로 지표 이름만으로 같은 값이라 가정하지 않습니다.

orElse와 orElseGet의 차이는 반환값이 아니라 대체 경로 식의 실행 시점입니다.

주소·배송처럼 조회 단계가 여러 개인 경우 Optional 연쇄의 부재와 실제 장애를 구분하고, 비용이 큰 대체 경로는 Supplier 경계 뒤에 둬야 합니다.