Java 8에서 도입된 Optional은 null 참조로 인한 NullPointerException을 방지하기 위해 설계된 컨테이너 객체입니다. 값의 유무를 명시적으로 표현하며, isPresent()로 존재 여부를 확인하고 get()으로 실제 값을 추출할 수 있습니다.
Optional 생성 방식
JDK는 세 가지 정적 팩토리 메소드를 제공합니다:
| 메소드 | 설명 |
|---|---|
Optional.of(T value) | non-null 값으로 Optional 생성, null 전달 시 N 발생 |
Optional.ofNullable(T value) | null 허용, null 시 Optional.empty() 반환 |
Optional.empty() | 값이 없는 빈 Optional 생성 |
핵심 메소드 활용
1. ifPresent - 조건부 실행
값이 존재할 때만 지정된 동작을 수행합니다. 기존 if-null 체크를 대체합니다.
@Component
@Slf4j
public class EventSubscriber {
@KafkaListener(topics = {"order-events"})
public void process(ConsumerRecord<String, OrderPayload> event) {
Optional.ofNullable(event.value())
.ifPresent(payload -> {
log.info("이벤트 수신: key={}, payload={}", event.key(), payload);
metrics.counter("kafka.event.received").increment();
});
}
}
다른 예시로, 사용자 조회 후 존재하면 출력하는 경우:
Optional<Member> member = Optional.ofNullable(repository.findByMemberNo(memberNo));
member.ifPresent(m ->
System.out.println("닉네임: " + m.getNickname())
);
2. orElse vs orElseGet - 기본값 제공
orElse는 항상 인자를 평가하고, orElseGet은 필요할 때만 Supplier를 실행합니다.
// orElse: 기본값이 이 존재하거나 생성 비용이 낮을 때
Member fallback = new Member(GuestPolicy.ID, "방문자");
Member current = Optional.ofNullable(cache.get(memberId)).orElse(fallback);
// orElseGet: 생성 비용이 높거나 동적으로 생성해야 할 때
Member result = Optional.ofNullable(cache.get(memberId))
.orElseGet(() -> memberService.fetchFromDatabase(memberId));
3. orElseThrow - 예외 발생
값 부재 시 명시적 예외를 던져 상위 계층에서 처리하도록 합니다.
@RestController
@RequestMapping("/api/v1/items")
public class ItemController {
@GetMapping("/{itemCode}")
public ItemDetail fetchItem(@PathVariable String itemCode) {
return itemRepository.findByCode(itemCode)
.orElseThrow(() -> new ItemNotFoundException(
String.format("상품 코드 %s를 찾을 수 없습니다", itemCode)
));
}
}
전역 예외 처리와 결합하면 깔끔한 에러 응답을 구성할 수 있습니다:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ItemNotFoundException.class)
public ResponseEntity<ErrorBody> handleNotFound(ItemNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorBody(ex.getMessage(), Instant.now()));
}
}
4. map - 값 변환
값이 존재하면 함수를 적용하여 새로운 Optional을 반환합니다. null 반환 시 Optional.empty()로 안전하게 처리됩니다.
Optional<String> displayName = Optional.ofNullable(fetchMember(memberId))
.map(Member::getProfile)
.map(Profile::getDisplayName)
.map(name -> name.trim().toLowerCase());
System.out.println("표시 이름: " + displayName.orElse("미설정"));
5. flatMap - 중첩 Optional 평탄화
변환 함수가 이미 Optional을 반환할 때 사용하여 중첩을 제거합니다.
public Optional<ContactInfo> findPrimaryContact(Long memberId) {
return Optional.ofNullable(memberRepository.findById(memberId))
.flatMap(Member::getPrimaryContact) // Optional<ContactInfo> 반환
.flatMap(this::validateContact);
}
private Optional<ContactInfo> validateContact(ContactInfo contact) {
return contact.isVerified() ? Optional.of(contact) : Optional.empty();
}
6. filter - 조건부 필터링
Predicate를 만족하지 않으면 Optional.empty()가 됩니다.
Optional<Order> eligibleOrder = Optional.ofNullable(orderService.findById(orderId))
.filter(o -> o.getStatus() == OrderStatus.PAID)
.filter(o -> o.getAmount() >= MINIMUM_FREE_SHIPPING);
eligibleOrder.ifPresentOrElse(
order -> shipService.scheduleDelivery(order),
() -> notification.send("배송 불가 상황 안내")
);
메소드 체이닝 패턴
여러 메소드를 조합하여 선언적으로 null 안전성을 확보할 수 있습니다:
public ShippingEstimate calculateEstimate(Long orderId, String couponCode) {
return Optional.ofNullable(orderRepository.findById(orderId))
.filter(order -> !order.isCancelled())
.map(order -> ShippingCalculator.builder()
.weight(order.totalWeight())
.destination(order.getShippingAddress())
.coupon(Optional.ofNullable(couponCode)
.flatMap(couponRepository::findByCode)
.filter(Coupon::isValidNow)
.orElse(null))
.build()
.compute())
.orElse(ShippingEstimate.unavailable());
}
주의사항
Optional은 반환 타입에 주로 사용하며, 필드나 메소드 파라미터로는 지양합니다get()은 값이 없을 때NoSuchElementException을 발생시키므로, 선호도:orElse>orElseGet>orElseThrow>get- 스트림 파이프라인에서
flatMap은Optional<Optional<T>>형태를 방지하는 핵심 도구입니다