Java Optional

Optional이란?

Java 8에서 도입된 Optional<T>는 null이 될 수 있는 값을 감싸는 컨테이너 객체입니다. NullPointerException을 방지하고 명시적으로 값의 부재를 표현할 수 있습니다.

Optional 생성

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

// null이 아닌 값으로 생성
Optional<String> of = Optional.of("Hello");

// null일 수 있는 값으로 생성
Optional<String> nullable = Optional.ofNullable(possiblyNull);

⚠️ 주의: Optional.of(null)은 NullPointerException을 발생시킵니다.

값 확인 및 추출

기본 메서드

optional.isPresent()     // 값이 있으면 true
optional.isEmpty()       // 값이 없으면 true (Java 11+)
optional.get()           // 값 반환 (없으면 NoSuchElementException)

안전한 값 추출

// 기본값 제공
String result = optional.orElse("default");

// 기본값을 Supplier로 제공 (lazy evaluation)
String result = optional.orElseGet(() -> getDefaultValue());

// 예외 던지기
String result = optional.orElseThrow();
String result = optional.orElseThrow(() -> new CustomException());

함수형 메서드

ifPresent / ifPresentOrElse

// 값이 있을 때만 실행
optional.ifPresent(value -> System.out.println(value));

// Java 9+: 값이 있을 때와 없을 때 각각 처리
optional.ifPresentOrElse(
    value -> System.out.println(value),
    () -> System.out.println("No value")
);

map

Optional<String> name = Optional.of("John");
Optional<Integer> length = name.map(String::length);

// 체이닝
Optional<String> upper = name
    .map(String::trim)
    .map(String::toUpperCase);

flatMap

중첩된 Optional을 평탄화할 때 사용:

public Optional<Address> getAddress(User user) {
    return Optional.ofNullable(user)
        .flatMap(User::getAddress);
}

// User 클래스
public Optional<Address> getAddress() {
    return Optional.ofNullable(address);
}

filter

Optional<String> filtered = optional
    .filter(s -> s.length() > 5)
    .filter(s -> s.startsWith("A"));

베스트 프랙티스

✅ DO (권장사항)

1. 반환 타입으로만 사용

public Optional<User> findUserById(Long id) {
    return userRepository.findById(id);
}

2. orElse vs orElseGet 올바르게 선택

// ❌ 매번 새 객체 생성
result.orElse(new User());

// ✅ 필요할 때만 생성
result.orElseGet(() -> new User());

3. ifPresent 대신 함수형 메서드 활용

// ❌ 절차적 스타일
if (optional.isPresent()) {
    String value = optional.get();
    System.out.println(value.toUpperCase());
}

// ✅ 함수형 스타일
optional.map(String::toUpperCase)
    .ifPresent(System.out::println);

4. Optional 체이닝으로 null 체크 제거

// ❌ 전통적인 null 체크
String city = null;
if (user != null && user.getAddress() != null) {
    city = user.getAddress().getCity();
}

// ✅ Optional 체이닝
String city = Optional.ofNullable(user)
    .flatMap(User::getAddress)
    .map(Address::getCity)
    .orElse("Unknown");

❌ DON’T (안티패턴)

1. 필드로 사용하지 말 것

// ❌ 직렬화 문제, 메모리 오버헤드
public class User {
    private Optional<String> middleName;
}

// ✅ null 허용
public class User {
    private String middleName; // can be null
}

2. 메서드 파라미터로 사용하지 말 것

// ❌ 불필요한 복잡성
public void updateUser(Optional<User> user) { }

// ✅ 오버로딩 또는 null 허용
public void updateUser(User user) { }

3. 컬렉션을 Optional로 감싸지 말 것

// ❌ 불필요한 래핑
public Optional<List<User>> getUsers() {
    return Optional.ofNullable(users);
}

// ✅ 빈 컬렉션 반환
public List<User> getUsers() {
    return users != null ? users : Collections.emptyList();
}

4. get() 직접 호출 지양

// ❌ NoSuchElementException 위험
String value = optional.get();

// ✅ 안전한 대안 사용
String value = optional.orElse("default");
String value = optional.orElseThrow(() -> new CustomException());

5. isPresent() + get() 조합 지양

// ❌ Optional의 의미 없음
if (optional.isPresent()) {
    return optional.get();
} else {
    return defaultValue;
}

// ✅ 메서드 활용
return optional.orElse(defaultValue);

성능 최적화 팁

// ❌ 비효율: 매번 새 객체 생성
return optional.orElse(new ExpensiveObject());

// ✅ 효율: 필요할 때만 생성
return optional.orElseGet(() -> new ExpensiveObject());

// ❌ 비효율: 불필요한 Optional 생성
public Optional<String> getName() {
    return Optional.of(this.name); // name이 항상 non-null이면 불필요
}

// ✅ 효율: null 가능성이 있을 때만 사용
public String getName() {
    return this.name; // null 가능하면 호출자가 처리
}

// Optional 캐싱 (불변 상수)
private static final Optional<String> EMPTY_STRING = Optional.empty();

성능 고려사항

  • Optional은 객체 래핑으로 인한 약간의 오버헤드가 있습니다
  • 성능이 매우 중요한 루프나 스트림 처리에서는 null 체크가 더 효율적일 수 있습니다
  • 하지만 대부분의 비즈니스 로직에서는 성능 차이가 미미하며, 가독성과 안정성 이점이 더 큽니다

Java 9+ 추가 기능

// or(): 대체 Optional 제공
Optional<String> result = optional.or(() -> Optional.of("alternative"));

// stream(): Optional을 Stream으로 변환
List<String> list = optionalList.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

// isEmpty(): 값이 없는지 확인 (Java 11+)
if (optional.isEmpty()) {
    // 값이 없을 때 처리
}

요약

Optional은 null 처리를 명시적이고 안전하게 만드는 강력한 도구입니다.

핵심 원칙

  1. 반환 타입으로 사용 - 메서드의 결과가 없을 수 있음을 명시
  2. 함수형 메서드 활용 - map, flatMap, filter로 체이닝
  3. 필드나 파라미터로 사용 금지 - 불필요한 복잡성 증가
  4. orElseGet 선호 - 성능 최적화
  5. get() 직접 호출 지양 - 안전한 대안 사용