전체 목록
JavaMedium#063

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

#Java#Optional#NPE#Java8

답변 포인트

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

정답 및 해설

빠른 요약

Optional<T>는 null을 감싸는 컨테이너 클래스로, NPE를 명시적으로 처리하게 합니다. 생성: - Optional.of(value): null이면 NPE - Optional.ofNullable(value): null 허용 - Optional.empty(): 빈 Optional 주요 메서드: - isPresent() / isEmpty() - get(): 값 없으면 NoSuchElementException - orElse(default): 없으면 기본값 - orElseGet(Supplier): 없으면 공급자 실행 - orElseThrow(): 없으면 예외 - map / flatMap / filter 올바른 사용: - 메서드 반환 타입으로 사용 (null 반환 대신) - 필드 타입으로는 사용 비권장 (직렬화 문제) - Optional.get()은 isPresent() 확인 후 사용

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

Optional이 필요한 이유

Java
// 기존 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 생성 방법

Java
// 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();

주요 메서드

값 확인 및 가져오기

Java
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!

기본값 처리

Java
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()); // 실행 안 됨

변환 및 필터링

Java
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

동작 수행

Java
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"

올바른 사용법

권장: 반환 타입으로 사용

Java
// 좋은 예: 조회 결과가 없을 수 있는 메서드의 반환 타입
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));

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

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

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

비권장: 필드로 사용

Java
// 나쁜 예: 필드에 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() 조합

Java
// 나쁜 예: 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 처리

Java
// 중첩 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+)

Java
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 패턴에서의 활용

Java
// 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

관련 질문

같은 카테고리/태그 기준