728x90
반응형
SMALL

자바8 이후에 또 아주 자주 사용되고 중요한 Optional에 대해 알아보자!

 

NullPointerException

자바 프로그래밍을 하다가 왜 이 NullPointerException이 종종 발생할까? 왜긴 왜야? null 체크를 깜빡 했으니까. 다음 코드를 보자.

 

Shop

public class Shop {
    private String name;
    private User host;
    private boolean isOpen;

    public Shop() {
    }

    public Shop(String name, boolean isOpen) {
        this.name = name;
        this.isOpen = isOpen;
    }

    public Shop(String name, User host, boolean isOpen) {
        this.name = name;
        this.host = host;
        this.isOpen = isOpen;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public User getHost() {
        return host;
    }

    public void setHost(User host) {
        this.host = host;
    }

    public boolean isOpen() {
        return isOpen;
    }

    public void setOpen(boolean open) {
        isOpen = open;
    }

    @Override
    public String toString() {
        return "Shop{" +
                "name='" + name + '\'' +
                ", isOpen=" + isOpen +
                '}';
    }
}

 

User

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • Shop 이라는 클래스에는 세가지 필드가 있다. `name`, `host`, `isOpen`.
  • 그리고 `host`는 타입이 레퍼런스 타입(User)이기 때문에 이 필드에 무언갈 채우지 않았다면 그 값은 null이다.

OptionalMain

public class OptionalMain {
    public static void main(String[] args) {
        Shop shop = new Shop();
        shop.setName("Shop A");
        shop.setOpen(true);

        System.out.println(shop.getHost().getName());
    }
}
  • 이 코드를 실행하면 어떻게 될까? 이 코드를 실행하면 NullPointerException이 발생한다.
  • 왜냐? getHost()null인데, `null.xxx`를 하는 순간 해당 에러가 터지는 것이다.

 

그럼 이런 NullPointerException을 방지하려면 어떻게 코드를 짜야하냐? 자바8 이전에는 이렇게 했다.

public class OptionalMain {
    public static void main(String[] args) {
        Shop shop = new Shop("A", true);

        if (shop.getHost() != null) {
            System.out.println(shop.getHost().getName());
        }
    }
}
  • 해당 값이 null인지, null이 아닌지 체크를 먼저 하고 그 이후에 뭔가를 진행하는 코드를 작성했다.
  • 이때 문제는? 이 코드를 작성하는 건 '사람'이다. 사람은? null 체크를 까먹을 확률이 매우 크다. 

또 다른 방법으로는 이런 방법도 있겠다.

public User getHost() {
    if (host != null) {
        return host;
    }
    throw new IllegalStateException("this field null");
}
  • 이건 더 큰 문제가 있다. 
  • 우선 이것도 역시 사람이 작성하는 것이라 null 체크를 안 할 가능성이 있다.
  • 위 코드처럼 null 체크를 했다고 치자. null인 경우, IllegalStateException을 던진다. 예외를 던지는 것은 생각보다 비싼값을 치뤄야 한다. 왜냐? 스택 트레이스를 찍어야 하니까.
  • 그리고 예외를 던지면, 사용하는 클라이언트 코드는 이번엔 null 에서는 해방될지언정, 예외 처리를 해줘야 한다.

 

Optional의 등장

자바8 이후로 이 Optional이 등장하면서, Optional을 리턴할 수 있게 됐다.

Optional은 뭔데? → 오직 한 개의 값이 들어있을수도 안 들어 있을수도 있는 컨테이너.

 

다음 코드를 보자.

public Optional<User> getHost() {
    return Optional.ofNullable(host);
}
  • 이번엔 getHost()Optional<User>를 리턴한다.
  • 그리고 실제 반환값으로는 Optional.ofNullable(host); 이다. 이름 그대로 null일수도 있다는 뜻이다.

그럼 이 코드를 사용하는 클라이언트 쪽은 어떻게 하냐? 이렇게 한다.

import java.util.Optional;

public class OptionalMain {
    public static void main(String[] args) {
        Shop shop = new Shop();
        shop.setName("Shop A");
        shop.setOpen(true);

        Optional<User> host = shop.getHost();
        host.ifPresent(System.out::println);
    }
}
  • 이건 단지 예시 코드일 뿐이다. 보통은 메서드 체인 형태로 사용하겠지만 그냥 설명을 위해 이렇게 작성했다.
  • 클라이언트 코드는 이 타입이 Optional이라는 것을 확인하는 순간부터 무엇을 생각하게 되냐면 null을 생각하게 된다. 즉, 명시적으로 "이 값은 빈 값일수도 있어! 그러니까 체크해야해!" 이 말을 해주는 단어가 된다.
  • 그래서 사용자는, 이 값이 있다면 어떤 처리를 하고, 없다면 어떤 처리를 할지 분기할 수가 있다.
shop.getHost().orElseThrow(RuntimeException::new);
shop.getHost().orElse(new User("hello", 20));
shop.getHost().ifPresent(System.out::println);
shop.getHost().ifPresentOrElse(System.out::println, () -> { /*null 인 경우*/ });
  • 이렇게 말이다. 이 값이 없는 경우에 대한 처리를 사용자로부터 강제하게 된다.
  • 그러다 보니 조금이라도 null safety한 코드가 작성될 수 있을 것이다.

그러니까, 명백하게 이 Optional을 제대로만 사용한다면 NullPointerException 으로부터 꽤나 자유로워 질 수 있을것만 같다.

Optional 주의점

그런데 Optional을 사용할 때 주의점이 있다. 일단, 다음 코드를 보자.

1. null일 수 있는 값을 Optional.of(...)로 사용하지 말 것

public Optional<User> getHost() {
    return Optional.of(host);
}
  • 이 코드 매우 위험하다.
  • Optional.of(...)는 전달하는 인자가 null이면 안된다.
  • of()를 사용하면 그 값이 null이 아닌 상태여야 한다. 만약, null이라면 이 자체로 NullPointerException이 발생한다.

반드시 null일 수도 있는 값은 Optional.ofNullable(...)을 사용하자.

public Optional<User> getHost() {
    return Optional.ofNullable(host);
}

 

2. 리턴값으로만 사용하기를 권장한다.

이게 무슨말일까? 다음 코드를 보자.

public Optional<User> getHost() {
    return Optional.ofNullable(host);
}
  • 이게 바로 리턴값으로 사용한 Optional이다.
  • 이렇게만 사용하라는 이야기다.

 

메서드 매개변수 타입, 맵의 키 타입, 인스턴스 필드 타입으로 쓰는 경우가 있는데 그런 경우를 최대한 최대한 최대한 지양해라!

아래에서 하나씩 그 이유를 알아보자.

 

2-1. 메서드 매개변수 타입을 Optional로 사용하지 말 것

public String isHostNameEqualsShopName(Optional<User> host) {
        ...
}
  • 지금 이 코드가 메서드의 매개변수로 Optional을 사용한 것이다.
  • 이거 왜 사용하면 안되냐? 다음 코드를 보자.
public String isHostNameEqualsShopName(Optional<User> host) {
    host.ifPresent(u -> {
        if (u.getName().equalsIgnoreCase(this.name)) {
            System.out.println("is equals!");
        }
    });
}
  • 지금 Optional로 받은 파라미터 덕분에 ifPresent()를 사용하는데, 이거 굉장히 위험한 코드다. 왜냐?
import java.util.Optional;

public class OptionalMain {
    public static void main(String[] args) {
        Shop shop = new Shop();
        shop.setName("Shop A");
        shop.setOpen(true);

        shop.isHostNameEqualsShopName(null);
    }
}
  • 이렇게 파라미터에 null을 넘기는 순간? host.ifPresent(...)에서 NullPointerException이 발생한다.
  • 그러니까 메서드 매개변수로 Optional 사용하는거 하지말자!

 

2-2. 맵의 키로 Optional을 사용하지 말 것

맵의 키로 Optional을 사용하는 건 정말 뭐랄까..? 맵이라는 자료 구조의 컨벤션을 망가뜨리는 행위이다.

Map을 통해 어떤 키값이 있는지 찾고 있으면 있는거고 없으면 명확히 없는거지, 있을수도 있고 없을수도 있다? ...음.. 하지말자!

사용하는 사람 입장에서 얜 뭘까.. 싶은 코드다 정말 이건.

 

 

2-3. 인스턴스 필드 타입으로 Optional을 사용하지 말 것

import java.util.Optional;

public class Shop {
    private String name;
    private Optional<User> host;
    private boolean isOpen;
}
  • 이런 경우를 말한다. 필드에 Optional을 적용하는 것.
  • 이것도 굉장히 나쁜 코드이다. 

`host`라는 필드는 원래도 null이 될 수도 그렇지 않을 수도 있다. 그렇기 때문에 getHost()와 같은 메서드를 이렇게 작성하는 것이다.

public Optional<User> getHost() {
    return Optional.ofNullable(host);
}

 

그런데, 왜 필드 자체에 이 값이 있을 수도 있고 없을 수도 있다고 더 더 모호하게 상황을 만드는가? 하지 말자!

 

 

3. Primitive 타입의 Optional은 따로 존재한다.

Optional.of(10);
  • 이렇게 Optionalprimitive 타입을 받을 순 있다. 근데, 곰곰히 생각해보자. primitive 타입에 null이란게 존재하는가? 아니다. 그런건 없다. 근데 primitive 타입을 Optional로 만든다? 이건 뭔가 매우 어색하기도 하면서 이걸 처리하기 위해 Boxing을 한다. 즉, 이 반환값은 이렇다.
Optional<Integer> i = Optional.of(10);
  • 그렇다. 타입이 Optional<Integer>로 변경된다. Primitive → Wrapper 클래스가 된다는 말이다. 이 과정에서 역시 리소스도 낭비되고 좋지 않다.

 

그래서, 이런게 있다.

OptionalInt optionalInt = OptionalInt.of(10);
  • OptionalInt가 있으면, OptionalDoubleOptionalLong도 있다.

 

4. Optional을 반환 타입으로 사용할 때 null을 리턴하지 마라.

진짜 정말 안 좋은 코드이다. 바로 보자.

public Optional<User> getHost() {
    return null;
}
  • 반환 타입은 Optional<User>이다. 리턴 타입으로 사용하고 있으니 위에서 말한 주의사항엔 걸리지 않는다.
  • 그런데 이 메서드의 리턴값이 null이면 어떤 문제가 발생하냐면, 
import java.util.Optional;

public class OptionalMain {
    public static void main(String[] args) {
        Shop shop = new Shop();

        Optional<User> host = shop.getHost();
        host.ifPresent(System.out::println);
    }
}
  • 이걸 사용하는 클라이언트 코드는, 이 메서드를 호출했을 때 반환값이 Optional이기 때문에 "어 Optional이네, 있는지 없는지 체크해야겠다."라고 생각하고 .ifPresent(...)를 호출하는 순간 NullPointerException이다.

그래서 이러면 안되고, 다음과 같이 해야 한다.

public Optional<User> getHost() {
    return Optional.empty();
}
  • Optional.empty()를 사용하면 된다.

 

5. Collection, Map, Optional 등 이미 이 자체로 빈 값이 될 수 있다고 표현하는 것들을 Optional로 또 감싸지 마라.

위에서 Map의 키를 Optional로 감싸지 마라. 라는 내용과 유사한데, 이미 이 자체로 빈 값인지 아닌지 판단할 수 있는 컨테이너들을 왜 Optional로 감싸나? 그럴 이유가 없는데. Mapget()을 통해 값이 있으면 가져오고 값이 없으면 null이다. 여기서 이미 판단을 할 수 있다. Collection, Optional도 마찬가지다. 이미 이 자체로도 빈 값인지 아닌지를 판단할수가 있는데 왜 Optional로 또 감싸는건가? 이러면 안된다.

 

 

 

정리

Optional을 제대로만 사용한다면, NullPointerException을 마주하는 경우를 굉장히 획기적으로 줄일수도 있고, 코드 자체의 가시성도 더 높일 수 있다. 개인적으로 좋아하는 방식이기도 하다. 그런데 이 Optional을 사용하는데 있어서 클라이언트 입장에서 매우 짜증나는 경우가 몇가지 있는데 그 경우를 여기에 정리했으니 이렇게 작성하는 것을 피하자! 

728x90
반응형
LIST

+ Recent posts