본문 바로가기
Computer Sience/Java

[JAVA8] Optional

by 제우제우 2024. 10. 3.

목차

  • Optional 등장 배경 
  • Optional 주의 사항 
  • Optional API 
  • 참고 자료

Optional 등장 배경 

자바 프로그래밍에서 NullPointerException을 종종 보게 되는 이유 

public class Progress {
    private Duration studyDuration;
    private boolean finished;
    public Duration getStudyDuration() {
        return studyDuration;
    }
    public void setStudyDuration(Duration studyDuration) {
        this.studyDuration = studyDuration;
    }
}

public class OnlineClass {
    private Integer id;
    private String title;
    private boolean closed;
    public Progress progress;
    
    public OnlineClass(Integer id, String title, boolean closed) {
        this.id = id;
        this.title = title;
        this.closed = closed;
    }
    // getter & setter 생략 
}

public class App {
    public static void main(String[] args) {
        OnlineClass spring_boot = new OnlineClass(1, "spring_boot", true);
        // NullPointerException 발생 getProgress() -> null 
        Duration studyDuration = spring_boot.getProgress().getStudyDuration();
        System.out.println("studyDuration = " + studyDuration);
    }
}

// 자바8 Optional 등장 이전에 null 체크 방식 
public class App {
    public static void main(String[] args) {
        OnlineClass spring_boot = new OnlineClass(1, "spring_boot", true);
        Progress progress = spring_boot.getProgress();
        if(progress != null){
            System.out.println(progress.getStudyDuration());
        }
    }
}

 

비용 측면에는 문제가 딱히 없다. 

하지만 null 체크를 항상 수동으로 해야 한다는 부담이 크다.

사람이기 때문에 빠뜨리거나 실수로 null 체크를 잊을 수 있다.

또한 모든 참조에서 null을 확인하는 코드는 불필요하게 복잡해질 수 있으며, 코드 가독성을 떨어뜨릴 수 있다.

 

만약 return 값이 null 이라면 예외를 던지는 방식 

public class Progress {
    private Duration studyDuration;
    private boolean finished;
    // 반환값이 null -> 예외를 던진다.
    public Duration getStudyDuration() {
        if (studyDuration == null) throw new IllegalStateException("studyDuration is null");
        return studyDuration;
    }
    public void setStudyDuration(Duration studyDuration) {
        this.studyDuration = studyDuration;
    }
}

 

이 방식의 문제점 

 

Checked 예외

만약 예외가 체크드(Checked) 예외라면, 개발자는 그 예외를 반드시 처리해야 하므로 코드에 예외 처리 로직이 강제적으로 추가된다. 이는 코드의 복잡도를 높이고, 개발자가 의도하지 않은 부분에서 예외 처리가 필요하게 만들 수 있다.

 

스택 트레이스 비용

예외가 발생하면 JVM은 해당 예외와 관련된 전체 스택 트레이스를 생성하여 로그에 기록한다.

이는 디버깅에는 유용하지만, 불필요하게 많은 예외를 던질 경우 성능 문제가 발생할 수 있다.

특히 예외가 자주 발생하면 성능에 부정적인 영향을 미칠 수 있다.

 

Optional 적용 

 

자바 8부터는 Optional을 활용하여 null 체크를 명시적으로 처리하는 방식이 등장했다.

간단하게 설명하면 오직 값 한 개가 들어있을 수도 없을 수도 있는 컨테이너이다. 

public class Progress {
    private Duration studyDuration;
    private boolean finished;
    public Optional<Duration> getStudyDuration() {
        return Optional.ofNullable(studyDuration);
    }
    public void setStudyDuration(Duration studyDuration) {
        this.studyDuration = studyDuration;
    }
}

 

ofNullable: Null 가능성이 있는 값을 Optional로 감싸서 사용 

of: Null 가능성이 없는 값을 Optional로 감싸서 사용 

 

Optional of() API에 대한 내 생각 

근데 Null 가능성이 없는 경우라면 Optional.of()로 감쌀 필요 없이 바로 값을 반환하는 것이 더 효울적인게 아닌가 싶다. 

Optional의 주요 목적은 null 값을 안전하게 다루기 위한 것인데, null 가능성이 없다면 굳이....


Optional 주의 사항 

리턴값으로만 쓰기를 권장한다. (메소드 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 쓰지 말자.)

public class OnlineClass {
    private Integer id;
    private String title;
    private boolean closed;
    public Progress progress;
    public Optional<Progress> getProgress() {
        return Optional.ofNullable(progress);
    }
    public void setProgress(Optional<Progress> progress) {
         progress.ifPresent(p -> this.progress = p);
    }
}

public class App {
    public static void main(String[] args) {
        OnlineClass spring_boot = new OnlineClass(1, "spring_boot", true);
        spring_boot.setProgress(null);
    }
}

 

Progress 클래스의 setProgress 메소드를 보면 파라미터로 Optional<Progress>를 받고 있다. 

그리고 Optional API인 ifPresent(Consumer<T> action)을 통해서 Progress 값이 null이 아니라면 set 하고 있다.

해당 로직의 문제점은 무엇일까? 

 

지금 App main() 메소드를 보면 setProgress에 null을 넘기고 있다. 

그럼 progress가 null 이니 progress.ifPresent(p -> this.progress = p); ifPresent API 호출하는 순간 

NullPointException이 발생한다. 

 

이를 방지하려면 null 값 체크 + Optional API를 통해서 존재하는지 2번 검증해야 하는데  Optional을 사용하는 이유가 있을까? 없다. 

 

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

Optional의 주된 목적이 null을 안전하게 대체하는 것이기 때문에, Optional을 사용하면서도 null을 반환하면 Optional의 의도를 무색하게 만드는 셈

 

프리미티브 타입용 Optional을 따로 있다. OptionalInt, OptionalLong ....

Optional<Integer> optional = Optional.ofNullable(10);

 

여기서 10은 기본 타입 int이지만, Optional은 참조 타입만 받을 수 있기 때문에 오토박싱이 일어나서 Integer 객체로 변환

마치 컬렉션에 원시 타입을 저장할 때 int → Integer 오토 박싱 같은 사례 결과적으로 Optional<Integer>가 반환.

이는 참조 타입을 사용하기 때문에 기본 타입보다 더 많은 메모리를 사용하게 되고, 오토박싱/언박싱 과정에서 성능 저하가 발생할 수 있다.

OptionalInt optionalInt = OptionalInt.of(10);

 

OptionalInt는 기본 타입인 int를 위한 Optional의 특화된 버전

오토박싱 없이 기본 타입 int를 그대로 저장하므로, 성능 면에서 더 효율적

OptionalInt는 Optional<Integer>와는 다르게 null을 표현할 수 없기 때문에, 값이 없는 경우 OptionalInt.empty()를 사용해야 한다.

OptionalInt empty = OptionalInt.empty();
if(empty.isPresent()){
        System.out.println(empty.getAsInt());
}
else System.out.println("is Empty");

// is Empty

 

Collection, Map, Stream Array, Optional은 Opiontal로 감싸지 말 것

 

해당 예시들은 이미 그 값 자체로도 비어있다를 표현 가능한 컨테이너 성격의 객체들로서 Optional로 감싸는 게 의미가 없다.

List<Integer> emptyList = new ArrayList<>();
System.out.println(emptyList.isEmpty()); // true

Optional API 

Optional 만들기

  • Optional.of()
  • Optional.ofNullable()
  • Optional.empty()
Optional<String> test1 = Optional.of("test");
Optional<String> test2 = Optional.ofNullable(null);
Optional<String> test4 = Optional.empty();

 

Optional.of ()

값을 감싸서 Optional 객체를 생성

이 메서드는 null을 허용하지 않기 때문에 만약 null을 전달하면 NullPointerException이 발생

 

Optional.ofNullable()

값을 감싸서 Optional 객체를 생성하는데, null도 허용합니다. 만약 값이 null이면 자동으로 Optional.empty()가 생성

 

Optional.empty()

값이 없는 빈 Optional 객체를 명시적으로 생성할 때 사용

이 경우, Optional 객체는 항상 값이 존재하지 않는 상태로 설정

 

Optional에 값이 있는지 없는지 확인하기

public class App {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));

        // Optional 반환 Stream 종료 연산
        Optional<OnlineClass> spring = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("spring"))
                .findFirst();

        boolean present = spring.isPresent();
        System.out.println("present = " + present); // true

        Optional<OnlineClass> jpa = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("jpa"))
                .findAny();

        boolean empty = jpa.isEmpty();
        System.out.println("empty = " + empty);     // true
    }
}

출력 
present = true
empty = true
  • isPresent()
  • isEmpty() (Java 11부터 제공)

Optional에 있는 값 가져오기

  • get()
  • 만약에 비어있는 Optional에서 무언가를 꺼낸다면?? → NoSuchElementException
// Optional 생성
Optional<String> test1 = Optional.of("test");
Optional<String> test2 = Optional.ofNullable(null);

// Optional get()
System.out.println(test1.get()); // test 출력

System.out.println(test2.get()); // NullSuchElementException

 

Optional에 값이 있는 경우에 그 값을 가지고 ~~를 하라

 

ifPresent(Consumer action)

List<OnlineClass> springClasses = new ArrayList<>();
springClasses.add(new OnlineClass(1, "spring boot", true));
springClasses.add(new OnlineClass(5, "rest api development", false));

Optional<OnlineClass> optional = springClasses.stream()
        .filter(oc -> oc.getTitle().startsWith("spring"))
        .findFirst();

optional.ifPresent(oc -> System.out.println(oc.getTitle()));

 

 

Optional에 값이 있으면 가져오고 없는 경우에 ~~를 리턴하라

 

orElse(T)

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("jpa"))
                .findFirst();

        OnlineClass onlineClass = optional.orElse(createNewClass());
        System.out.println("onlineClass = " + onlineClass.getTitle());
    }

    private static OnlineClass createNewClass() {
        System.out.println("Practice.createNewClass");
        return new OnlineClass(10, "new class", false);
    }
}

// 출력 
Practice.createNewClass
onlineClass = new class

 

현재 OnlineClass 인스턴스에는 jpa로 시작하는 제목을 가진 인스턴스는 없다. 

createNewClass() 메소드에서 생성한 OnlineClass 인스턴스를 반환한다. 

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("spring"))
                .findFirst();

        OnlineClass onlineClass = optional.orElse(createNewClass());
        System.out.println("onlineClass = " + onlineClass.getTitle());
    }

    private static OnlineClass createNewClass() {
        System.out.println("Practice.createNewClass");
        return new OnlineClass(10, "new class", false);
    }
}

// 출력 
Practice.createNewClass
onlineClass = spring boot

 

현재 OnlineClass 인스턴스에는 "spring"로 시작하는 제목을 가진 인스턴스가 있다. 

그런데 반환 자체는 찾은 인스턴스를 반환하지만 createNewClass 메소드가 호출되었다는 걸 출력을 통해 알 수 있었다.

왜 그럴까?

 

orElse() 메소드는 전달된 인자(createNewClass())를 즉시 호출한다.

따라서 createNewClass()가 반환하는 OnlineClass 인스턴스를 생성하는 메서드는 항상 호출

orElse() 대신 지연된 호출을 원할 경우 orElseGet() 메서드를 사용할 수 있다.

 

Optional에 값이 있으면 가져오고 없는 경우에 ~~를 하라

 

orElseGet(Supplier) 

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("spring"))
                .findFirst();

        OnlineClass onlineClass = optional.orElseGet(Practice::createNewClass); // Supplier
        System.out.println(onlineClass.getTitle());
    }

    private static OnlineClass createNewClass() {
        System.out.println("Practice.createNewClass");
        return new OnlineClass(10, "new class", false);
    }
}

// 출력 
spring boot

 

Supplier 통한 지연 방식(지연된 실행(lazy evaluation))

Optional 값이 있으면 createNewClass() 메소드를 호출하지 않고 가져오고 

Optional 값이 없으면 Supplier에 정의된 createNewClass() 메소드가 호출되어 OnlineClass 인스턴스를 만들고 반환 

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("jpa"))
                .findFirst();

        OnlineClass onlineClass = optional.orElseGet(Practice::createNewClass); // Supplier
        System.out.println(onlineClass.getTitle());
    }

    private static OnlineClass createNewClass() {
        System.out.println("Practice.createNewClass");
        return new OnlineClass(10, "new class", false);
    }
}

// 출력 
Practice.createNewClass
new class

 

Optional에 값이 있으면 가져오고 없는 경우 에러를 던져라

 

orElseThrow(Supplier<Thorwable> exceptionSupplier )

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("jpa"))
                .findFirst();
        // default Exception = NoSuchElementException                 
        OnlineClass onlineClass = optional.orElseThrow();
    }
}

// 출력 
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.orElseThrow(Optional.java:377)

 

기본 예외는 NoSuchElementException 

public class Practice {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(5, "rest api development", false));
        
        Optional<OnlineClass> optional = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("jpa"))
                .findFirst();
        // default Exception = NoSuchElementException                 
        OnlineClass onlineClass = optional.orElseThrow(IllegalStateException::new);
    }
}

// 출력 
Exception in thread "main" java.lang.IllegalStateException
at java.base/java.util.Optional.orElseThrow(Optional.java:403)

 

다른 예외 던지기 Supplier를 매개변수로 받기 때문에 메소드 래퍼런스 활용 

 

Optional에 들어있는 값 걸러내기

 

Optional filter(Predicate)

 

List<OnlineClass> classes = new ArrayList<>();
classes.add(new OnlineClass(1, "spring class1",true));
classes.add(new OnlineClass(2, "spring class2",false));

Optional<OnlineClass> closedClass = classes.stream() // id = 1 spring class1
        .filter(oc -> oc.isClosed())
        .findAny();

Optional<OnlineClass> closedClassAndIdOverOne = closedClass.filter(oc -> oc.getId() >= 1);

 

Optional에 들어있는 값 변환하기

  • Optional map(Function)
  • Optional flatMap(Function): Optional 안에 들어있는 인스턴스가 Optional인 경우에 사용하면 편리하다.
public class OnlineClass {
    private Integer id;
    private String title;
    private boolean closed;
    public Progress progress;

    public Optional<Progress> getProgress() {
        return Optional.ofNullable(progress);
    }
    // 생략 ...
}    

List<OnlineClass> classes = new ArrayList<>();
classes.add(new OnlineClass(1, "spring class1",true));
classes.add(new OnlineClass(2, "spring class2",false));

Optional<OnlineClass> optional1 = classes.stream().filter(oc -> oc.getId() > 1).findFirst();
Optional<Optional<Progress>> progress1 = optional1.map(OnlineClass::getProgress);

 

OnlineClass 인스턴스에는 Progress가 존재

현재 getProgress 리턴 타입은 Optional

 

여기서 stream().map(OnlineClass::getProgress) 타입은 Optional<Progress> 

그런데 findAny() 또한 Optional로 감싸서 반환하기 때문에 Optional로 2번 감싸진 Progress가 나오게 된다.

 

Optional.flatMap() 활용

List<OnlineClass> classes = new ArrayList<>();
classes.add(new OnlineClass(1, "spring class1",true));
classes.add(new OnlineClass(2, "spring class2",false));

Optional<OnlineClass> optional2 = classes.stream().filter(oc -> oc.getId() > 1).findFirst();
Optional<Progress> progress2 = optional2.flatMap(OnlineClass::getProgress);

 

Optional flatMap VS Stream flatMap

 

Optional flatMap 같은 경우 input이 하나면 output이 하나이다. 

Optional flatMap에 넘겨주는 함수(Function)의 결과가 Optional인 경우에 사용하는데  

Optional 자체가 한 값만 가질 수 있는 거니까 아웃풋은 하나다.

 

반면 Stream flatMap 같은 경우 input이 하나여도 output이 여러개일 수 있다.

ex) List<List<String>> list = new ArrayList<>();


참고 자료 

인프런 백기선님 더 자바, Java 8

 

더 자바, Java 8 강의 | 백기선 - 인프런

백기선 | 자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합

www.inflearn.com

 

자바8 공식 문서