Optional과 ifPresent를 활용한 null 안전성 처리

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
  • 스트림 파이프라인에서 flatMapOptional<Optional<T>> 형태를 방지하는 핵심 도구입니다

태그: java Optional NullPointerException Functional Programming kafka

5월 30일 17:25에 게시됨