LostCatBox

Null-Optional-Empty

Word count: 1.3kReading time: 8 min
2022/12/25 Share

null?Optional?빈 객체 반환?

Created Time: September 22, 2022 7:25 PM
Last Edited Time: December 23, 2022 5:36 PM
References: https://moonsiri.tistory.com/140
https://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/
https://dzone.com/articles/using-optional-correctly-is-not-optional

null 피해야하는 이유?

null 때문에 발생하는 문제

  1. 에러의 근원
    • NullPointerException은 자바에서 가장 흔히 발생하는 에러이다.
    • 예외를 던지는 경우에는 스택 추적 전체를 캡처하는 비용문제가 있다.
    • null을 반환하는 경우에는 메서드를 호출하는 곳에서 null처리 코드를 추가해야 하고 만약 무시하면 언제 어디서 NPE를 만날지 모른다.(if(xxx≠null))
  2. 코드를 어지럽힘
    • 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다.
  3. 자바 철학에 위배
    • 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null 포인터다.
  4. 형식 시스템에 구멍을 만듦
    • null은 무형식이며 정보를 포함하고 있지 않으므로 모든 레퍼런스 형식에 null을 할당할 수 있다. 이런 식으로 null이 할당되기 시작하면서 시스템의 다른 부분으로 null이 퍼졌을 때 애초에 null이 어떤 의미로 사용되었는지 알 수 없다.

해결법

Optional 사용 이점과 단점 반드시 주의하기

Class Optional

Optional은 Null이 아닌 값을 포함할 수도 있고 포함하지 않을 수도 있는 컨테이너 개체입니다. 값이 있으면 isPresent()가 true를 반환하고 get()이 값을 반환합니다. orElse()(값이 없으면 기본값을 반환함) 및 ifPresent()(값이 있으면 코드 블록을 실행함)와 같이 포함된 값의 유무에 의존하는 추가 메서드가 제공됩니다.

값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환합니다. Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드입니다

활용법

메소드의 결과물로 Optional 반환

1
Optional optCar = Optional.empty(); //아예 빈값 반환
1
2
3
4
5
// Object를 반환하지 않고 Optional로 한번 감싸서 반환한다.
// >> Object객체는 빈값(null)이 될 수 있음을 알려준다.
public Optional<Object> getObject(String key){
return Optional.ofNullable(map.get(key));
}

메소드를 호출하는 쪽에서도 if else로 분기하여 처리하지 않고 비즈니스 로직에 따라 처리 (1,2번중 선택)

1
2
3
4
5
6
7
8
public Object run(){
Optional<Object> optValue = getObject("key"); // 위의 getObject()를 호출

// 1. 빈 객체를 생성하여 반환
Object value = optValue.orElseGet(Object::new);
// 2. 오류로 종료
optValue.orElseThrow(() -> new NoSuchKeyException("해당 키의 값이 없습니다."));
}

컬렉션은 Optional로 감싸지 말고 빈 컬렉션을 반환

1
2
3
4
5
6
7
// 컬렉션에 데이터가 없으면 Optional로 감싸지 않는다.
List<Object> list = getList();
return Optional.ofNullable(list);

// 대신 빈 컬렉션을 반환한다.
List<Object> list = getList();
return list != null ? list : Collections.emptyList();

그밖의 연산자

Optional 연산 메서드

  • map : Optional의 값을 추출하고 변환
1
2
Optional optInsurance = Optional.ofNullable(insurance);
Optional name = optInsurance.map(Insurance::getName);
  • filter : 특정값을 걸러냄
  • flatMap : Optional 객체 연결
1
2
3
4
5
6
// 옵셔널 사용public String getCarInsuranceName(Person person) {
return person.flatMap(Person::getCar)// flatMap이 아니라 map 연산일 경우, Optional<optional<Car>>형식을 반환한다.
.flatMap(Car::getInsurance)
.map(Insurance::getName)// 메서드 참조형
.orElse("Unknown");
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}

Optional 메서드

  • get() : 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드다. 메서드 get은 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElemnetException을 발생시킨다.
  • orElse(T other) : orElse 메서드를 이용하면 Optional이 값을 포함하지 않을 때 디폴트값을 제공할 수 있다.
  • orElseGet(Supplier other) : orElse 메서드에 대응하는 게으른 버전의 메서드다. Optional에 값이 없을 때만 Supplier가 실행되기 때문이다. 디폴트 메서드를 만드는 데 시간이 걸리거나 Optional이 비어있을 때만 디폴트값을 생성하고 싶을 때 사용한다.
  • orElseThrow(Supplier exceptionSupplier) : Optional이 비어있을 때 발생시킬 예외의 종류를 선택할 수 있다.
  • isPresent() : Optional이 값을 포함하면 true를 반환하고, 값을 포함하지 않으면 false를 반환한다.
  • ifPresent(Consumer consumer) : 값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 아무 일도 일어나지 않는다.
1
.ifPresent(d -> System.out.println(d.getName());

Optional 성능 이슈

Optional을 사용하면 코드가 Null-Safe 해지고, 가독성이 좋아지며 애플리케이션이 안정적이 된다는 등과 같은 얘기들을 많이 접할 수 있습니다. 하지만 이는 Optional을 목적에 맞게 올바르게 사용했을 때의 이야기이고, Optional을 남발하는 코드는 오히려 다음과 같은 부작용(Side-Effect)을 유발할 수 있습니다.

Optional이 위험한 이유

  • NullPointerException 대신 NoSuchElementException가 발생함
    • Null-Safe 하기 위해 Optional을 사용하였는데, 값의 존재 여부를 판단하지 않고 접근한다면 NullPointerException는 피해도 NoSuchElementException가 발생할 수 있다.
  • 이전에는 없었던 새로운 문제들이 발생함
    • 기본적으로 Optional은 직렬화를 지원하지 않기 때문에 캐시나 메세지큐 등과 연동할 때 문제가 발생할 수 있다.
  • 과도한 사용은 코드의 가독성을 떨어뜨림
    • 과도한 사용은 남용될 수 있으며 가독성도 저하될 수 있다.
  • 시간적, 공간적 비용(또는 오버헤드)이 증가함
    • 공간적 비용: Optional은 객체를 감싸는 컨테이너 이므로 기존의 객체를 저장하기 위한 메모리에 더해 Optional 객체를 저장하기 위한 메모리가 할당된다. 이는 추가적인 메모리를 사용하는 것이다.
    • 시간적 비용: Optional 안에 있는 객체를 얻기 위해서는 Optional 객체를 통해 접근해야 하므로 접근 비용이 증가한다.

그렇다면, 무조건 Optional 사용을 지양해야하는가? 아닙니다. Stream 또는 삼항 연산자와 마찬가지로 적절하게 사용하면 가독성을 높이고 복잡성을 줄일 수 있습니다.

올바르게 Optional을 사용하는 방법

  • Optional 변수에 Null을 할당하지 않는다.

    • Optional은 container/boxing 클래스일 뿐이며, Optional 변수에 null을 할당하는 것은 Optional 변수 자체가 null인지 또 검사해야 하는 문제를 야기하므로 값이 없는 경우라면 Optional.empty()로 초기화하는 것이 좋다.
  • 값이 없을 때 Optional.orElseXXX()로 기본 값을 반환한다.

    • Optional의 장점 중 하나는 함수형 인터페이스를 통해 가독성 좋고 유연한 코드를 작성할 수 있다는 것이다. 가급적이면 isPresent()로 검사하고 get()으로 값을 꺼내기보다는 orElseGet 등을 활용해 처리하는 것이 좋다.
    • orElseGet은 값이 준비되어 있지 않은 경우, orElse는 값이 준비되어 있는 경우에 사용하면 된다. 만약 null을 반환해야 하는 경우라면 orElse(null)을 활용하도록 하자. 만약 값이 없어서 throw 해야 하는 경우라면 orElseThrow를 사용하면 되고 그 외에도 다양한 메서드들이 있으니 적당히 활용하면 된다. 추가적으로 Java9 부터는 ifPresentOrElse도 지원하고 있으며, Java 10부터는 orElseThrow()의 기본으로 NoSuchElementException()를 던질 수 있다. 만약 Java8이나 9를 사용 중이라면 명시적으로 넘겨주면 된다.
  • 단순히 값을 얻으려는 목적으로만 Optional을 사용하지 않는다.

    • 단순히 값을 얻으려고 Optional을 사용하는 것은 Optional을 남용하는 대표적인 경우이다. 이러한 경우에는 굳이 Optional을 사용해 비용을 낭비하는 것보다는 직접 값을 다루는 것이 좋다.
    • 메서드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다.
  • 생성자, 수정자, 메서드 파라미터 등으로 Optional을 넘기지 않는다.

    • Optional을 파라미터로 넘기는 것은 상당히 의미 없는 행동이다. 왜냐하면 넘겨온 파라미터를 위해 자체 null체크도 추가로 해주어야 하고, 코드도 복잡해지는 등 상당히 번거로워지기 때문이다.
    • Optional은 Java Bean으로 사용하도록 만들어진 것이 아님을 잊지 말아야 한다.
  • Collection의 경우 Optional을 사용하지 말고 빈 Collection으로 반환한다.

    • Collection, Stream, Array 같은 컨테이너 타입은 Optional로 감싸는 것보다 그냥 빈 객체를 반환하는 것이 처리가 가볍다.
    • Map의 값으로 Optional을 사용하면 절대 안 된다. Map 안에 키 자체가 없거나 키는 있지만 Optional인 경우 복잡성만 증가하고 오류 가능성을 키울 뿐이다.
  • 반환 타입으로만 사용한다.

    • Optional은 반환 타입으로써 에러가 발생할 수 있는 경우에 결과 없음을 명확히 드러내기 위해 만들어졌으며, Stream API와 결합되어 유연한 체이닝 api를 만들기 위해 탄생한 것이다.
    • 예를 들어 Stream API의 findFirst()나 findAny()로 값을 찾는 경우에 어떤 것을 반환하는 게 합리적 일지 Java 언어를 설계하는 사람이 되어 고민해보자. 언어를 만드는 사람의 입장에서는 Null을 반환하는 것보다 값의 유무를 나타내는 객체를 반환하는 것이 합리적일 것이다. Java 언어 설계자들은 이러한 고민 끝에 Optional을 만든 것이다. 그러므로 Optional이 설계된 목적에 맞게 반환 타입으로만 사용되어야 한다.

    정리

    Optional은 null 또는 실제 값을 value로 갖는 wrapper로 감싸서 NPE(NullPointerException)로부터 자유로워지기 위해 나온 Wrapper 클래스입니다. 또한 Optional은 파라미터로 넘어가는 등이 아니라 반환 타입으로써 제한적으로 사용되도록 설계되었습니다. Optional은 값을 Wrapping 하고 다시 풀고, null일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 성능을 저하시킬 수 있습니다.

    하지만 올바른 방식으로 사용한다면 성능 문제가 없기 때문에 Optional 사용을 무조건 지양하기 보단 올바르게 사용하는 법을 익히는 것이 좋을듯합니다.

    실무 예시

    1
    2
    3
    4
    5
    6
    7
    8
    public List<TabMenu> getTabMenu (UserInfo userInfo) {
    return List<TabMenu> tabMenus = Optional.ofNullable(getDBData(userInfo))
    .orElse(fallback());
    }

    private List<TabMenu> fallback() {
    // fallback yml 데이터를 읽어옴
    }
CATALOG
  1. 1. null?Optional?빈 객체 반환?
  2. 2. null 피해야하는 이유?
    1. 2.0.1. null 때문에 발생하는 문제
  • 3. 해결법
    1. 3.1. Optional 사용 이점과 단점 반드시 주의하기
    2. 3.2. Class Optional
      1. 3.2.1. 활용법
      2. 3.2.2. 그밖의 연산자
    3. 3.3. 실무 예시