[Java] 옵셔널

5 분 소요

옵셔널

옵셔널은 자바의 NullPointerException을 방지하기 위해 만들어진 기능이다.
프로그래밍에서 가장 많이 발생하는 오류가 이 NullPointerException인데, 보통 프로그래밍을 할 때
변수에 일일히 Null값이 들어올 상황에 대비해주는 코드를 넣어주는 것은 매우 복잡하고 번거로운 일이기 때문에,
입력을 받는 부분에서는 항상 Null에 대한 유효성을 체크해주어야 한다.

따라서 이러한 유효성을 검사하기 위해서 일일히 if문을 넣어 Null값이 들어왔을 때의 예외를 선언해주어야 했던 번거로움을 해결하기 위해,
자바8에서부터는 Optional이란 기능이 추가되었다.

옵셔널로 변수를 선언하는 방법은 아래와 같이 Optional 자료형으로 변수를 선언해주면 되는데,

1
2
3
Optional<String> optional = Optional.empty();
Optional<String> optionalOf = Optional.of("str"); 
Optional<String> optionalOfNull = Optional.ofNullable(null);

첫번째 줄은 빈 옵셔널 타입을 선언해준 것이고, 두번째 줄은 str이라는 문자열이 들어있는 옵셔널타입의 변수를 선언 해주었다.
하지만 두번째 줄같은 경우 Null값이 of메서드 안에 들어오면 에러를 출력하므로, 만약 Null값이 들어올 수 있는 값이라면 세번째 줄 처럼 ofNullable을 사용해주어야 한다.

이렇게 옵셔널로 선언한 값을 런해서 출력해보면 다음과 같이 뜨는걸 볼 수 있는데,

1
2
3
4
5
6
public static void main(String[] args) {
    Optional<String> optionalOf = Optional.of("hello");
    System.out.println(optionalOf);
}
    
> Optional[hello]

이렇게 Optional[]이란 괄호안에 값이 들어있는걸 볼 수 있다.

이것을 래핑되었다고 하는데, 비유하자면 물건이 박스에 들어가 있는 상태라고 생각하면 된다.
박스에 들어가 있기 때문에, 아직 Null값임이 확정되지 않아 NullException 에러를 출력하지 않는 것이고, 이 값을 꺼낼 때 Null이라면 그때서야 에러를 출력하게 되는 것이다.

따라서 우리가 물건을 사용하려면 박스에서 꺼내야 하듯이, 이 값을 사용하려면 언래핑 이라는 과정을 거쳐서 상자에서 이 값을 꺼내주어야 한다.

1
2
3
4
5
public static void main(String[] args) {
    Optional<String> optionalOf = Optional.of(null);
    
    System.out.println(optionalOf);
}

위 코드를 돌리게 되면 NullPointException이 발생하게 되는데, 이는 Optional.of는 Null값을 입력하면 오류를 출력하기 때문이다.
따라서 Optional에 Null값을 넣고 싶다면, empty() 메서드를 사용하거나, ofNullable()을 사용해서 값을 할당해야 한다.

그렇다면 이 상자에서 어떻게 해야 이 값을 꺼낼 수 있을까??

상자에서 꺼내는 방법은 두가지가 있는데, get()을 사용하는 방법과 orElse() 혹은 orElseGet()를 사용하는 방법이 있다.

먼저 get()의 경우 강제로 박스를 여는것에 가까운데, get()은 Null값에 대한 대비 없이 바로 상자를 열어버리기 때문에 래핑된 값이 Null이라면 에러를 출력한다.

orElse()의 경우는 orElse() 파라미터 단에 해당 옵셔널 변수가 Null일때의 예외 값을 지정해줌으로써, 만약 박스를 열었는데 안에 Null값이 있다면 해당 예외값이 들어있던 것으로 판단한다.

orElseGet()은 기본적으로 orElse와 같지만, 파라미터단에 값자체가 아닌 함수가 들어오며 null이던 아니건 우선 예외값을 불러오는
orElse()와 달리 안에 있는 값이 Null일 때만 해당 예외처리를 불러온다.

따라서 특이한 상황이 아니라면 Null값이 들어올 수 있는 옵셔널 변수에는 orElseGet()을 사용해주는게 가장 NullPointerException에서 가장 안전하다.

이러한 특성 때문에 만약 NullPointerException을 막으려면 아래와 같이 복잡한 코드를 선언해야했던 이전과 달리,

1
2
3
4
5
6
public static void main(String[] args) {
    String number = null;
    if (number != null){
        System.out.println(number);
    }
}

다음과 같이 Optional을 사용해서 코드를 더 간소화 할 수 있게 되었다.

1
2
3
4
5
public static void main(String[] args) {
    String number = null;
    Optional<String> opt = Optional.ofNullable(number);
    System.out.println(opt.orElseGet(() -> "null"));
}

이렇게 옵셔널은 더 안정적이고, 간소화된 코드를 위해 사용되는 유용한 기능이다.

그럼, 이 기능을 더 자연스럽게 사용하기 위해 몇가지 예제를 풀어보자.

###요구사항 1 - Optional을 활용해 조건에 따른 반환
nextstep.optional.User의 ageIsInRange1() 메소드는 30살 이상, 45살 이하에 해당하는 User가 존재하는 경우 true를 반환하는 메소드이다.

1
2
3
4
5
6
7
8
9
10
public static boolean ageIsInRange1(User user) {
    boolean isInRange = false;

    if (user != null && user.getAge() != null
            && (user.getAge() >= 30
            && user.getAge() <= 45)) {
        isInRange = true;
    }
    return isInRange;
}

같은 기능을 Optional을 활용해 ageIsInRange2() 메소드에 구현한다. 메소드 인자로 받은 User를 Optional로 생성하면 stream의 map, filter와 같은 메소드를 사용하는 것이 가능하다.

nextstep.optional.UserTest의 테스트가 모두 pass해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserTest {
    @Test
    public void whenFiltersWithoutOptional_thenCorrect() {
        assertThat(ageIsInRange1(new User("crong", 35))).isTrue();
        assertThat(ageIsInRange1(new User("crong", 48))).isFalse();
        assertThat(ageIsInRange1(new User("crong", null))).isFalse();
        assertThat(ageIsInRange1(new User("crong", 29))).isFalse();
        assertThat(ageIsInRange1(null)).isFalse();
    }

    @Test
    public void whenFiltersWithOptional_thenCorrect() {
        assertThat(ageIsInRange2(new User("crong", 35))).isTrue();
        assertThat(ageIsInRange2(new User("crong", 48))).isFalse();
        assertThat(ageIsInRange2(new User("crong", null))).isFalse();
        assertThat(ageIsInRange2(new User("crong", 29))).isFalse();
        assertThat(ageIsInRange2(null)).isFalse();
    }
}
  • Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
  • Optional.ofNullable(user)을 활용해 User을 Optional로 생성하는 것이 가능하다.
  • Optional의 map(), filter() 메소드등을 활용해 필요한 데이터를 추출
  • Optional의 isPresent() 메소드 활용
1
2
3
4
public static boolean ageIsInRange2(User user) {
    Optional<User> targetUser = Optional.ofNullable(user);
    return targetUser.map(User :: getAge).filter(age -> age >= 35).filter(age -> age < 45).isPresent();
}

user를 Optional로 감싸주고, ofNullable로 선언함으로써 Null값이 들어올 수 있도록 선언해준다.
user의 getAge를 비교해주어야 하므로, map을 사용해서 들어온 user에서 age를 얻어오고, 이를 조건에 맞게 filter()를 사용해 걸러준다.
그리고 isPresent()를 통해 값이 존재하면 true, 존재하지않으며 false를 반환하면 된다.

###요구사항 2 - Optional에서 값을 반환
nextstep.optional.Users의 getUser() 메소드를 자바 8의 stream과 Optional을 활용해 구현한다.

1
2
3
4
5
6
7
8
 User getUser(String name) {
        for (User user : users) {
            if (user.matchName(name)) {
                return user;
            }
        }
        return DEFAULT_USER;
    }

자바 8의 stream과 Optional을 사용하도록 리팩토링한 후 UsersTest의 단위 테스트가 통과해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UsersTest {

    @Test
    public void getUser() {
        Users users = new Users();
        assertThat(users.getUser("crong")).isEqualTo(new User("crong", 35));
    }


    @Test
    public void getDefaultUser() {
        Users users = new Users();
        assertThat(users.getUser("codesquard")).isEqualTo(Users.DEFAULT_USER);
    }
}
  • Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
  • Optional의 orElse() 메소드 활용해 구현한다.
1
2
3
User getUser(String name) {
    return users.stream().filter(user -> user.matchName(name)).findFirst().orElse(DEFAULT_USER);
}

users는 User 자료형을 갖는 리스트이므로, 먼저 filter를 통해 user.matchName이 true인 값만을 필터링 해주고,
findFirts()를 통해 해당 값을 찾아준다. 만약 Null값이라면 orElse를 사용해서 예외에 대한 디폴트값을 설정해준다.

요구사항 3 - Optional에서 exception 처리

nextstep.optional.ExpressionTest의 테스트가 통과하도록 Expression의 of 메소드를 구현한다.

1
2
3
4
5
6
7
8
9
static Expression of(String expression) {
    for (Expression v : values()) {
        if (matchExpression(v, expression)) {
            return v;
        }
    }

    throw new IllegalArgumentException(String.format("%s는 사칙연산에 해당하지 않는 표현식입니다.", expression));
}

단, of 메소드를 구현할 때 자바 8의 stream을 기반으로 구현한다.

  • Guide To Java 8 Optional 문서를 참고해 Optional 사용 방법을 익힌다.
  • 자바의 enum 전체 값은 values() 메소드를 통해 배열로 접근 가능하다.
  • Arrays.stream()을 이용해 배열을 stream으로 생성할 수 있다.
  • 일치하는 값 하나를 추출할 때 findFirst() 메소드를 활용 가능하다.
  • Optional의 orElseThrow() 메소드 활용해 구현한다.
1
2
3
4
5
6
static Expression of2(String expression) {
    return Arrays.stream(values())
            .filter(value -> matchExpression(value, expression))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException(String.format("%s는 사칙연산에 해당하지 않는 표현식입니다.", expression)));
}

Expression의 value를 받아 filter해준 뒤, orElseThrow를 사용해 예외 발생시 IllegalArgumentException를 출력하도록 해준다.

카테고리:

업데이트:

댓글남기기