[Java][Optional] Optional 클래스를 통한 Null 처리

2022. 11. 28. 14:25JAVA/Language

1. Optional 클래스의 개념

1.1 NPE(NullPointerException)란 무엇인가?

NPE란 null을 가리키고 있는 객체가 객체의 멤버를 참조하려고 할때 발생하는 예외입니다. 다음 예시는 null값을 가지는 문자열 객체가 메서드를 호출하려고 하는 예시입니다.

public boolean isHello(String text){
	return text.equals("hello");
}
  • text는 null이기 때문에 NullPointerException 예외가 발생합니다.

 

위와 같은 경우를 막기 위해서 조건문을 통해서 null인지 검사를 하고 호출할 수 있습니다.

public boolean isHello(String text){
    if(text != null){
        return text.equals("hello");
    }
    return false;
}
  • 위와 같은 null 조건문 검사의 문제점은 다른 메서드에서도 동일하게 발생할 가능성이 높습니다.
  • 매개변수가 늘어날 수록 동일한 형식의 null 검사 조건문이 늘어날 수 있습니다.

 

1.2 Optional 클래스란 무엇인가?

  • Optional<T> 클래스는 Null이 올수 있는 값을 감싸는 래퍼(Wrapper) 클래스
  • Optional 클래스의 필드멤버인 value에 null이 올수 있는 값을 저장하기 때문에 null이 오더라도 바로 NPE가 발생하지 않으며 NPE 대신 다른 대체적인 방법(기본값, 예외, 함수형 인터페이스)을 제공합니다.
public final class Optional<T> {
    /**
     * If non-null, the value; if null, indicates no value is present
     */
    private final T value;

	...
 }

 

 

2. Optional 활용하기

2.1 Optional 인스턴스의 생성방법

2.1.1 Optional.empty() : 값이 없는 경우

Optional.empty() 호출시 값이 없는 Optional<T> 인스턴스를 반환합니다.

    @Test
    public void testOptionalEmpty(){
        //given
        Optional<Student> emptyPerson = Optional.empty();
        //when
        boolean actual = emptyPerson.isPresent();
        //then
        assertThat(actual).isFalse();
    }
  • emptyPerson 인스턴스에 있는 value 필드 멤버가 null인 상태입니다.

 

2.1.2 Optional.of() : 값이 null이 아닌 경우

Optional.of() 메서드에 들어가는 값이 절대 null이 아닌 경우에 사용할 수 있습니다. 만약 매개변수로 들어가는 값이 null인 경우에 NullPointerException이 발생합니다.

    @Test
    public void testOptionalOf(){
        //given
        Student std1 = new Student(1L, "김용환");
        //when
        Optional<Student> optionalStudent = Optional.of(std1);
        Student actual = optionalStudent.get();
        //then
        assertThat(actual.getName()).isEqualTo("김용환");
    }

    @Test(expected = NullPointerException.class)
    public void testOptionalOf_whenStudentIsNull(){
        //given
        Student std1 = null;
        //when
        Optional<Student> optionalStudent = Optional.of(std1);
        //then
        fail("NullPointerException이 발생해야 합니다.");
    }
  • Optional.of() 메서드에 null이 들어오는 경우에 NullPointerException이 발생함

 

2.1.3 Optional.ofNullable() : 값이 null일 수도 있고 null이 아닐 수도 있는 경우

만약 어떤 데이터가 null일수도 있고 null이 아닐수도 있는 경우 Optional.ofNullable() 메서드를 사용할 수 있습니다. Optional.ofNullable()로 생성한 Optional 인스턴스는 orElse 또는 orElseGet 메서드를 이용하여 값이 없는 경우에 대해서도 대처(기본값 등)할 수 있습니다.

    @Test
    public void testOptionalOfNullable(){
        //given
        Student std1 = null;
        //when
        Optional<Student> optionalStudent = Optional.ofNullable(std1);
        Student student = optionalStudent.orElse(new Student(0L, "미정"));
        //then
        assertThat(student.getName()).isEqualTo("미정");
    }
  • std1 인스턴스는 null값이기 때문에 orElse에 생성되어 있는 인스턴스를 반환합니다.
  • 가장 권장하는 방법

 

2.2 Optional 사용법 예시

예를 들어 Person 클래스와 University 클래스가 다음과 같이 정의되어 있다고 가정합니다.

@Getter
@Setter
@AllArgsConstructor
class Person {
    private String name;
    private int age;
    private University university;
}
@Getter
@Setter
@AllArgsConstructor
class University {
    String universityName;
    int grade;
    String studentId;
}

 

다음과 같이 Person 인스턴스의 학번(studentId)을 반환하는 메서드가 있다고 가정합니다.

    // AVOID
    public String getStudentId(Person person){
        return person.getUniversity().getStudentId();
    }
  • 다음 메서드는 NullPointerException이 발생할 가능성이 높은 메서드입니다.
  • person, university 인스턴스 중 어느 하나라도 null인 경우 NullPointerException이 발생할 것입니다.
  • studnetId 인스턴스도 null일 경우 예외는 발생하지는 않으나 메서드 호출부쪽으로 null이 전달되어 NullPointerException이 전파될 가능성이 높습니다.

 

따라서 Optional.ofNullable()을 사용하여 다음과 같이 리팩토링할 수 있습니다.

    // PREFER
    public String getStudentId(Person person){
        return Optional.ofNullable(person)
                        .map(Person::getUniversity)
                        .map(University::getStudentId)
                        .orElse("student is null");
    }
  • person, university, studentId중 어느 하나라도 null인 경우 "student is null" 값이 반환될 것입니다.
  • 전부 null이 아니라면 정상적으로 studentId 값이 반환될 것입니다.

테스트 코드는 다음과 같습니다.

    @Test
    public void testGetStudentId(){
        //given
        Person person1 = new Person("kim", 20, new University("A대", 1, "std1"));
        Person person2 = null;
        Person person3 = new Person("lee", 21, null);
        Person person4 = new Person("park", 20, new University("A대", 1, null));
        //when
        String actual1 = getStudentId(person1);
        String actual2 = getStudentId(person2);
        String actual3 = getStudentId(person3);
        String actual4 = getStudentId(person4);
        //then
        assertThat(actual1).isEqualTo("std1");
        assertThat(actual2).isEqualTo("student is null");
        assertThat(actual3).isEqualTo("student is null");
        assertThat(actual4).isEqualTo("student is null");
    }

 

3. Optional의 orElse와 orElseGet 차이 및 예시 코드

3.1 orElse와 orElseGet의 차이

orElse 메서드

  • 매개변수 타입으로 값(T other)을 받음
  • Optional의 value가 null인 경우 매개변수로 받은 값(other)을 반환함

 

 

orElseGet 메서드

  • 매개변수 타입으로 함수형 인터페이스(함수)를 받음
  • Optional의 value가 null인 경우 매개변수로 받은 함수를 실행시킴

 

public T orElse(T other) {
	return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();
}

 

3.2 orElse와 orElseGet의 차이 예시 코드

    @Test
    public void testOrElse(){
        //given
        String name = "yonghwan";
        Optional<String> optionalStudent = Optional.ofNullable(name);
        //when
        String actual = optionalStudent.orElse(getDefaultName());
        //then
        System.out.println(actual);
        assertThat(actual).isEqualTo("yonghwan");
    }

    @Test
    public void testOrElseGet(){
        //given
        String name = "yonghwan";
        Optional<String> optionalStudent = Optional.ofNullable(name);
        //when
        String actual = optionalStudent.orElseGet(()->getDefaultName());
        //then
        System.out.println(actual);
        assertThat(actual).isEqualTo("yonghwan");
    }

    private String getDefaultName(){
        System.out.println("call getDefaultName()");
        return "anonymous";
    }

 

testOrElse 테스트 수행결과

call getDefaultName()
yonghwan

 

testOrElseGet 테스트 수행결과

yonghwan

 

위 두 테스트를 통해 알수 있는 점은 Optional.orElse() 메서드를 사용하는 경우 매개변수에 값을 반환하는 메서드를 호출하는 경우 Optional 인스턴스의 value가 null이든 아니든 무조건 호출한다는 점입니다. 반대로 Optional.orElseGet() 메서드를 사용하는 경우 매개변수에 함수형 인터페이스를 전달했기 때문에 Optional 인스턴스의 value가 null인 경우에만 getDefaultName() 메서드를 호출한다는 점입니다.

 

즉, 정리하면 orElse 메서드를 사용하는 경우 value가 null이든 아니든 상관없이 매개변수에 들어가는 로직이 무조건 수행됩니다. orElseGet 메서드를 사용하는 경우 value가 null인 경우에만 함수형 인터페이스가 실행됩니다.

 

3.3 orElse에 의한 발생 가능한 장애 예시

예를 들어 Student 클래스의 id 필드멤버는 데이터베이스에서 유니크한 값을 가져야 한다고 가정합니다. 그리고 StudentService가 다음과 같이 정의되어 있고 findById(1L)을 호출하였다고 가정합니다.

public class StudentService {
    ...
    
    // wrong
    public Student findById(Long studentId){
        return studentRepository.findById(studentId).orElse(createStudentWithId(studentId));
    }

    private Student createStudentWithId(Long studentId){
        Student student = new Student(studentId, "anonymous");
        return studentRepository.save(student);
    }
}
  • orElse를 호출하였기 때문에 조회 결과와 무관하게 createStudentWithId 메서드가 호출되어 데이터베이스에 저장하려고 하기 때문에 Id가 유니크로 설정되어 에러가 발생합니다.

 

따라서 조회한 결과 해당 학생이 없는 경우에만 createStudentWithId 메서드가 호출되어야 하기 때문에 orElse를 orElseGet으로 수정해야 합니다.

public class StudentService {
    ...

    // right
    public Student findById(Long studentId){
        return studentRepository.findById(studentId).orElseGet(()->createStudentWithId(studentId));
    }

    private Student createStudentWithId(Long studentId){
        Student student = new Student(studentId, "anonymous");
        return studentRepository.save(student);
    }
}

 

 

References

https://sorjfkrh5078.tistory.com/94
https://mangkyu.tistory.com/70