본문 바로가기
Language/자바&코틀린

Optional 클래스의 특징과 올바른 사용법

by 코딩공장공장장 2024. 7. 8.

0. Optional 클래스

optional 클래스는 객체가 null을 참조하는 경우 발생할 수 있는 NPE 예외를 벗어나기 위해

내부적으로 null 처리를 대신해주는 래퍼 클래스이다.

public final class Optional<T> {

  private final T value;  
  ...
}

 

1. Optional 클래스를 사용하는 이유

  1. null을 직접 다루게 되면 NPE 예외 발생 가능성이 높아질 수 있음 -> 간접적으로 null을 다루기 위함
  2. null 체크 로직이 반복됨 -> if문으로 지저분해짐

 

Bag이라는 클래스 안에 Purse가 있는 예제를 통해 optional 클래스 사용시 얻는 이점을 살펴보자.

 

[Optional을 사용하지 않는 경우]

 

Bag bag = getBag();

if (bag == null) {

   bag = new Bag();

} else {

   if (bag.purse == null) {

       bag.setPurse(new Purse());

   }

}

optional을 사용하지 않는 경우 위와 같이 중첩 if문으로 코드 가독성이 떨어지고, null 처리 코드가 중복되어 나타날 수 있다.

 

[Optional을 사용하는 경우]

Bag bag1 = Optional.ofNullable(getBag()).orElse(new Bag());

 

Purse purse1 = Optional.ofNullable(bag1.getPurse()).orElse(new Purse());

optional을 사용하는 경우는 if문 구조 없이 NPE를 대처할 수 있다.

 

2. Optional 사용법

2.1.  Optional 생성

- 초기값 없는 경우

Optional<String> optVal = null; // null로 초기화 바람직하지 않음

 

Optional<String> optVal2 = Optional.empty(); // 빈 객체로 초기화

 

생성시 할당할 객체가 없는 경우 위와 같이 null을 할당하거나, empty를 통해 래핑 하는 객체를 null로 선언할 수 있다.

 허나, Optional 객체를 null 자체로 선언하는 것은 Optional 객체에 대한 null 처리를 필요로 하므로 Optional 사용목적과 맞지 않다.

따라서 empty 메서드를 통해 래핑하는 객체를 null로 선언하는 것이 바람직하다.

 

[참고] empty는 모두 같은 객체를 바라봄

 

public final class Optional<T> {

  

   private static final Optional<?> EMPTY = new Optional<>(null);

 

   private final T value;

   . . .

}

empty 메서드를 사용하면 위의 EMPTY 객체가 반환된다.

실제 객체가 참조되는 value 변수에 할당하지 않고 EMPTY에 할당하니 모두 같이 공유하는 것이 가능하고,

이를 통해 메모리 자원의 효율적인 사용 또한 가능하다.

 

- 초기 값(객체) 있는 경우

Optional<String> optStr = Optional.of("abc");

 

Optional<String> optStr2 = Optional.of(null);  // npe 발생

 

Optional<String> optStr3 = Optional.ofNullable("abc");

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

 

객체 할당은 of와 ofNullable을 통해 할당한다.

of는 null이 아님을 보장할 때 사용을 하는데 만일 null이 들어오게 되는 경우 NPE가 발생한다.

ofNullable은 null일 수도 있는 객체를 래핑할때 사용한다. 

 

Optional 자체가 null처리를 Optional 클래스에 맡기기 위함인데 null을 허용하지 않는 of 메서드가 왜 존재하나 의문이 들 수 있지만,

반환값을 Optional 타입으로 지정하고 경우에 따라 null 이 나올 수 있고 null이 안 나올 수도 있는 상황에서 각각을 구분하여 사용하면 명확한 코드 파악에 도움이 될 수 있다.

(null 존재여부에 따라 분기 처리를 of와 ofNullable을 같이 사용하는 경우는 있겠지만, of만 사용하는 경우는 거의 희박할 것이다.)

 

2. 2 Optional 객체의 값 가져오기

i) optional.get();

public T get() {

   if (value == null) {

       throw new NoSuchElementException("No value present");

   }

   return value;

}

 

래핑 객체가 null인 경우 NoSuchElementException이 발생한다.

Optional 클래스의 사용 목적이 null처리를 맡기는 것인데 get을 통해서도 예외가 발생하니 실제 사용성은 낮다.

 

ii) optional.orElse(T other);

public T orElse(T other) {

   return value != null ? value : other;

}

 

NULL일 경우 반환할 객체나 값을 지정할 수 있다.

 

iii) optional.orElseGet(Supplier<? extends T> supplier);

public T orElseGet(Supplier<? extends T> supplier) {

   return value != null ? value : supplier.get();

}

 

람다함수를 인자로 받아 null일 경우 람다함수의 결과를 반환할 수 있다.

 

[참고] orElse와 orElseGet의 차이

아래와 같은 getValue 메서드가 존재한다고 하자.

private String getValue() {

   System.out.println("value");

   return "value";

}

 

[orElse 실행]

String str = "Empty";

String result = Optional.ofNullable(str).orElse(getValue());

 

System.out.println(result);

 

[출력]

value

Empty

 

orElse 메서드는 인자값으로 값을 주입 받기에 getValue 메서드의 리턴 값을 주입하였다.
리턴 값을 가져오기 위해 getValue 메서드가 실행되었다. 어떻게 보면 당연한 자바의 실행 방식이다.

 

[orElseGet]

String str = "Empty";

String result = Optional.ofNullable(str).orElseGet(OptionalGetUsage::getValue);

 

System.out.println(result);

 

[출력]

Empty

 

이 람다함수는 null일 때에만 실행된다.

당영한 동작 방식이지만 실수하기 좋은 패턴이다.

orElse를 사용할 때는 값이 이미 정해져있는 경우에 사용하는 걸 권장하고 orElseGet은 값이 정해지지 않은 경우 사용하는 것이 권장된다.

 

 

iv) optional.orElseThrow(), orElseThrow(Supplier<? extends X> exceptionSupplier)

 

Null 시에 NoSuchElementException을 반환하거나 인자값에 지정한 예외를 발생시킬 수 있다.

 

3. Optional 클래스의 단점

 3.1 나쁜 가독성

이전의 예제코드를 다시 살펴보자.

Bag bag1 = Optional.ofNullable(getBag()).orElse(new Bag());

 

Purse purse1 = Optional.ofNullable(bag1.getPurse()).orElse(new Purse());

Optional에서 Bag을 꺼내고 Bag에서 purse를 꺼내고 purse의 null 체크를 위해 purse를 다시 Optional로 래핑하였다.

(물론 purse를 Optional로 래핑하지 않아도 된지만 그렇게 되면 purse에 대한 null 체크를 직접 수행하게 된다.)

또한, 체인 형식으로 길어지는 코드는 중첩 if문 만큼이나 가독성이 나쁘다.

 

3.2 비효율적인 메모리 사용

  • Optional 객체 생성으로 인한 메모리 점유
    객체를 래핑하는 Optional 또한 객체이므로 메모리 자원을 필요로 한다.

  • 박싱과 언박싱으로 인한 잦은 메모리 할당과 해제
    박싱과 언박싱 과정으로 힙 영역에 메모리를 할당하고 해제하는 작업이 반복되어
    Optional을 사용하지 않는 방식에 비해 높은 메모리 점유와 GC 작업을 유발한다.

 

4. Optional 클래스 사용여부 결정

optional 클래스는 객체 사용 비용이 높고 코드 가독성 또한 좋지 못하기에 null이 나올 수 있는 모든 곳에서 남발하듯 사용하는 것은 좋지 못하다.

 

Optional class 를 설계한 사람은 아래와 같은 말을 하였다.

Optional is intended to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result," and using null for such was overwhelmingly likely to cause errors.

- Brian Goetz(Java Architect) -

“Optional 클래스는 매우 제한적인 메커니즘을 제공하고 라이브러리의 리턴 타입을 위해 설계되었다”

 

나는 설계자의 말과 Optional 클래스의 특징을 고려하면 

Optional 클래스는 null에 대한 처리를 직접할 수 없고 제3자가 해야하는 경우에 사용하도록 설계하였다”고 해석이 된다.

 

null 처리를 개발자가 직접할 수 있다면 null 처리를 직접 진행하는게 가장 좋은 방식이라고 생각한다.

마땅히 처리 할게 없다면 예외를 터트리는 것도 굉장히 좋은 방법이 될 수 있다.

 

그러나 라이브러리나 모듈을 개발할 때에는 리턴타입에 대한 null 처리는 라이브러리 개발자가 하는게 아니라 라이브러리 사용하는 사용자들이 null 처리를 하게 된다.
라이브러리에서 넘어온 null을 사용자가 처리 하지 않으면 NPE 문제가 발생할 수 있다.

이런 상황에서 Optional 클래스를 반환타입으로 제공하면 사용자들은 null 처리를 대신해주는 Optional 클래스의 메서드를 통해
강제적으로 null처리를 하여 객체를 가져올 수 있다.

 

개인적으로 Optional을 사용하는 목적은 null을 대신 처리해주기 위함이 아니라 사용자들로 하여금 null 처리를 강제화하는 것에 의의가 있지 않나 싶다.

반응형