Optional

서버란·2024년 10월 9일

자바 궁금증

목록 보기
34/35

Optional은 Java 8에서 도입된 클래스이며, 주로 NullPointerException(NPE)을 방지하기 위해 사용됩니다. Optional을 통해 객체의 존재 여부를 명시적으로 처리할 수 있어, 개발자가 의도치 않게 null을 참조하는 상황을 방지하고 코드의 가독성과 안정성을 높여줍니다.

1. Optional이란 무엇인가?

Optional은 값이 있을 수도 있고 없을 수도 있는 객체를 나타냅니다. 즉, Optional은 특정 값이 존재할 수 있고(Optional이 값을 감싸고 있는 경우), 값이 없을 수도 있으며(null 대신 빈 Optional 객체를 가질 수 있음), 이러한 상태를 명시적으로 처리할 수 있는 기능을 제공합니다.

  • Null 값 대신 사용하는 객체: Optional을 사용하면 메서드에서 값을 반환할 때 null 대신 빈 Optional 객체를 반환할 수 있습니다.
  • NullPointerException 방지: Optional을 사용함으로써 null을 직접 다루지 않게 되어 예기치 않은 NPE를 예방할 수 있습니다.

2. Optional 생성 방법

Optional 객체를 생성하는 방법은 여러 가지가 있습니다.

1) Optional.of(T value)

  • 값을 감싼 Optional을 생성합니다. 값이 null일 경우, NullPointerException을 발생시킵니다.
Optional<String> optional = Optional.of("Hello");
// Optional: "Hello"를 감싼 Optional 객체 생성

2) Optional.ofNullable(T value)

  • 값이 null일 수 있는 경우에 사용합니다. 값이 null이면 비어 있는 Optional 객체를 반환하고, 값이 있으면 그 값을 감싼 Optional을 반환합니다.
Optional<String> optional = Optional.ofNullable(null);
// Optional: 빈 Optional 객체 생성 (null 대신 비어 있는 Optional)

3) Optional.empty()

  • 아무 값도 가지지 않는 빈 Optional 객체를 반환합니다.
Optional<String> emptyOptional = Optional.empty();
// Optional: 비어 있는 Optional 객체 생성

3. Optional 사용 방법

1) 값이 존재하는지 확인
Optional 안에 값이 존재하는지 확인할 때는 isPresent() 메서드를 사용합니다.

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

if (optional.isPresent()) {
    System.out.println(optional.get());  // "Hello" 출력
}

Java 11부터는 isPresent() 대신 ifPresent() 메서드를 사용하여 값이 있을 때만 특정 동작을 수행할 수 있습니다.

optional.ifPresent(value -> System.out.println(value));  // 값이 있으면 출력

2) 값을 꺼내기
Optional에 값이 존재하면 get() 메서드를 사용하여 값을 가져올 수 있습니다. 그러나, 값이 없을 때 get()을 호출하면 NoSuchElementException이 발생하므로, 반드시 isPresent() 또는 ifPresent()로 값의 존재를 확인한 후에 사용하는 것이 좋습니다.

String value = optional.get();

3) 기본 값 설정
값이 없을 때 기본값을 설정하려면 orElse() 메서드를 사용할 수 있습니다. 이 메서드는 Optional 안에 값이 있으면 그 값을 반환하고, 없으면 지정한 기본값을 반환합니다.

String result = optional.orElse("default value");  // 값이 없으면 "default value" 반환

4) 값이 없을 때 대체 동작
orElseGet() 메서드는 값이 없을 때만 대체 동작을 실행하도록 할 수 있습니다. 주로 값이 없을 때만 대체 값을 계산하거나 얻는 로직을 사용하고 싶을 때 유용합니다.

String result = optional.orElseGet(() -> "Generated value");
// 값이 없으면 "Generated value" 반환

5) 값이 없을 때 예외 던지기
orElseThrow()는 값이 없을 때 특정 예외를 던지도록 할 수 있습니다. 주로 값이 필수적인 경우 사용합니다.

String result = optional.orElseThrow(() -> new IllegalArgumentException("No value present"));
// 값이 없으면 IllegalArgumentException 발생

6) Optional에서 값을 변환하기
map() 메서드는 Optional 안에 있는 값을 다른 형태로 변환할 때 사용됩니다. 값이 있으면 변환하고, 없으면 빈 Optional을 반환합니다.

Optional<String> optional = Optional.of("Hello");
Optional<Integer> lengthOptional = optional.map(String::length);
// "Hello"의 길이를 반환하는 Optional<Integer> 반환

7) Optional 체이닝 (flatMap)
map()과 비슷하지만, 중첩된 Optional을 다룰 때는 flatMap()을 사용합니다. 이를 통해 중첩된 Optional을 한 단계로 평탄화(flatten)할 수 있습니다.

Optional<String> optional = Optional.of("Hello");
Optional<String> result = optional.flatMap(value -> Optional.of(value.toUpperCase()));
// Optional: "HELLO" 반환

4. Optional 사용 시 주의사항

1) 필드에 Optional을 사용하지 않기
Optional은 주로 반환값이나 메서드 매개변수에서 사용되는 것이 적절합니다. 필드에 Optional을 사용하는 것은 권장되지 않으며, 필드에는 값이 없을 경우를 명시적으로 null로 나타내거나 기본값을 설정하는 것이 좋습니다. 필드에 Optional을 사용하면 불필요한 객체 생성이 늘어나 메모리와 성능에 악영향을 미칠 수 있습니다.

2) 컬렉션이나 배열에서 Optional을 사용하지 않기
Optional은 빈 컬렉션(List, Set 등)이나 배열과 함께 사용되지 않습니다. 이미 비어 있는 컬렉션이나 배열 자체가 값이 없는 상태를 표현할 수 있기 때문에, 불필요하게 중첩된 Optional을 사용하면 코드가 복잡해질 수 있습니다.

3) 값을 직접 Optional로 감싸지 않기
직접적으로 Optional 객체를 반환하는 것이 아니라, null을 반환하지 않도록 설계를 개선하는 것이 더 나은 경우가 많습니다. Optional은 주로 외부 API나 라이브러리 호출 시 null 값 처리를 우아하게 처리하기 위한 도구로, 남용은 피해야 합니다.

5. Optional의 장점

  • 명시적 Null 처리: 메서드 반환 값이 null일 수 있다는 것을 명확하게 표현함으로써 null을 처리하는 코드의 가독성을 높이고, 실수를 줄일 수 있습니다.
  • 안전한 Null 처리: null을 직접 다루지 않고, Optional을 통해 안전하게 값을 다룰 수 있습니다. 이로 인해 NullPointerException을 예방할 수 있습니다.
  • 간결한 코드: if 문으로 null 체크를 해야 하는 번거로움 없이, 메서드 체이닝으로 간결하게 null을 처리하는 코드를 작성할 수 있습니다.

Q1: Optional을 사용하여 메서드 반환 값을 안전하게 처리할 때 성능 상의 단점이 있을 수 있을까요?

네, Optional을 사용할 때 성능에 영향을 줄 수 있는 몇 가지 요소가 있습니다. 다만, 일반적인 상황에서는 큰 문제가 되지 않을 수 있지만, 성능이 중요한 환경에서는 주의가 필요합니다.

  • 추가적인 객체 생성: Optional 객체는 내부적으로 값을 감싸기 때문에, 값이 있을 때마다 새로운 Optional 객체가 생성됩니다. 이로 인해 메모리 사용량이 약간 증가할 수 있으며, 이는 빈번한 메서드 호출 시 성능 저하로 이어질 수 있습니다. 특히 Optional이 빈 경우라도 Optional.empty()를 호출하는 것은 결국 객체 생성을 수반합니다.

  • 값 접근 시 추가적인 메서드 호출: Optional을 통해 값을 가져오거나 처리할 때마다 map(), orElse(), ifPresent() 등의 메서드를 호출해야 합니다. 이는 직관적이고 안전한 코드 작성에는 도움이 되지만, 그만큼 추가적인 메서드 호출 비용이 발생할 수 있습니다.

  • 박싱과 언박싱: 기본형 데이터 타입(int, double 등)을 Optional로 감쌀 경우, 내부적으로 박싱(Boxing)과 언박싱(Unboxing)이 발생합니다. 기본형 타입을 감싼 OptionalInt, OptionalDouble과 같은 기본형에 특화된 클래스를 사용하면 이를 피할 수 있지만, 일반적으로 Optional, Optional 등을 사용할 경우 성능에 약간의 영향이 있을 수 있습니다.

하지만 일반적으로 Optional의 성능 이슈는 미미하며, 안정성과 가독성을 높이기 위해 사용하는 것이 적절합니다. 성능이 매우 중요한 상황에서는 객체 생성 및 메서드 호출 비용을 고려하여 다른 방법을 선택할 수 있습니다.

Q2: 컬렉션과 Optional을 함께 사용하는 경우의 예시와 해당 방식의 문제점은 무엇인가요?

Optional과 컬렉션을 함께 사용하는 것은 불필요한 중첩을 초래할 수 있고, 가독성을 떨어뜨릴 수 있습니다. 그 이유는 컬렉션 자체가 이미 빈 상태를 표현할 수 있는 기능을 제공하기 때문입니다.

예시

Optional<List<String>> optionalList = Optional.ofNullable(getList());

위 코드는 Optional<List> 형태의 중첩된 구조를 사용하고 있습니다. 이 코드는 문제가 있는데, 그 이유는:

  • 중첩된 구조: Optional<List>는 두 가지 상태를 나타낼 수 있습니다. 즉, 리스트가 비어있을 수 있고(Optional.empty()), 리스트 자체가 null일 수도 있는 상태입니다. 하지만, 컬렉션(List, Set 등)은 비어 있는 상태를 자연스럽게 표현할 수 있으므로, 굳이 Optional로 다시 감싸는 것은 불필요한 중첩을 만들게 됩니다.

  • 예를 들어, List이 비어있으면 빈 리스트를 반환하면 됩니다:

List<String> list = getList();
if (list.isEmpty()) {
    // 리스트가 비어 있을 때 처리
}
  • 복잡한 코드를 유발: Optional<List>를 사용하면 값을 가져올 때 get()이나 orElse() 같은 메서드를 통해 값을 한 번 더 풀어야 하는 복잡한 코드가 발생합니다.

해결 방법
이 문제를 해결하는 가장 간단한 방법은 빈 리스트를 반환하는 것입니다. 즉, 컬렉션이 null일 수 있다면 빈 리스트를 반환함으로써 Optional을 사용할 필요 없이 빈 상태를 처리할 수 있습니다.

public List<String> getList() {
    return list != null ? list : Collections.emptyList();
}

이렇게 하면 Optional을 사용할 필요가 없어지고, 리스트가 비어 있는 상태는 그 자체로 명시적이므로 코드가 간결해집니다.

Q3: Optional이 남용될 수 있는 상황과 이를 피하기 위한 설계 개선 방법은 무엇이 있을까요?

Optional을 남용할 수 있는 상황과 이를 피하기 위한 방법은 다음과 같습니다.

남용될 수 있는 상황

  • 필드에 Optional 사용: Optional을 클래스의 필드로 사용하는 것은 권장되지 않습니다. 필드에서 Optional을 사용하는 것은 객체 생성 시 추가적인 메모리와 오버헤드를 유발할 수 있으며, 불필요하게 Optional을 감싸는 구조가 생깁니다.

예시:

public class Person {
    private Optional<String> name;  // 남용 사례
}
  • 메서드 인자로 Optional 사용: 메서드 인자로 Optional을 사용하는 것도 권장되지 않습니다. 메서드 인자 자체로 Optional을 받게 되면, 호출하는 측에서 불필요하게 Optional.ofNullable()로 감싸야 하며, 코드가 복잡해집니다.

예시:

public void setName(Optional<String> name) {  // 남용 사례
    this.name = name.orElse("default");
}
  • 필수 값에도 Optional 사용: 필수적으로 존재해야 하는 값에 Optional을 사용하는 것은 불필요합니다. 필수 값이라면 그냥 해당 타입으로 처리하는 것이 낫습니다.

이를 피하기 위한 설계 개선 방법

  • 필드에서는 Optional 대신 기본 값을 사용: 필드에서 Optional을 사용하는 대신, null 대신 기본값을 사용하거나, 클래스 설계 단계에서 필수 필드와 선택 필드를 명확히 구분하는 것이 좋습니다.

예시:

private String name = "Unknown";  // 기본값 설정
  • 메서드 인자로는 명시적으로 null을 허용: 메서드 인자로 Optional을 받지 말고, null을 처리하는 메서드 내부 로직을 추가하는 것이 좋습니다.

예시:

public void setName(String name) {
    this.name = (name != null) ? name : "default";
}
  • 필수 값과 선택 값을 명확히 구분: 선택적인 값을 Optional로 반환하는 것은 괜찮지만, 필수적인 값은 그냥 그 값을 반환하는 것이 좋습니다. 예를 들어, 데이터베이스 조회 시 값이 있을 수 있는 필드는 Optional로 반환하되, 항상 존재하는 필드는 그냥 값을 반환하는 식으로 처리합니다.
profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글