우아한테크코스 레벨 1에서 학습한 내용을 정리한 글입니다.

 

💭 들어가며

구구의 매서운 리뷰...

장기 미션 리뷰로 Optional 관련 피드백을 받았다. 장기 판에 기물이 없는 경우 null을 반환하게 되면 악명이 자자한 NPE의 위험이 있다고 들어, 이를 방지하고자 Optional을 사용했다. 그런데... Optional 관련 피드백이 마구마구 달렸다.

Brain Goetz는 Optional을 만든 의도에 대해, "결과 없음"을 명확하게 표현할 수 있는 방법이 필요한 라이브러리 메서드 반환 유형을 위한 제한된 메커니즘을 제공하는 것이라고 말했다. 하지만 실제로는 사람들이 의도와 다르게 사용하는 경우가 많아 주의할 점이 많다고 한다. (이래서 코틀린을 써야 하는 건가...?)

찾아보니 안티패턴이란 안티패턴은 다 내가 쓰고 있던 것이었어서, 정확한 사용법을 알아보고자 Optional의 기본 사용법부터 안티패턴까지 정리하려 한다.

 

 

✅ Optional이란

Optional은 Java 8에서 도입된 기능으로, null을 직접 다루는 것을 피하고 보다 안전하고 명시적인 코드를 작성하기 위해 등장했다. 하지만 잘못 사용하면 오히려 코드의 가독성을 해치거나 성능 저하로 이어질 수 있다.

🔽 사용 시점

  • 메서드의 반환 타입으로 사용하는 것이 가장 적절하다. 클라이언트가 값의 존재 여부를 명시적으로 처리하도록 유도할 수 있다. 
  • 값이 존재할 수도, 존재하지 않을 수도 있는 경우 의미를 명확하게 표현할 수 있다. 

🔽 메서드

메서드 설명
static <T> Optional<T> empty() 비어 있는 Optional 생성
Optional<T> filter(Predicate<? super T> predicate) 조건에 맞는 값만 유지
<U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper) 중첩된 Optional을 평탄화
T get() Optional 안의 값을 반환 (null이면 NPE)
boolean isPresent()
값 존재 여부 반환
void ifPresent(Consumer consumer)
값이 있으면 실행
<U> Optional<U> map(Function<? super T,? extends U> mapper) 값이 있으면 변환
static <T> Optional<T> of(T value) null이 아닌 값을 감쌈 (null이면 NPE)
static <T> Optional<T> ofNullable(T value) null 여부에 관계 없이 값을 감쌈
T orElse(T other) 값이 없을 경우 기본값 반환
T orElseGet(Supplier<? extends T> other) 값이 없을 경우 Supplier로 생성된 값 반환
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier 값이 없을 경우 예외 던짐

 

 

✅ Optional의 기본 사용법

▶ 생성

Optional<String> name = Optional.of("미소");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null);
  • of(): 절대 null이 아닌 값을 감쌀 때 사용
  • empty(): 비어 있는 Optional 생성
  • ofNullable(): null일 수 있는 값을 감쌀 때 사용

 

▶ 값 꺼내기

Optional<String> name = Optional.of("미소");

// isPresent() - get()
if (name.isPresent()) {
    String result1 = name.get();
}

// orElse()
String result2 = name.orElse("기본값");

// orElseGet()
String result3 = name.orElseGet(() -> "기본값");

// orElseThrow()
String result4 = name.orElseThrow(() -> new IllegalArgumentException("값이 없습니다."));

 

▶ 값 변환

Optional<String> name = Optional.of("미소");
Optional<Integer> length = name.map(String::length);
  • map(): 값이 있으면 변환 함수 적용, 없으면 빈 Optional 반환
  • flatMap(): 변환 결과가 Optional인 경우 중첩 방지

 

▶ 조건 필터링

Optional<String> name = Optional.of("미소");
Optional<String> filtered = name.filter(n -> n.length() >= 2);
  • filter(): 조건에 맞지 않으면 빈 Optional 반환

 

 

✅ Optional 올바른 사용법

▶ get()을 무작정 사용하지 마라

🔽 잘못된 사용

Optional<String> name = Optional.of("미소");
String value = name.get(); // NPE 발생 가능
  • Optional이 비어 있을 경우 get()은 예외를 발생시킨다.
  • 이는 Optional의 본래 목적(안전한 null 처리)을 무의미하게 만든다.

🔽 올바른 사용

Optional<String> name = Optional.ofNullable(null);

// orElse()
String value1 = name.orElse("기본값");

// orElseGet()
String value2 = name.orElseGet(() -> "기본값 생성 로직 실행");

// orElseThrow()
String value3 = name.orElseThrow(() -> new IllegalArgumentException("이름이 없습니다."));

// isPresent()
name.ifPresent(System.out::println);
  • orElse(): 값이 없을 경우 기본값을 반환한다.
  • orElseGet(): orElse()와 달리, 값이 없을 경우에만 람다를 실행한다.
  • orElseThrow(): 값이 없을 경우, 명확한 예외를 던진다.
  • isPresent(): 값이 존재할 때만 동작을 수행하고, 값이 없을 경우 아무 일도 하지 않는다.

 

▶ Optional을 필드로 사용하지 마라

🔽 잘못된 사용

public class User {
    private Optional<String> name = Optional.empty();
    
    public Optional<String> getName() {
        return name;
    }
}
  • Optional은 반환값의 유무를 표현하기 위한 용도로 설계되었으며, 객체의 상태를 나타내는 필드 타입으로는 부적절하다.
  • JPA에서는 Optional 타입의 필드를 지원하지 않고, Jackson에서는 직렬화, 역직렬화 처리에서 문제가 발생할 수 있으며, 불필요하게 객체를 감싸면서 메모리 낭비가 발생한다. 또한, Optional<Optional<T>>와 같이 중첩 구조를 사용하게 되어 혼란을 유발할 수 있다.

🔽 올바른 사용

public class User {
    private String name;
    
    public Optional<String> getName() {
    	return Optional.ofNullable(name);
    }
}
  • 필드는 일반 객체로 선언하고, 값의 유무를 표현할 때만 Optional로 감싸 반환하자.

 

▶ 매개변수에 Optional을 사용하지 마라

🔽 잘못된 사용

public void find(Optional<String> name) {
    // ...
}
  •  호출 시마다 Optional을 직접 만들어 넘겨야 해서 API 사용성이 떨어진다.
  • null을 넘기면 Optional을 써도 결국 NPE가 발생할 수 있다. 이는 Optional의 본래 목적(안전한 null 처리)을 무의미하게 만든다.

🔽 올바른 사용

public void find(String name) {
    if (name != null) {
    	// ...
    }
}
  • 매개변수는 일반 타입으로 받고, Optional은 내부에서 필요한 경우에만 사용하자.

 

▶ Optional을 컬렉션의 원소로 사용하지 마라

🔽 잘못된 사용

Map<String, Optional<String>> map = new HashMap<>();
map.put("name", Optional.of("이소은"));
map.put("nickname", Optional.ofNullable(someNickname));

String name = map.get("name").orElse("");
String nickname = map.get("nickname").orElse("");
  • 컬렉션은 이미 null 판단 메서드를 제공하므로, 원소를 Optional로 감싸는 것은 중복 표현이며 메모리 낭비로 이어진다.

🔽 올바른 사용

Map<String, String> map = new HashMap<>();
map.put("name", "미소");
map.put("nickname", null);

String name = map.getOrDefault("name", "");
String nickname = map.computeIfAbsent("nickname", k -> "");
  • Optional은 값을 꺼낸 후, 필요한 경우에만 사용하는 것이 적절하다.

 

▶ orElse() 대신 orElseGet()을 사용하라

🔽 잘못된 사용

String value = optional.orElse(expensiveOperation()); // 항상 실행됨
  • orElse()는 내부 Optional 상태와 관계없이 eager evaluation을 하기 때문에, 값이 존재하더라도 비용이 큰 연산이 불필요하게 실행될 수 있다.

🔽 올바른 사용

String value = optional.orElseGet(() -> expensiveOperation()); // Optional이 비어있을 때만 실행됨
  • orElseGet()은 Optional이 비어 있을 때만 lazy evaluation되어 불필요한 리소스 낭비를 막을 수 있다.

 

▶ 단순히 값을 얻을 목적이라면 Optional 대신 null 체크를 해라

🔽 잘못된 사용

Optional<String> name = Optional.ofNullable(user.getName());

if (name.isPresent()) {
    System.out.println(user.getName());
}
  • Optional을 사용했지만 실제로는 null 체크와 다를 바 없는 불필요한 래핑이다.

🔽 올바른 사용

if (user.getName() != null) {
    System.out.println(user.getName());
}
  • 이 경우에는 기존 방식(null 체크)이 더 단순하고 성능 상으로도 효율적이다.

 

▶ Optional 대신 비어있는 컬렉션을 반환하라

🔽 잘못된 사용

Optional<List<String>> names = Optional.of(new ArrayList<>());

if (names.isPresent() && !names.get().isEmpty()) {
    // ...
}
  • 컬렉션은 비어 있음을 자체적으로 표현할 수 있으므로, Optional로 감싸는 것은 불필요한 중복이다.

🔽 올바른 사용

List<String> names = new ArrayList<>();

if (!names.isEmpty()) {
    // ...
}
  • 비어 있는 컬렉션을 그대로 반환하는 것이 명확하고 안전하다.

 

▶ of()와 ofNullable()을 적절히 사용하라

🔽 잘못된 사용

Optional<String> middleName = Optional.of(user.getMiddleName());
  • of()는 null이 절대 아님을 보장할 때만 사용해야 한다.
  • 값이 null이면 NullPointerException이 발생한다.

🔽 올바른 사용

Optional<String> middleName = Optional.ofNullable(user.getMiddleName());
  • ofNullable()은 값이 null일 수도 있는 경우 안전하게 감쌀 수 있다.
  • 값이 null이면 Optional.empty()를 반환하고, null이 아니면 내부 값을 감싼 Optional을 반환한다.

 

▶ Optional<T> 대신 OptionalInt, OptionalLong, OptionalDouble을 사용하라

🔽 잘못된 사용

Optional<Integer> count = Optional.of(10);  // 박싱 발생

for (int i = 0; i < count.get(); i++) { ... }
  • 기본형을 Optional로 감싸면 박싱, 언박싱으로 인한 불필요한 객체 생성이 발생한다.

🔽 올바른 사용

OptionalInt count = OptionalInt.of(10);

for (int i = 0; i < count.getAsInt(); i++) { ... }
  • OptionalInt, OptionalLong, OptionalDouble은 기본형 값을 직접 저장하므로 박싱, 언박싱이 발생하지 않는다.

 

 

📍 참고 자료

soeun2537