[Java] Optional를 잘 사용하는 법 1 (NPE, if문으로 null 체크를 하지 말자)
Optional을 왜 사용하냐, 그럼 언제 사용해야할까, 에 대한 질문과 답을 적으려고 한다.
요점
- NPE을 방지하기 위해 사용하는 클래스.
- null이 들어올 수 있는 값을 한 번 감싸는 Wrapper 클래스.
NPE (Null Pointer Exception)
null 참조로 인해 널 포인터 예외(NPE)가 발생하는 것이 가장 문제이다. 컴파일 시점에서는 문제가 없지만 런타임때 NPE가 발생한다면 가장 골치아프다.
java.lang.NullPointerException
at seo.dale.java.practice(OptionalTest.java:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
NULL 처리가 취약한 코드
null 처리가 취약한 코드에서 NPE 발생 확률이 높다.
"어떤 주문을 한 회원이 어느 도시에 살고 있는지 알아내기"에 관한 메서드가 다음과 같다고 하자.
// 주문을 한 회원이 살고있는 도시를 반환한다.
public String getCityOfMemberFromOrder(Order order) {
return order.getMember().getAddress().getCity();
}
위 메서드는 NPE 위험에 노출된 상태이다.
1) order 파라미터에 null 일 경우
2) order.getMember() 의 결과가 null 일 경우
3) order.getMember().getAddress() 의 결과가 null 일 경우
적절히 null 처리를 하지 않으면, NPE가 발생할 수 있다.
JAVA8 이전의 NPE 방어 패턴
NPE의 위험에 노출된 코드를 다음과 같은 코딩 스타일로 회피하였다.
public String getCityOfMemberFromOrder(Order order) {
if (order != null) {
Member member = order.getMember();
if (member != null) {
Address address = member.getAddress();
if (address != null) {
String city = address.getCity();
if (city != null) {
return city;
}
}
}
}
return "Seoul"; // default
}
객체 탐색의 모든 단계마다 null 체크를 하고 있다. 들여쓰기 때문에 코드를 한눈에 읽을 수 없어 핵심 비즈니스 파악이 어렵다.
public String getCityOfMemberFromOrder(Order order) {
if (order == null) {
return "Seoul";
}
Member member = order.getMember();
if (member == null) {
return "Seoul";
}
Address address = member.getAddress();
if (address == null) {
return "Seoul";
}
String city = address.getCity();
if (city == null) {
return "Seoul";
}
return city;
}
결과값을 여러 곳에서 리턴하기 때문에 유지보수의 어려움이 생길 수 있다.
2가지 방법 모두 기본적으로 객체의 필드나 메서드에 접근하기 전에 null 체크를 하면서 NPE 를 방지하고 있다.
결국 비즈니스 로직의 파악이 어려워지고 코드 가독성과 유지보수성을 희생해야하는 코드가 나오게 되었다.
Java8 이전 문제
1. 런타임에 NPE가 발생할 수 있다.
2. NPE 방어를 위해 들어간 null 체크 로직 때문에 코드 가독성과 유지 보수성이 떨어진다.
함수형 언어에서 해법 찾기
스칼라같은 소위 함수형 언어들은 전혀 다른 방법으로 이 문제를 해결하고 있다. 자바가 "존재하지 않는 값"을 표현하기 위해서 null을 사용하였다면, 함수형 언어들은 _"존재할지 안할지 모르는 값"_을 표현할 수 있는 별개의 타입이 있다. 이 타입이 존재할지 안할지 모르는 값을 제어할 수 있는 여러가지 API를 제공하기 때문에 해당 API를 통해서 간접적으로 값에 접근할 수 있다. Java8은 java.util.Optional 라는 새로운 클래스를 도입하였다.
Optional 이란?
존재할 수도 있지만 안 할 수도 있는 객체, 즉 null이 될 수도 있는 객체를 감싸고 있는 일종의 래퍼 클래스이다.
Optional을 쓰면
장점
1. 객체가 null이어도 NPE가 발생하지 않고 비즈니스 로직대로 흘러갈 수 있다.
2. 비즈니스 코드와 null 방어 코드가 뒤섞여 있지 않아 로직을 더 쉽게 파악할 수 있다.
3. 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있어 불필요한 방어 로직을 줄일 수 있다.
단점
1. null 체크 및 Wrapping 시 오버헤드가 발생하여 성능이 저하될 수있다. (null이 절대 나오지 않는 경우 Optional을 사용하지 않는 것이 더 성능에 더 좋다.)
Optional 기본 사용법
Optional 변수 사용하기
제네릭을 제공하기 때문에 변수를 선언할 때 명기한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정된다.
변수명은 maybe나 opt같은 접두어를 부어 Optional 타입의 변수라는 것을 좀 더 명확히 나타내기도 한다.
Optional<Order> maybeOrder; // Order 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
Optional 객체 생성하기
Optional 클래스는 간편하게 객체 생성을 할 수 있도록 3가지 정적 팩토리 메서드를 제공한다. (empty, of, ofNullable)
- Optional.empty()
null을 담고 있는(비어있는) Optional 객체를 가져온다. 이 비어있는 객체는 Optional 내부적으로 미리 생성해놓은 싱글턴 인스턴스이다.
Optional<Member> maybeMember = Optional.empty();
- Optional.of(value)
null이 아닌 객체를 담고있는 Optional 객체를 생성한다. null이 들어올 경우 NPE를 던지기 때문에 주의해야 한다.
Optional<Member> maybeMember = Optional.of(aMember);
- Optional.ofNullable(value)
null이 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다. null이 넘어올 경우 NPE를 던지지 않고 Optional.empty()와 동일하게 비어있는 Optional 객체를 가져온다. 해당 객체가 null인지 아닌지 자신없는 상황에서 이 메서드를 사용해야 한다.
Optional<Member> maybeMember = Optional.ofNullable(aMember);
Optional<Member> maybeNotMember = Optional.ofNullable(null);
Optional이 담고있는 객체 접근하기
Optional 클래스는 담고있는 객체를 꺼내기 위해 다양한 인스턴스 메서드를 제공한다. 아래 메서드는 모두 Optional이 담고있는 객체가 존재할 경우 동일하게 해당 값을 반환한다. 반면에 비어있는 경우 (즉, null일 경우) 메서드마다 다르게 동작한다.
- get()
비어있는 Optional 객체에 대해서, NoSuchElementException을 던진다.
- orElse(T other)
비어있는 Optional 객체에 대해서, 넘어온 인자를 반환한다.
- orElseGet(Supplier<? extends T> other)
비어있는 Optional 객체에 대해서, 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.
- orElseThrow(Supplier<? extends X> exceptionSupplier)
비어있는 Optional 객체에 대해서, 넘어온 함수형 인자를 통해 생성된 예외를 던진다.
Optional을 잘못 사용할 경우
get() 메서드는 비어있는 Optional 객체를 대상으로 호출할 경우, 예외를 발생시키기 때문에 객체 존재여부를 boolean으로 반환하는 isPresent()라는 메서드를 통해 null 체크가 필요하다.
// Optional을 사용한 경우
String text = getText();
Optional<String> maybeText = Optional.ofNullable(text);
int length;
if (maybeText.isPresent()) {
length = maybeText.get().length();
} else {
length = 0;
}
// Optional을 사용하지 않은 경우
String text = getText();
int length;
if (text == null) {
length = text.length();
} else {
length = 0;
}
이렇게 사용할 경우 Optional을 사용해서 좋아진 부분이 없다.
Optional 적용 후에는 null 체크를 할 필요가 없다. (null 체크를 하지 않고 Optional에 위임하기 위해 Optional을 사용한다.)
Optional을 정확히 이해했다면 다음과 같이 한줄로 코드를 작성할 수 있어야 한다.
int length = Optional.ofNullable(getText()).map(String::length).orElse(0);
출처
www.daleseo.com](https://www.daleseo.com/java8-optional-after/)
https://www.daleseo.com/java8-optional-after/
https://maivve.tistory.com/332
'programming language > Java' 카테고리의 다른 글
[Java] Optional를 잘 사용하는 법 2 (어떻게 사용할까) (2) | 2023.02.03 |
---|---|
[Java] Enum을 활용적으로 쓰는 법 (0) | 2022.11.16 |
[Java] setter를 지양하라 > HOW? (0) | 2022.10.28 |
[Java] Meta Annotation @Target, @Retention (0) | 2022.07.13 |
[Java] ModelMapper 라이브러리 (0) | 2022.06.02 |
댓글 개