orElse·orElseGet
값이 있는 상태 `Optional`에서도 실행되는 orElse 인수를 확인하고 중첩 주소와 배송 상태를 `map`·flatMap·지연 대체 경로로 처리합니다.
orElse(value)의 인수는 일반 Java 인수라 Optional이 값이 있는 상태여도 먼저 계산됩니다.
orElseGet(supplier)는 빈 상태일 때만 Supplier를 호출합니다.
결과 문자열이 같아도 비용, 부수 효과, 예외 시점이 다르므로 대체 경로 생성 방식에 맞춰 선택합니다.
값 존재 시 불필요한 대체 조회
아래 Optional에는 이미 주소가 있지만 orElse(loadDefault())의 인수가 즉시 평가됩니다.
실제 출력은 address=Seoul, wrong-queries=1입니다.
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 객체 생성과 캡처 비용은 발생할 수 있습니다.
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;
}
}즉시 평가의 호출 횟수는 1이고 지연 평가도 빈 상태 분기를 한 번 실행하므로 1입니다. 값이 있는 분기만 비교하면 지연 호출은 0입니다. 대체 경로를 여러 번 호출하지 않는다는 보장은 해당 메서드 호출 한 번의 범위에만 적용됩니다.
Optional 연쇄 탐색
User가 없을 수 있고 User.addressId도 null을 허용하는 레거시 필드라면 두 단계의 부재가 있습니다.
저장소의 Optional에 map을 적용했을 때 식별자가 null이면 Optional.map은 빈 상태로 바꿉니다.
주소 저장소가 반환한 Optional은 flatMap으로 연결합니다.
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, 둘째는 알 수 없음입니다.
Address 저장소의 장애를 빈 상태로 바꾸지 않고 실제 예외는 밖으로 전달합니다.
Optional 연쇄는 정상 부재만 표현합니다.
null 검사와 Optional 분기
명시적 null 검사는 각 단계별로 다른 대체 경로와 로깅이 필요할 때 읽기 좋습니다.
Optional 연쇄는 부재가 같은 방식으로 전파되고 최종 처리 하나면 충분할 때 간결합니다.
연쇄가 열 단계로 길어지면 도메인 메서드를 나눕니다.
Optional을 필드 그래프 순회의 만능 해결책으로 만들지 않습니다.
새 모델에서는 Address가 필수인지 선택인지 생성자와 관계 타입으로 표현할 수 있으므로 null 허용 식별자에 의존할 필요가 없습니다.
Optional은 저장소 조회 결과가 시스템에 들어오는 경계에서 가장 효과적입니다.
Order와 Delivery의 배송 조회
Order는 존재하지만 Delivery가 아직 생성되지 않을 수 있습니다.
배송이 없으면 대기 중 레이블을, 있으면 추적 번호를 반환합니다.
대기 중 값 생성이 단순 상수라면 orElse()도 안전하지만 로캘 조회 같은 비용이 있으면 orElseGet()을 선택합니다.
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으로 보일지 NOT_FOUND로 구분할지 제품 규칙을 정해야 합니다.
Optional.or의 지연 대체
값이 아니라 다른 Optional 조회를 대체 경로로 사용하려면 primary.or(() -> secondary())가 유용합니다.
기본 값이 있으면 secondary 공급자를 호출하지 않고 결과도 Optional로 유지합니다.
orElseGet은 T를 반환하므로 Optional을 다시 감싸는 용도가 아닙니다.
캐시 조회 뒤 데이터베이스 조회처럼 원본의 우선순위가 있을 때 사용할 수 있습니다. 보조 조회의 실패를 빈 상태로 숨기지 말고 예외 정책을 유지합니다.
orElseThrow로 필수 관계 확인
배송 생성 명령처럼 Address가 반드시 있어야 작업을 시작할 수 있다면 orElseThrow(() -> new MissingAddressException(userId))로 불변식을 명시합니다.
단순 조회 화면에 “알 수 없음”을 표시하는 대체 경로와 명령 입력 검사의 예외는 요구 사항이 다릅니다.
Supplier를 사용하면 메시지 서식 처리와 예외 객체 생성도 값이 없는 경로에서만 수행됩니다.
예외 타입에는 호출자가 복구 여부를 판단할 수 있는 의미를 담습니다.
연습 문제
User.primaryEmail이 있으면 사용하고, 없으면 Profile 저장소에서 이메일을 찾으세요.
둘 다 없으면 anonymous@example.com을 반환합니다.
프로필 조회 횟수를 세어 기본 이메일이 있을 때는 0인지 확인하세요.
정답과 Optional.or
기본 Optional 뒤의 or 공급자에서만 프로필 저장소를 조회합니다.
최종 기본값은 값싼 상수이므로 orElse를 씁니다.
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("anonymous@example.com");
}
public static void main(String[] args) {
Map<Long, Profile> profiles = Map.of(10L, new Profile(10, "profile@example.com"));
AtomicInteger lookups = new AtomicInteger();
System.out.println(email(new User(10L, "primary@example.com"), profiles, lookups));
System.out.println(email(new User(10L, null), profiles, lookups));
System.out.println("lookups=" + lookups.get());
}
}두 결과는 기본 이메일과 프로필 이메일이며 프로필 조회는 한 번뿐입니다.
프로필 Map이 null 값을 허용하지 않는다는 전제도 유지합니다.
orElse와 orElseGet의 차이는 반환값이 아니라 대체 경로 식의 실행 시점입니다.
주소·배송처럼 조회 단계가 여러 개인 경우 Optional 연쇄의 부재와 실제 장애를 구분하고, 비용이 큰 대체 경로는 Supplier 경계 뒤에 둬야 합니다.