반응형

Optional API

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

  • 메소드 호출 시 null을 리턴 && null 체크를 하지 않은 경우

 

메소드에서 작업 중 특별한 상황에서 값을 제대로 리턴할 수 없는 경우 선택할 수 있는 방법

  • 자바 8 이전에는 아래 방법 이외에 별다른 대안이 없다.
    • 예외를 던진다.
      • 자바는 에러가 발생하기 전까지의 스택 트레이스를 찍어둔다. 이 자체로 리소스를 많이 사용한다.
    • null을 리턴한다.
      • 비용 문제가 없지만 그 코드를 사용하는 클라이언트 코드가 주의해야 한다.
  • (자바 8부터) Optional을 리턴한다.
    • 클라이언트에 코드에게 명시적으로 빈 값일 수도 있다는 걸 알려주고, 빈 값인 경우에 대한 처리를 강제한다.

Optional

  • 오직 값 한 개가 들어있을 수도 없을 수도 있는 컨테이너.

 

Optional 사용 시 주의할 것

  • 리턴 값으로만 쓰기를 권장한다.
    • 메소드 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 사용해도 문법적인 오류는 발생하지 않는다.
    • 위의 타입에 사용할 경우, null 처리가 오히려 더 복잡해지거나 맵의 특징을 깨드리는 등의 문제가 발생할 수 있다.
  • Optional을 리턴하는 메소드에서 null을 리턴하지 말자.
    • null 리턴 시, 해당 변수로 Optional 관련 메소드 호출하면 에러가 발생한다.
    • 값이 없다면 Optional의 empty() 메소드를 사용해야한다.
  • 프리미티브 타입은 일반 Optional을 사용하지 말자.
    • 프리미티브 타입용 Optional을 따로 있다.
      • OptionalInt, OptionalLong, ...
    • 해당하는 프리미티브 타입용 Optional을 사용하여 불필요한 박싱, 언박싱이 일어나지 않도록 하는 것이 성능적으로 좋다.
  • Collection, Map, Stream Array, Optional은 Optional로 감싸지 말자.
    • 컨테이너 성격의 인스턴스들은 자신이 비었다는 것을 표현할 수 있다.
    • 즉, Optional로 감싸지 않아도 이미 그 자체로 null여부 판단이 가능하기 때문에 이를 Optional로 감싸는 것은 불필요한 행위이다.

 

Optional 개념 예제

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;
    /*
        아래와 같이 인스턴스 필드 타입으로 Optional을 사용하는 것은 좋지 않다.
        아래와 같은 형태는 설계의 문제이다.
        상위 클래스, 하위 클래스로 쪼개거나 위임 방식을 사용하는 것이 좋다.
    */
//  public Optional<Progress> progress;

    public OnlineClass(Integer id, String title, boolean closed) {
        this.id = id;
        this.title = title;
        this.closed = closed;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public boolean isClosed() {
        return closed;
    }

    public void setClosed(boolean closed) {
        this.closed = closed;
    }

//    public Progress getProgress() {
//        에러는 필요한 상황에서만 던져야 하고 로직에서 사용하는 것은 좋지 않다.
//        if (this.getProgress() == null) {
//            throw new IllegalStateException();
//        }
//        return progress;
//    }
    public Optional<Progress> getProgress() {
        return Optional.ofNullable(progress);
        /*
            아래와 같이 Optional을 리턴하는 메소드에서 null을 리턴하면 안된다.
            null을 리턴하면 Optional을 받는 변수에서 관련 메소드 호출 시, 에러가 발생한다.
        */
//      return null;
    }

    public void setProgress(Optional<Progress> progress) {
        /*
            Optional은 리턴 값으로만 사용하는 것이 권장된다.
            위와 같이 매개변수에 Optional을 사용한 경우, 위험하다.(NullPointerException)
            매개변수가 null로 전달된 경우, 오히려 매개변수에 Optional을 사용함으로서 처리가 더 복잡해질 수 있다.(추가적인 null 비교 조건 필요)
            비교 조건을 추가하지 않는다면, 메소드 호출 시, 매개변수가 null일 경우에도 progress가 null로 설정되고 결과적으로 에러가 발생한다.
        */
        if (progress != null) {
            progress.ifPresent((p) -> this.progress = p);
        }
    }
}

 

public class Main {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot", true));
        springClasses.add(new OnlineClass(2, "spring data jpa", true));
        springClasses.add(new OnlineClass(3, "spring mvc", false));
        springClasses.add(new OnlineClass(4, "spring core", false));
        springClasses.add(new OnlineClass(5, "rest api development", false));

        OnlineClass spring_boot = new OnlineClass(1, "spring boot", true);
        Optional<Progress> progress = spring_boot.getProgress();
        progress.ifPresent((p) -> System.out.println(p.getStudyDuration()));

        /*
            Map의 Key 타입에 Optional을 사용하는 것은 매우 좋지 않은 방법이다.
            Map의 특징 중 하나가 Key 타입은 null일 수 없다는 점인데
            Optional을 사용한다면 Key 타입은 null이 아니라는 Map의 특징을 깨트리는 행위이다.
        */

        /*
            프리미티브 타입을 아래와 같이 일반 Optional로 사용하는 것은 권장하지 않는다.
            내부적으로 프리미티브 타입이 박싱, 언박싱되는 불필요한 단계를 거치게 된다.(성능이 좋지 않다.)
            따라서 프리미티브 타입용 Optional을 사용해서 불필요한 박싱, 언박싱을 방지해야 한다.(권장)
        */
        Optional.of(10); // 프리미티브 타입 Optional 생성 올바른 방법 X
        OptionalInt.of(10); // 프리미티브 타입 Optional 생성 올바른 방법 O

    }
}

[참고자료]

더 자바, Java 8, 백기선

Class Optional<T>

Tired of Null Pointer Exceptions? Consider Using Java SE 8's "Optional"!

이팩티브 자바 3판, 아이템 55 적절한 경우 Optional을 리턴하라.

반응형

+ Recent posts