null 관련 문제 돌아보기
이 전 포스팅의 예제 코드를 통해 살펴본 null과 관련된 문제들은 크게 2가지로 요약된다.
- 런타임에 NPE(NullPointerException)라는 예외를 발생시킬 수 있음
- NPE 방어를 위해서 들어간 null 체크 로직 때문에 코드 가독성과 유지 보수성이 떨어짐
그냥 두자니 곳곳에 숨어서 일으켜 장애를 유발하고, 조치를 하자니 코드를 엉망으로 만드는 null, 어떻게 하면 조금 더 현명하게 다룰 수 있을까?
함수형 언어에서 그 해법을 찾다
스칼라나 하스켈과 같은 소위 함수형 언어들은 전혀 다른 방법으로 이 문제를 해결한다. 자바가 "존재하지 않는 값"을 표현하기 위해서 null을 사용했다면, 이 함수형 언어들은 "존재할지 안 할지 모르는 값"을 표현할 수 있는 별개의 타입을 가지고 있다. 그리고 이 타입은 이 존재할지 안 할지도 모르는 값을 제어할 수 있는 여러 가지 API를 제공하기 때문에 개발자들은 해당 API를 통해서 간접적으로 그 값에 접근하게 된다. Java 8에서는 이러한 함수형 언어의 접근 방식에서 영감을 받아 java.util.Optional <T>라는 새로운 클래스를 도입하였다.
Optional이란?
Optional는 "존재할 수도 있지만 안 할 수도 있는 객체", 즉, "null이 될 수도 있는 객체"를 감싸고 있는 일종의 Wrapper 클래스이다. 원소가 없거나 최대 하나밖에 없는 Collection이나 Stream으로 생각해도 좋다. 직접 다루기에 위험하고 까다로운 null을 담을 수 있는 특수한 그릇으로 생각하면 이해가 쉬울 것이다.
Optional의 효과
Optional로 객체를 감싸서 사용하게 되면...
- NPE를 유발할 수 있는 null을 직접 다루지 않아도 된다.
- 수고롭게 null 체크를 직접 하지 않아도 된다.
- 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현할 수 있다. (따라서 불필요한 방어 로직을 줄일 수 있다.)
Optional 기본 사용법
Optional 변수 선언하기
제네릭을 제공하기 때문에 변수를 선언할 때 명시한 타입 파라미터에 따라서 감쌀 수 있는 객체의 타입이 결정된다.
Optional<Order> maybeOrder; // Order 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
Optional<Member> optMember; // Member 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
Optional<Address> address; // Address 타입의 객체를 감쌀 수 있는 Optional 타입의 변수
변수명은 그냥 클래스 이름을 사용하기도 하지만 "maybe"나 "opt"와 같은 접두어를 붙여서 Optional 타입의 변수라는 것을 좀 더 명확히 나타내기도 한다.
Optional 객체 생성하기
Optional 클래스는 간편하게 객체 생성을 할 수 있도록 3가지 정적 팩토리 메소드를 제공한다.
- Optional.empty()
null을 담고 있는, 한 마디로 비어있는 Optional 객체를 얻어온다. 이 비어있는 객체는 Optioanl 내부적으로 미리 생성해 놓은 싱글턴 인스턴스이다.
Optional<Member> maybeMember = Optional.empty();
- Optional.of(value)
null이 아닌 객체를 담고 있는 Optional 객체를 생성한다. null이 넘어올 경우, NPE를 던지기 때문에 주의해서 사용해야 한다.
Optional<Member> maybeMember = Optional.of(aMember);
- Optional.ofNullable(value)
null인지 아닌지 확신할 수 없는 객체를 담고 있는 Optional 객체를 생성한다.
Optional.empty() 와 Optioanl.ofNullable(value)를 합쳐놓은 메소드라고 생각하면 된다. null이 넘어올 경우 NPE를 던지지 않고 Optional.empty()와 동일하게 비어있는 Optional 객체를 얻어온다. 해당 객체가 null인지 아닌지 자신이 없는 상황에서는 이 메소드를 사용해야 한다.
Optional<Member> maybeMember = Optional.ofNullable(aMember);
Optional<Member> maybeNotMember = Optional.ofNullable(null);
Optional이 담고 있는 객체 접근하기
Optional 클래스는 담고 있는 객체를 꺼내오기 위해서 다양한 인스턴스 메소드를 제공한다. 아래 메소드들은 모두 Optional이 담고 있는 객체가 존재할 경우 동일하게 해당 값을 반환한다. 반면에 Optional이 비어있는 경우(즉, null을 담고 있는 경우), 다르게 작동한다. 따라서 비어있는 Optional에 대해서 다르게 작동하는 부분을 설명한다.
- get()
- 비어있는 Optional 객체에 대해서, NoSuchElementException을 던진다.
- orElse(T other)
- 비어있는 Optional 객체에 대해서, 넘어온 인자를 반환한다.
- orElseGet(Supplier<? extends T> other
- 비어있는 Optional 객체에 대해서, 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.
orElse(T other) 의 게으른 버전이라고 생각하면 된다. 비어있는 경우에만 함수가 호출되기 때문에 orElse(T other) 대비 성능상 이점을 기대할 수 있다.
- 비어있는 Optional 객체에 대해서, 넘어온 함수형 인자를 통해 생성된 객체를 반환한다.
- orElseThrow(Supplier<? extends X> exceptionSupplier)
- 비어있는 Optional 객체에 대해서, 넘어온 함수형 인자를 통해 생성된 예외를 던진다.
지금까지 Optional에서 제공하는 주요 메소드들에 대해서 퀵하게 알아보았다. 이제부터 이 메소드들을 어떻게 활용하는지에 대해서 이야기하도록 하겠다.
Optional의 잘못된 사용
위에서 설명한 것 처럼 get() 메소드는 비어있는 Optional 객체를 대상으로 호출할 경우, 예외를 발생시키므로 다음과 같이 객체 존재 여부를 bool 타입으로 반환하는 isPresent()라는 메소드를 통해 null 체크가 필요하다.
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 = maybeText.get().length();
} else {
length = 0;
}
위 코드를 보고 이렇게 말씀하시는 분들이 많을 거다. "이럴 거면 뭐 하러 Optional을 사용하는 걸까요? Optional을 사용해서 도대체 뭐가 좋아진 거죠? 사실 이렇게 코딩하실 거라면 차라리 Optional을 사용하지 않는 편이 나을 것 같습니다."
안타깝게도 Optional 관련해서 개발자들이 제일 많이 하는 질문 중 하나가 "Optional 적용 후 어떻게 null 체크를 해야 하나요?"이다. 사실 이 질문에 대한 답변은 "null 체크를 하실 필요가 없으시니 하시면 안 됩니다."이다.
우리가 Optional을 사용하려는 이유는 앞서 설명한 것처럼 고통스러운 null 처리를 직접 하지 않고 Optional 클래스에 위임하기 위함이다.
따라서 위와 같은 방식으로 Optional을 사용하게 되면 Java8 이 전에 직접 null 체크를 하던 코딩 수준에서 크게 벗어나지 못하게 된다.
다른 잘못된 예제로 이 전 포스팅에서 보았던 getCityOfMemberFromOrder() 메소드를 같은 스타일로 작성하면 다음과 같다. 이 전 포스팅에서 보았던 코드와 별반 다르지 않은 수준의, 사실 오히려 살짝 더 복잡해 보이는 끔찍한 코드가 탄생하였다. 무엇이 잘못된 걸까?
/* 주문을 한 회원이 살고 있는 도시를 반환한다 */
public String getCityOfMemberFromOrder(Order order) {
Optional<Order> maybeOrder = Optional.ofNullable(order);
if (maybeOrder.isPresent()) {
Optional<Member> maybeMember = Optional.ofNullable(maybeOrder.get());
if (maybeMember.isPresent()) {
Optional<Address> maybeAddress = Optional.ofNullable(maybeMember.get());
if (maybeAddress.isPresent()) {
Address address = maybeAddress.get();
Optinal<String> maybeCity = Optional.ofNullable(address.getCity());
if (maybeCity.isPresent()) {
return maybeCity.get();
}
}
}
}
return "Seoul";
}
Optional을 정확히 이해하고 제대로 사용하실 수 있는 개발자라면 첫 번째 예제의 코드는 다음과 같이 한 줄의 코드로 작성할 수 있어야 한다.
다시 말해서, 기존에 조건문으로 null을 대하던 생각을 함수형 사고로 완전히 새롭게 바꿔야 한다.
int length = Optional.ofNullable(getText()).map(String::length).orElse(0);
'Study > WEB' 카테고리의 다른 글
[nGrinder] nGrinder Script 사용법 (0) | 2023.02.23 |
---|---|
[WEB] 프록시(Proxy)란? (0) | 2023.02.12 |
[Java] 빠져나올 수 없는 null 처리의 늪 - 1 (2) | 2023.02.06 |
[JAVA] 예외 던지기(throw) & 예외 연결(Chained Exception) (0) | 2023.01.29 |
[WEB] HTTP Method (0) | 2023.01.26 |