자바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);
- 이렇게 Optional로 primitive 타입을 받을 순 있다. 근데, 곰곰히 생각해보자. primitive 타입에 null이란게 존재하는가? 아니다. 그런건 없다. 근데 primitive 타입을 Optional로 만든다? 이건 뭔가 매우 어색하기도 하면서 이걸 처리하기 위해 Boxing을 한다. 즉, 이 반환값은 이렇다.
Optional<Integer> i = Optional.of(10);
- 그렇다. 타입이 Optional<Integer>로 변경된다. Primitive → Wrapper 클래스가 된다는 말이다. 이 과정에서 역시 리소스도 낭비되고 좋지 않다.
그래서, 이런게 있다.
OptionalInt optionalInt = OptionalInt.of(10);
- OptionalInt가 있으면, OptionalDouble도 OptionalLong도 있다.
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로 감싸나? 그럴 이유가 없는데. Map은 get()을 통해 값이 있으면 가져오고 값이 없으면 null이다. 여기서 이미 판단을 할 수 있다. Collection, Optional도 마찬가지다. 이미 이 자체로도 빈 값인지 아닌지를 판단할수가 있는데 왜 Optional로 또 감싸는건가? 이러면 안된다.
정리
Optional을 제대로만 사용한다면, NullPointerException을 마주하는 경우를 굉장히 획기적으로 줄일수도 있고, 코드 자체의 가시성도 더 높일 수 있다. 개인적으로 좋아하는 방식이기도 하다. 그런데 이 Optional을 사용하는데 있어서 클라이언트 입장에서 매우 짜증나는 경우가 몇가지 있는데 그 경우를 여기에 정리했으니 이렇게 작성하는 것을 피하자!
'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글
Lombok은 어떻게 동작하는걸까? (0) | 2024.12.01 |
---|---|
[Java 8] CompletableFuture (0) | 2024.11.30 |
[Java 8] Stream API (0) | 2024.11.27 |
[Java 8] 함수형 인터페이스와 람다 표현식 (0) | 2024.11.25 |
애노테이션 (6) | 2024.10.21 |