Java Optional 제대로 사용하는 방법 — 흔한 실수와 올바른 패턴 정리

2026-07-09


사진: Unsplash 의 Luca Bravo


개요

Java 8부터 도입된 Optional은 NullPointerException을 방지하고 null 처리를 명시적으로 표현하기 위한 컨테이너 클래스입니다.

그러나 실무에서 잘못 사용하면 오히려 코드가 더 복잡해지거나, null 체크 수준에 머무는 경우가 많습니다. 이번 글에서는 Optional의 올바른 사용 패턴과 피해야 할 안티패턴을 정리합니다.


Optional 기본 생성 방법

// 값이 있을 때
Optional<String> opt1 = Optional.of("hello");

// null일 수도 있을 때
Optional<String> opt2 = Optional.ofNullable(getValue());

// 빈 Optional
Optional<String> opt3 = Optional.empty();

Optional.of(null) 은 즉시 NullPointerException을 던집니다. null 가능성이 있다면 반드시 ofNullable을 사용하세요.


❌ 흔한 안티패턴

1. isPresent() + get() 조합 — if-null 체크와 다를 게 없음

// ❌ 안티패턴: Optional을 쓰는 의미가 없음
Optional<User> userOpt = userRepository.findById(id);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    System.out.println(user.getName());
}

2. Optional을 메서드 파라미터로 사용

// ❌ 안티패턴: 파라미터에 Optional 쓰지 말 것
public void process(Optional<String> name) { ... }

Optional은 반환 타입에만 사용하는 것이 원칙입니다. 파라미터로 넘기면 호출자가 Optional.of()로 감싸야 하는 번거로움이 생기고 가독성이 떨어집니다.

3. Optional을 필드로 선언

// ❌ 안티패턴: 직렬화 불가, 설계 의도와 맞지 않음
public class User {
    private Optional<String> nickname;
}

✅ 올바른 사용 패턴

1. orElse() / orElseGet() 으로 기본값 처리

// orElse: 항상 기본값 객체를 생성함
String name = userOpt.orElse("익명");

// orElseGet: 람다로 지연 평가 → 생성 비용이 있을 때 유리
String name = userOpt.orElseGet(() -> generateDefaultName());

구분 orElse() orElseGet()

기본값 평가 시점 Optional 상태와 무관하게 항상 실행 Optional이 비어있을 때만 실행
사용 권장 단순 상수, 리터럴 메서드 호출, 객체 생성 등 비용 있는 경우

2. map() / flatMap() 으로 체이닝

// ✅ null 체크 없이 안전하게 변환
Optional<String> city = userRepository.findById(id)
    .map(User::getAddress)
    .map(Address::getCity);

3. orElseThrow() 로 예외 처리

// ✅ 값이 없으면 명시적 예외
User user = userRepository.findById(id)
    .orElseThrow(() -> new IllegalArgumentException("유저를 찾을 수 없습니다. id=" + id));

4. ifPresent() / ifPresentOrElse() 로 사이드 이펙트 처리

// 값이 있을 때만 실행
userOpt.ifPresent(user -> log.info("로그인: {}", user.getName()));

// Java 9+: 있을 때 / 없을 때 분기
userOpt.ifPresentOrElse(
    user -> log.info("유저 존재: {}", user.getName()),
    () -> log.warn("유저 없음")
);

filter() 활용 — 조건부 처리

// 특정 조건을 만족하는 경우에만 처리
Optional<User> activeUser = userRepository.findById(id)
    .filter(user -> user.isActive());

activeUser.ifPresent(u -> sendWelcomeEmail(u));

Stream과 연계 (Java 9+)

// Optional → Stream 변환: stream()
List<String> names = userIds.stream()
    .map(userRepository::findById)   // Stream<Optional<User>>
    .flatMap(Optional::stream)        // Stream<User> (비어있는 Optional 제거)
    .map(User::getName)
    .collect(Collectors.toList());

Java 9 이전에는 filter(Optional::isPresent).map(Optional::get) 패턴을 써야 했지만, Optional::stream으로 훨씬 간결하게 처리할 수 있습니다.


정리

상황 권장 방법

기본값이 필요할 때 orElse() / orElseGet()
예외를 던져야 할 때 orElseThrow()
값을 변환할 때 map() / flatMap()
사이드 이펙트 처리 ifPresent() / ifPresentOrElse()
조건 필터링 filter()
절대 쓰면 안 되는 곳 메서드 파라미터, 클래스 필드, 컬렉션 원소

결론

Optional은 null 대신 "값이 없을 수도 있다"는 의도를 타입으로 표현하는 도구입니다. isPresent() + get() 조합은 지양하고, map(), orElseGet(), orElseThrow() 중심의 함수형 스타일로 작성하면 코드가 훨씬 명확해집니다.


메인 이미지 출처 : 사진: UnsplashLuca Bravo

태그: Java, Optional, Java8, NullPointerException, 자바 카테고리: Language/Java