전체 목록
JavaMedium#63

Java의 Optional이란 무엇이며 올바른 사용법은?

#Java#Optional#NPE#Java8
힌트

NullPointerException 방지를 위한 래퍼 클래스입니다.

정답 및 해설

Java의 Optional이란 무엇이며 올바른 사용법은?

Optional<T>는 Java 8에서 도입된 컨테이너 클래스로, 값이 있거나 없을 수 있는 상황을 명시적으로 표현합니다. null 반환 대신 Optional을 사용하면 NullPointerException(NPE)을 명시적으로 다루도록 유도하여 코드의 안정성을 높입니다. 하지만 잘못 사용하면 오히려 코드가 복잡해지므로 올바른 사용 패턴을 이해하는 것이 중요합니다.

Optional이 필요한 이유

// 기존 null 처리 방식 - 잊어버리기 쉬움
public String getUserEmail(Long userId) {
    User user = userRepository.findById(userId);
    if (user == null) {
        return null; // 호출자가 null 체크를 해야 하는데 강제되지 않음
    }
    return user.getEmail();
}

// 호출 코드에서 NPE 위험
String email = getUserEmail(1L);
System.out.println(email.toUpperCase()); // user가 없으면 NPE!

// Optional 사용 - null 가능성을 타입으로 명시
public Optional<String> getUserEmail(Long userId) {
    // 반환 타입이 Optional이므로 호출자는 반드시 처리를 고려하게 됨
    return userRepository.findById(userId)
        .map(User::getEmail);
}

Optional 생성 방법

// 1. Optional.of(value): 값이 반드시 null이 아닐 때
Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.of(null); // NullPointerException 발생!

// 2. Optional.ofNullable(value): 값이 null일 수 있을 때
Optional<String> opt3 = Optional.ofNullable("Hello"); // Optional["Hello"]
Optional<String> opt4 = Optional.ofNullable(null);    // Optional.empty

// 3. Optional.empty(): 명시적으로 빈 Optional 생성
Optional<String> opt5 = Optional.empty();

주요 메서드

값 확인 및 가져오기

Optional<String> opt = Optional.of("Hello");
Optional<String> empty = Optional.empty();

// isPresent(): 값이 있으면 true
System.out.println(opt.isPresent());   // true
System.out.println(empty.isPresent()); // false

// isEmpty(): Java 11+ 값이 없으면 true
System.out.println(opt.isEmpty());   // false
System.out.println(empty.isEmpty()); // true

// get(): 값 반환, 없으면 NoSuchElementException
System.out.println(opt.get()); // "Hello"
// empty.get(); // NoSuchElementException!

기본값 처리

Optional<String> opt = Optional.empty();

// orElse: 값이 없으면 기본값 반환 (항상 평가됨)
String result1 = opt.orElse("default");
System.out.println(result1); // "default"

// orElseGet: 값이 없을 때만 Supplier 실행 (지연 평가)
String result2 = opt.orElseGet(() -> generateDefault());
// opt가 비어있을 때만 generateDefault() 호출

// orElseThrow: 값이 없으면 예외 던짐 (Java 10+: 기본 NoSuchElementException)
String result3 = opt.orElseThrow(() -> new IllegalStateException("값이 없습니다."));

// orElse vs orElseGet 성능 차이
Optional<String> hasValue = Optional.of("value");
// orElse: "default"가 항상 생성됨
String r1 = hasValue.orElse(expensiveOperation()); // expensiveOperation 항상 실행
// orElseGet: 값이 있으면 람다 미실행
String r2 = hasValue.orElseGet(() -> expensiveOperation()); // 실행 안 됨

변환 및 필터링

Optional<String> opt = Optional.of("  hello world  ");

// map: 값을 변환 (변환 결과가 null이면 Optional.empty 반환)
Optional<String> upper = opt.map(String::trim).map(String::toUpperCase);
System.out.println(upper.get()); // "HELLO WORLD"

// flatMap: Optional을 반환하는 함수를 적용 (중첩 Optional 방지)
Optional<Optional<String>> nested = opt.map(s -> Optional.of(s.trim())); // Optional<Optional<String>>
Optional<String> flat = opt.flatMap(s -> Optional.of(s.trim()));         // Optional<String>

// filter: 조건을 만족하지 않으면 Optional.empty 반환
Optional<String> filtered = opt.map(String::trim)
    .filter(s -> s.length() > 5);
System.out.println(filtered.isPresent()); // true ("hello world" 길이 11)

Optional<String> shortOpt = Optional.of("hi");
Optional<String> filtered2 = shortOpt.filter(s -> s.length() > 5);
System.out.println(filtered2.isPresent()); // false

동작 수행

Optional<String> opt = Optional.of("Hello");

// ifPresent: 값이 있을 때만 Consumer 실행
opt.ifPresent(System.out::println); // "Hello"

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

// or: Java 9+ 값이 없을 때 대체 Optional 반환
Optional<String> result = Optional.<String>empty()
    .or(() -> Optional.of("대체값"));
System.out.println(result.get()); // "대체값"

// stream: Java 9+ Optional을 Stream으로 변환
opt.stream()
   .map(String::toUpperCase)
   .forEach(System.out::println); // "HELLO"

올바른 사용법

권장: 반환 타입으로 사용

// 좋은 예: 조회 결과가 없을 수 있는 메서드의 반환 타입
public Optional<User> findUserById(Long id) {
    return userRepository.findById(id);
}

public Optional<String> findConfigValue(String key) {
    return Optional.ofNullable(configMap.get(key));
}

// 호출 코드
findUserById(1L)
    .map(User::getName)
    .ifPresent(name -> System.out.println("사용자: " + name));

비권장: 메서드 파라미터로 사용

// 나쁜 예: 파라미터에 Optional 사용
public void process(Optional<String> name) { // 안티패턴
    // 호출자가 null을 Optional 대신 직접 전달 가능
}

// 좋은 예: 오버로딩 또는 @Nullable 사용
public void process(String name) { ... }
public void process() { ... } // 이름 없는 경우 별도 메서드

비권장: 필드로 사용

// 나쁜 예: 필드에 Optional 사용 (직렬화 불가, 메모리 낭비)
public class User {
    private Optional<String> middleName; // 안티패턴
}

// 좋은 예: null 허용 필드로 선언하고 getter에서 Optional 반환
public class User {
    private String middleName; // null 허용

    public Optional<String> getMiddleName() {
        return Optional.ofNullable(middleName);
    }
}

비권장: isPresent() + get() 조합

// 나쁜 예: isPresent/get 조합은 기존 null 체크와 동일
Optional<User> userOpt = findUserById(1L);
if (userOpt.isPresent()) {
    User user = userOpt.get();
    System.out.println(user.getName());
}

// 좋은 예: ifPresent 또는 map 사용
findUserById(1L).ifPresent(user -> System.out.println(user.getName()));

// 또는
String name = findUserById(1L)
    .map(User::getName)
    .orElse("알 수 없음");

실전 패턴

체이닝으로 null-safe 처리

// 중첩 null 체크를 Optional 체이닝으로 단순화
// 기존 방식
String city = null;
if (user != null && user.getAddress() != null) {
    city = user.getAddress().getCity();
}

// Optional 방식
String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .orElse("서울");

Stream과 함께 사용 (Java 9+)

List<Optional<String>> list = Arrays.asList(
    Optional.of("A"),
    Optional.empty(),
    Optional.of("B"),
    Optional.empty(),
    Optional.of("C")
);

// flatMap으로 빈 Optional 제거 후 값만 추출
List<String> values = list.stream()
    .flatMap(Optional::stream) // Java 9+
    .collect(Collectors.toList());
System.out.println(values); // [A, B, C]

Repository 패턴에서의 활용

// Spring Data JPA는 Optional을 자연스럽게 지원
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByUsername(String username);
}

// 서비스 계층에서 활용
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserDto getUser(Long id) {
        return userRepository.findById(id)
            .map(UserDto::from)
            .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + id));
    }

    public void updateEmail(Long id, String newEmail) {
        userRepository.findById(id)
            .ifPresentOrElse(
                user -> {
                    user.setEmail(newEmail);
                    userRepository.save(user);
                },
                () -> { throw new UserNotFoundException("사용자 없음: " + id); }
            );
    }
}

Optional 메서드 요약

메서드설명값 없을 때
of(value)null 불가 Optional 생성NPE 발생
ofNullable(value)null 가능 Optional 생성Optional.empty()
empty()빈 Optional 생성-
isPresent()값 존재 여부 확인false
isEmpty() (Java 11+)값 없음 여부 확인true
get()값 반환NoSuchElementException
orElse(default)기본값 반환 (항상 평가)default 반환
orElseGet(supplier)기본값 지연 평가supplier.get() 반환
orElseThrow(supplier)예외 던짐예외 발생
map(function)값 변환Optional.empty()
flatMap(function)Optional 반환 함수 적용Optional.empty()
filter(predicate)조건 필터링Optional.empty()
ifPresent(consumer)값 있을 때 동작 수행아무것도 안 함
ifPresentOrElse (Java 9+)있을 때/없을 때 각각 처리emptyAction 실행
or(supplier) (Java 9+)대체 Optional 반환supplier.get() 반환
stream() (Java 9+)Optional → Stream 변환빈 Stream