티스토리 뷰

언어/Java

Optional 바르게 사용하기

snvlqkq 2025. 8. 26. 21:04

Optional이란 null 참조로 인한 문제를 해결하기 위해 Java 8에서 도입된 클래스이다. Optional은 객체를 감싸는 Wrapper Class 이다.

Optional 객체는 값이 없음null 대신 Optional.empty()로 표현한다. 기존에 null을 대체하는 개념이다. Optional을 사용하면 값이 없는경우 null로 인해 발생할 수 있는 문제를 방지하고 전달 받는 요소의 값의 유무를 명시적으로 표현할 수 있다.

// null - 애매함
String result = getUserEmail(123); // null이 나올 수 있는지 불분명
if (result != null) { ... }

// 빈 Optional - 명확함
Optional<String> result = getUserEmail(123); // "값이 있을 수도, 없을 수도 있음"을 명시
if (result.isPresent()) { ... }

Optional 생성 방법

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

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

// 3. null 가능성이 있는 값으로 생성(가장 많이 사용함)
Optional<String> maybe = Optional.ofNullable(getString());

값 확인 및 접근 방법

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

// 값 존재 확인
// Java 11 이전 방식
boolean hasValue = opt.isPresent();

// Java 11 이후 방식 - 더 직관적!
boolean isEmpty = opt.isEmpty();

// 1. get() : 값 없으면 NoSuchElementException 발생
String value = opt.get(); // "Hello"

// 2. orElse() : 기본 값 제공
String value = opt.orElse("기본 값");

// 3. orElseGet() : 지연 평가로 기본값 제공
String value = opt.orElseGet(() -> "계산된 기본값");

// 4. orElseThrow() : 예외 발생
String value = opt.orElseThrow(); // NoSuchElementException 발생
String value = opt.orElseThrow(() -> new IllegalStateException("값이 필요합니다."));

// 5. ifPresent() : 값이 있을 때만 작업 수행
opt.ifPresent(value -> {
    ...
});

// 6. ifPresentOrElse : 값 유무에 따라 다른 작업 (Java 9+)
opt.ifPresentOrElse(
    value -> {}, // 값이 있을 때
    () -> {} // 값이 없을 때
);

지연 평가

지연 평가란 필요할 때까지 계산을 미루는 기법이다. 즉, 값이 실제로 필요한 시점에만 연산을 수행한다. 지연 평가인 것과 그렇지 않은 로직의 차이점을 확인해보면 지연 평가에 대해 감을 잡을 수 있을 것이다.

Optional<String> hasValue = Optional.of("이미 값이 있음");
Optional<String> noValue = Optional.empty();

// 둘 다 expensiveOperation() 실행됨!
String result1 = hasValue.orElse(expensiveOperation()); // 값이 있어도 실행됨!
String result2 = noValue.orElse(expensiveOperation());  // 값이 없어도 실행됨!

Java의 메서드 호출 규칙 때문에 이런 현상이 발생한다.

String result = opt.orElse(expensiveOperation());

위 코드는 아래와 같은 순서대로 실행한다.

  1. expensiveOperation() 먼저 실행 (메서드 매개변수 평가)
  2. 그 결과를 가지고 orElse() 호출
  3. orElse() 내부에서 Optional 값 유무를 확인
  4. 값이 있으면 기존 값 반환, 없으면 매개변수로 받은 값 반환
    빈 값인 경우에 반환할 값이나 로직들을 먼저 수행한 다음 Optional 의 값 유무에 따라 최종적으로 반환할 값만 전달한다.

그러면 무조건 지연 평가(orElseGet())가 더 좋은게 아닌가?

단순한 값을 반환하는 경우에는 orElse()를 사용하는게 코드의 가독성이 더 뛰어나기 때문에 상황에 따라 적절하게 선택해야 한다.

// 간단하고 명확
String name = userOpt.orElse("Guest");

// 오히려 복잡함
String name = userOpt.orElseGet(() -> "Guest");

빈 값인 경우 수행할 로직이 비용이 많이 드는 경우에 지연 평가 로직을 사용하는게 적절하다.

// 네트워크, DB, 파일 I/O 등
String config = configOpt.orElseGet(() -> loadFromDatabase());
User user = userOpt.orElseGet(() -> createDefaultUser());
String report = reportOpt.orElseGet(() -> generateComplexReport());

Optional 사용시 주의사항

원래 Optional은 메서드의 반환 타입으로만 사용되도록 설계되었다.

// Optional의 의도된 사용법
public Optional<String> findUserName(Long id) {
    // 메서드 반환 시에만 사용
    return Optional.ofNullable(database.findName(id));
}

1. Optional 객체 생성 오버헤드

Optional은 매번 새로운 객체를 생성하므로 메모리 할당 및 GC 압박이 있다. 특히 루프나 빈번한 호출에서는 상당한 영향을 준다.

// 매번 Optional 객체 생성
Optional<String> opt1 = Optional.of("test");        // 객체 생성
Optional<String> opt2 = Optional.ofNullable(null);  // 객체 생성
Optional<String> opt3 = Optional.empty();           // 캐시된 싱글톤이지만 여전히 객체

// vs null 체크 (객체 생성 없음)
String value = getValue();
if (value != null) {
  // 처리
}

2. 필드나 멤버변수 사용 금지

1. 직렬화 문제

직렬화란 객체를 바이트로 변환하여 저장하거나 전송할 수 있게 만드는 과정이다. Serializable 인터페이스를 구현한 클래스만 직렬화가 가능하다. Optional은 Serializable 인터페이스를 구현하지 않았기 때문에 Optional 필드는 직렬화가 불가능하다.

public class User implements Serializable {
    private Optional<String> name; // Optional은 Serializable이 아님!
    private Optional<Integer> age;
}
// → NotSerializableException 발생!
2. 메모리 오버헤드

객체 생성이기 때문에 메모리를 더 많이 사용한다.

// ❌ 비효율적
public class Product {
    private Optional<String> description; // Optional 객체 + String 객체
    private Optional<BigDecimal> price;   // Optional 객체 + BigDecimal 객체
    private Optional<Category> category;  // Optional 객체 + Category 객체
}

// ✅ 효율적
public class Product {
    private String description;     // null 가능하지만 메모리 효율적
    private BigDecimal price;       // null 가능
    private Category category;      // null 가능

    // getter에서 Optional 반환
    public Optional<String> getDescription() {
        return Optional.ofNullable(description);
    }
}
3. null 체크 복잡

오히려 null 체크가 복잡해지는 단점이 있다.

public class User {
    private Optional<String> email;

    public boolean hasValidEmail() {
        return email != null && email.isPresent() && !email.get().isEmpty();
        //     ↑ Optional 자체가 null일 수도 있음!
    }
}

2. 매개변수로 사용 금지

1. 호출자에게 부담 전가

Optional을 매개변수로 받으면 메서드를 호출하는 호출자는 항상 인자를 Optional로 감싸야 한다.

// ❌ 나쁜 API 설계
public void updateUserName(Optional<String> name) {
    if (name.isPresent()) {
        // 로직 수행
    }
}

// 호출할 때마다 Optional로 감싸야 함
updateUserName(Optional.of("홍길동"));
updateUserName(Optional.ofNullable(getName()));
updateUserName(Optional.empty()); // 복잡함!

// ✅ 더 나은 설계
public void updateUserName(String name) { // null 허용
    if (name != null) {
        // 로직 수행
    }
}
2. Optional 자체의 null 가능성

Optional 자체도 null이 될 수 있기 때문에 불완전하다.

// ❌ Optional 매개변수도 null이 될 수 있음!
public void process(Optional<String> value) {
    if (value.isPresent()) { // NullPointerException 위험!
        // ...
    }
}

// 호출
process(null); // 💥 NPE 발생!
3. 메서드 복잡도 증가

메서드 선언 부분이 너무 복잡해진다.

// ❌ 복잡한 매개변수
public User createUser(String name, Optional<String> email, Optional<Integer> age) {
    // ...
}

// ✅ 간단하고 명확한 매개변수
public User createUser(String name, String email, Integer age) {
    // null 체크는 내부에서 처리
}

3. 컬렉션 내부

1. 메모리 낭비

Optional 객체를 요소로 받는 컬렉션을 사용하면 요소들이 전부 Optional 객체를 생성해야하기 때문에 메모리 낭비가 심하다.

// ❌ 비효율적 - Optional 객체들이 추가로 생성됨
List<Optional<String>> names = Arrays.asList(
    Optional.of("김철수"),
    Optional.empty(),
    Optional.of("이영희"),
    Optional.empty()
);

// ✅ 효율적 - null 사용
List<String> names = Arrays.asList(
    "김철수",
    null,
    "이영희", 
    null
);
// 또는 빈 컬렉션 반환
List<String> validNames = Arrays.asList("김철수", "이영희");
2. 스트림 처리의 복잡성

Optional로 인해 stream 처리가 복잡해진다.

// ❌ 복잡함
List<Optional<String>> optionals = getOptionals();
List<String> result = optionals.stream()
    .filter(Optional::isPresent)  // Optional 필터링
    .map(Optional::get)           // 값 추출
    .collect(toList());

// ✅ 간단함
List<String> values = getValues();
List<String> result = values.stream()
    .filter(Objects::nonNull)     // null 필터링
    .collect(toList());

Optional vs null 성능 비교

import java.util.Optional;

public class SimplePerformanceTest {

    public static void main(String[] args) {
        int iterations = 1_000_000;

        // 워밍업
        for (int i = 0; i < 10_000; i++) {
            processWithNull("test");
            processWithOptional("test");
        }

        // Null 방식 성능 측정
        long startTime = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            processWithNull("홍길동");
        }
        long nullTime = System.nanoTime() - startTime;

        // Optional 방식 성능 측정
        startTime = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            processWithOptional("홍길동");
        }
        long optionalTime = System.nanoTime() - startTime;

        // 결과 출력
        System.out.println("=== 성능 비교 결과 ===");
        System.out.printf("반복 횟수: %,d회%n", iterations);
        System.out.printf("Null 체크: %.2f ms%n", nullTime / 1_000_000.0);
        System.out.printf("Optional: %.2f ms%n", optionalTime / 1_000_000.0);
        System.out.printf("성능 차이: Optional이 %.1f배 느림%n", (double) optionalTime / nullTime);
    }

    // ✅ 전통적인 null 체크
    public static String processWithNull(String name) {
        if (name != null && !name.isEmpty()) {
            return "처리됨: " + name;
        }
        return "기본값";
    }

    // ❌ Optional 사용 (권장하지 않는 방식)
    public static String processWithOptional(String name) {
        return Optional.ofNullable(name)
                .filter(n -> !n.isEmpty())
                .map(n -> "처리됨: " + n)
                .orElse("기본값");
    }
}

요약

처음에 반복적인 null 체크의 대안을 찾아보다 Optional을 알게 되었다. Optional은 값이 null인 경우 발생하는 다양한 문제점을 방지할 수 있는 훌륭한 대체 방안이다.
하지만 Optional을 잘 못 사용하게 되면 메모리 오버헤드 등 다양한 성능 문제가 발생할 수 있다. Optional은 원래 용도에 맞게 메서드 반환 값에만 사용하고 null 체크 코드의 반복은 다른 대안을 찾아보자.

'언어 > Java' 카테고리의 다른 글

애플리케이션 로그 분석 가이드  (0) 2025.12.28
Record 살펴보기  (0) 2025.09.14
JVM 메모리 구조 정리  (0) 2025.08.16
Java 버전 별 변화 변천사  (0) 2025.08.03
String, StringBuilder, StringBuffer 성능 차이  (0) 2025.05.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함