Language/Java

Java 8 - Optional이란?

JaeHoney 2022. 3. 12. 21:02

Optional

Optional은 Null 검사를 기존의 번거로운 방법에서 벗어나기 위해 Java 8 버전부터 제공하는 클래스입니다.

 

Optional<T>는 T타입의 객체를 포장합니다. Optional을 사용하면 T타입 객체가 null일때 결과를 핸들링하거나, null이 아니라 비어 있는 Optional을 반환해서 NullPointerException을 방지할 수 있습니다.

 

문제 (예시)

ThreadLocal이라는 클래스가 있다고 가정합시다.

public class ThreadLocal {
    DBInfo info;

    public DBInfo getDBInfo() {
        return info;
    }
}

만약 아래 코드에서 ThreadLocal.getDBInfo()의 결과가 null이 되면 어떻게 될까요? 많이 발생하는 문제입니다.

int partitionNo = ThreadLocal.getDBInfo().getPartition();

결과는 당연히 NullPointerException이 발생합니다.

 

기존의 문제 해결 방식

문제를 해결하려면 중간에 null이 발생할 수 있는 메서드의 결과를 변수에 임시로 저장했다가, null검사를 하고 사용할 수 있습니다.

DBInfo info = ThreadLocal.getDBInfo();
String partitionNo;
if(info != null) {
    partitionNo = info.getPartition();
}

하지만, 저런 방식은 null 체크를 깜빡해서 에러를 내기도 쉬워지고, null 체크로 인해서 코드가 지저분합니다. CleanCode에서는 null을 리턴하는 것 자체가 문제라는 주장도 있었습니다.

 

다른 방법으로는 메서드가 null이 아니라 에러를 던지도록 수정하는 것입니다.

public class ThreadLocal {
    DBInfo info;

    public DBInfo getDBInfo() {
        if (this.info == null) {
            throw new IllegalStateException();
        }
        return info;
    }
}

첫 번째 방법보다는 낫지만, 던진 에러에 대한 에러 처리가 강제 되고,  에러가 발생할 때마다 stacktrace가 출력되므로, 서버 리소스가 낭비됩니다.

 

Optional을 활용한 문제 해결 방식

문제를 해결하기 위해 Optional.ofNullable을 사용해서 Return값을 Optional로 감싸는 방법이 있습니다.

public Optional<DBInfo> getDBInfo() {
    return Optional.ofNullable(dbInfo);
}

메소드 호출부에서는 null검사를 하지 않아도, 메소드의 리턴값이 Optional로 정의되있음을 인지하고, Optional을 이용해서 체크만 해주면 됩니다.

Optional<DBInfo> info = ThreadLocal.getDBInfo();
info.ifPresent(info -> process(info.getPartition()));

응용

 

아래와 같은 Optional이 하나 있다고 가정하겠습니다. -> (students 중에 name이 Kim인 것을 선택하고, 없으면 빈 Optional을 반환)

Optional<Student> kim = students.stream()
	.filter(s -> s.getName().equals("Kim")
	.findFirst();

 

Optional은 비어있는 지 확인하고 get으로 가져와야 합니다. 아래 코드는 Optional이 비어있는지 확인하고, 비어있으면 새롭게 Student를 생성하는 코드입니다.

Student student;

if(kim.isPresent()) {
    student = kim.get();
} else {
    createNewStudnet("Kim");
}

orElseGet() 메서드를 사용하면 훨씬 간결하게 줄일 수 있습니다.

Optional이 비어있으면 괄호의 함수를 실행하고, 그렇지 않으면 get합니다.

Student student = kim.orElseGet(createNewStudent("Kim"));

 

orElseThrow를 사용하면 Optional이 비어있을 때 예외를 던질 수도 있습니다.

Student student = kim.orElseThrow(IllegalStateException::new);

 

주의 사항

1. Optional은 Return 값으로만 사용하기를 권장합니다. (메소드 매개변수, 맵의 키, 인스턴스 필드 X)

 

예를 들어, 아래와 같이 Optional을 매개변수로 사용한 경우 Optional이 비어있을 수 있습니다.

public void setDBInfo(Optional<DBInfo> info) {
    this.info = info.get();
}

따라서, ifPresent() 등을 통해서 Optional 내부가 비어있는지 검사하는 로직이 필요합니다.

public void setDBInfo(Optional<DBInfo> info) {
    info.ifPresent(i -> this.info = i);
}

 

나름 안전하게 사용하고 싶어서 저렇게 처리를 해도 무의미합니다. 메서드를 호출하는 쪽에서 null이 넘어오면, null을 참조해서 ifPresent를 찾게 되기 때문에, NullPointerException이 발생합니다. 

ThreadLocal.getDBInfo(null);

그래서 Null체크를 하지 않기 위해서 Optional을 사용하는데, Null체크도 하고 Optional도 사용해야 합니다.

 

Optional은 Return값으로만 사용하는 것이 바람직합니다.

 

2. Primitive type을 사용할 때는 Optional을 사용하지 말자!

 

Primitive type을 사용할 때 of를 사용할 수 있지만, 이 경우 박싱, 언박싱이 발생해서 리소스를 낭비하게 됩니다.

Optional.of(10);

그래서 OptionalInt, OptionalLong, ...을 사용해야 리소스 낭비를 줄일 수 있습니다.

OptionalInt.of(10);

 

3. Optional을 사용하는 메소드에서 null을 리턴하지 말자!

 

Optional을 반환하는 메소드에서 null을 반환하게 되면, 호출하는 과정에서 메소드의 결과가 당연히 Optional이라고 생각하기 때문에 그에 맞게 처리를 하게 되서 문제가 발생하게 됩니다.

public Optional<DBInfo> getDBInfo() {
    return this.dbInfo.isDel() ? null : Optional.ofNullable(dbInfo);
}

따라서 null을 리턴해선 안됩니다.

Optional.empty()

Optional.empty()를 null 대신 사용해야 합니다.

 

4. 남용하지 말아라!

 

Optional은 기존의 값을 Wrapping하고, 값이 null일 경우 대체 함수를 호출하는 작업 등 Optional을 사용하지 않을 때보다 당연히 성능이 떨어집니다.

 

null이 나올 리가 없는 메소드까지 몽땅 Optional을 사용하면 리소스 낭비가 됩니다.