[Java][Optional] Optional의 위험 및 사용 가이드

2022. 11. 28. 15:55JAVA/Language

1. Optional 클래스가 위험한 이유

  • NullPointerException 대신 NoSuchElementException이 발생 할 수 있음
  • 이전에는 없었던 문제가 발생할 수 있음
  • 코드의 가독성 저하
  • 시간적, 공간적 비용 증가

 

1.1 NullPointerException 대신 NoSuchElementException이 발생할 수 있음

    @Test(expected = NoSuchElementException.class)
    public void testNoSuchElementException(){
        //given
        Optional<String> optional = Optional.ofNullable(null);
        //when
        String actual = optional.get();
        //then
        fail("NoSuchElementException이 발생해야함");
    }
  • Optional로 받은 변수를 값이 있는지 없는지 모르고 get 메서드와 같이 호출하게 되면 NoSuchElementException이 발생할 수 있음
  • NullPointerException은 피해도 NoSuchElementExpceiton이 발생할 수 있음

 

1.2 이전에 없었던 새로운 문제가 발생할 수 있음

Optional 클래스 타입을 필드멤버로 갖게 되면 직렬화(Serialize) 할 수 없습니다. 왜냐하면 Optional 클래스는 직렬화를 지원하지 않기 때문입니다. 다음과 같이 필드 멤버로 Optional을 사용하게 되면 기존에 없었던 새로운 문제가 발생할 수 있습니다.

class Student implements Serializable{
	private Optional<String> name;
    
    ...
}

 

1.3 코드의 가독성 저하

Optional 클래스의 value 멤버가 null이면 참조시 NoSuchElementException이 발생할 수 있습니다. 이에 대비하여 다음과 같이 값의 유무를 검사하도록 작성할 수 있습니다.

    @Test
    public void testIfPresent(){
        //given
        Optional<String> optional = Optional.ofNullable(null);
        String actual = "";
        //when
        if(optional.isPresent()){
            actual = optional.get();
        }else{
            actual = "default";
        }
        //then
        assertThat(actual).isEqualTo("default");
    }
  • 위와 같은 코드는 또다시 NullPointerException이 발생할 수 있습니다.
  • 예를 들어 optional 인스턴스에 Optional.ofNullable(null)이 아닌 null 값 자체를 가리킨다면 isPresent 메서드 호출시 NullPointerException이 발생할 수 있습니다.

 

optional 인스턴스 자체가 null일 경우를 대비하여 다시 다음과 같이 작성해야 할 것입니다.

    @Test
    public void testIfPresent_whenOptionalIsNull(){
        //given
        Optional<String> optional = null;
        String actual = "";
        //when
        if(optional != null && optional.isPresent()){
            actual = optional.get();
        }else{
            actual = "default";
        }
        //then
        assertThat(actual).isEqualTo("default");
    }
  • optional 인스턴스 자체가 null일수 있기 때문에 조건문에 null 검사조건을 추가하였습니다.
  • 이와 같은 코드는 값의 유무를 2번 검사하게 하여 더욱 코드가 복잡해졌습니다.

 

따라서 Optional을 남용하면 오히려 코드의 가독성이 떨어지게 됩니다.

 

1.4 시간적, 공간적 비용 증가

공간적 비용

Optional은 객체를 감싸는 래퍼 클래스이므로 Optional 객체 자체를 저장하기 위한 메모리가 추가로 필요합니다.

 

시간적 비용

Optional 안에 있는 객체를 얻기 위해서 Optional 객체를 통해 접근해야 하므로 접근 비용이 증가합니다.

 

 

2. Optional 사용 가이드

  • Optional 변수에 Null을 할당하지 말아라
  • 값이 없을 때 Optional.orElseGet()으로 기본 값을 반환하라
  • 단순히 값을 얻으려는 목적으로만 Optional을 사용하지 마라
  • 생성자, 수정자, 메소드 파라미터 등으로 Optional을 넘기지 마라
  • Collection의 경우 Optional이 아닌 빈 Collection을 사용하라
  • 반환 타입으로만 사용하라

 

2.1 Optional 변수에 Null을 할당하지 말아라

Optional 변수에 Null을 할당하게 되면 Optional 인스턴스 자체가 null인지 검사해야 하는 문제가 발생합니다. 값이 없는 경우라면 Optional.empty()을 호출하여 빈 Optional 인스턴스를 초기화합니다.

// AVOID
public Optional<Student> fetchStudent(){
	Optional<Student> emptyStudent = null;
    ...
}

// PREFER
public Optional<Student> fetchStudent(){
	Optional<Student> emptyStudent = Optional.empty();
    ...
}

 

2.2 값이 없을 때 Optional.orElseGet()으로 기본 값을 반환하라

Optional 클래스의 orElseGet() 메서드를 사용하면 매개변수에 함수형 인터페이스를 전달하여 value가 null인 경우에 매개변수에 전달된 함수형 인터페이스를 수행시켜 대체값을 반환할 수 있습니다. 따라서 isPresent()와 get() 메서드를 통해 값을 가져오기 보다는 orElseGet() 메서드를 사용하는 것을 권장합니다.

    // AVOID
    @Test
    public void testFindStudentName_isPresentAndGet(){
        //given
        Optional<String> optionalStudentName = Optional.ofNullable(null);
        String actual;
        //when
        if(optionalStudentName.isPresent()){
            actual = optionalStudentName.get();
        }else{
            actual = findDefaultName();
        }
        //then
        assertThat(actual).isEqualTo("defaultName");
    }

    // PREFER
    @Test
    public void testFindStudentName_orElseGet(){
        //given
        Optional<String> optionalStudentName = Optional.ofNullable(null);
        //when
        String actual = optionalStudentName.orElseGet(()->findDefaultName());
        //then
        assertThat(actual).isEqualTo("defaultName");
    }

    private String findDefaultName(){
        return "defaultName";
    }

 

2.3 단순히 값을 얻으려는 목적으로만 Optional을 사용하지 마라

Optional 클래스를 사용해 비용을 낭비하기 보다는 단순한 조건문을 통해서 직접 값을 반환하는 것이 적절합니다.

    // AVOID
    private String findStudentName(Long id){
        String name = "김용환"; // 저장소로부터 값을 얻었다고 가정
        return Optional.ofNullable(name).orElse("미정");
    }
    
    // PREFER
    private String findStudentName(Long id){
        String name = "김용환"; // 저장소로부터 값을 얻었다고 가정
        return name == null ? "미정" : name;
    }

 

2.4 생성자, 수정자, 파라미터 등으로 Optional을 넘기지 마라

  • 넘겨온 매개변수를 위해 Optional 인스턴스 자체를 null 검사를 해야하고 코드도 복잡해짐
  • 예를 들어 생성자로 Optional 타입으로 받았다는 의미는 필드 멤버의 타입도 Optional이 되는데 이는 직렬화를 하지 못하는 문제를 야기 할 수 있음
public class Student{
	private Optional<String> name;
    
    public Student(Optional<String> name){
    	this.name = name;
    }
}

 

2.5 컬렉션이 null인 경우 굳이 Optional을 사용하지 말고 빈 컬렉션을 반환하라

// AVOID
public Optional<List<Student>> getStudentList(){
	List<Student> studentList = ...; // null이 올수도 있음
    return Optional.ofNullable(studentList);
}

// PREFER
public List<Student> getStudentList(){
	List<Student> studentList = ...; // null이 올수도 있음
    return studentList == null ? Collections.emptyList() : studentList;
}

 

2.6 Optional은 반환 타입으로만 사용하라

Optional은 반환 타입으로써 에러가 발생할 수 있는 경우에 결과 없음을 명확히 드러내기 위해 설계되었습니다. 따라서 Optional은 반환타입으로써 대체 동작을 하기 위해 사용하는 것이 적절합니다.

 

References

https://mangkyu.tistory.com/203