예기치 않은 NullPointException을 피하기 위해 필요한 곳에 다양한 null 확인 코드를 추가해서 null 예외 문제를 해결해야 한다.
- null이 들어오지 않는 영역을 제외하고는 변수를 참조할 때마다 null을 확인해야 한다.
public String getCarInsuranceNameNullSafe(Person person) {
String unknown = "Unknown";
if (person == null) {
return unknown;
}
CarV1 car = person.getCar();
if (car == null) {
return unknown;
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return unknown;
}
return insurance.getName();
}
- if문으로 인한 4개의 출구로 인해 유지보수가 어려워 진다.
- 중첩 if문을 사용할 경우는 코드 들여쓰기 수준⬆️ (가독성⬇️, 구조⬇️)
null 때문에 발생하는 문제
- 에러의 근원: NullPointException은 자바에서 가장 흔한 에러
- 코드를 어지럽힌다: 때로는 중첩된 null 확인 코드를 추가해야 하므로 가독성⬇️
- 아무 의미가 없다: null은 아무 의미도 표현하지 않기 때문에 정적 형식 언어에서 값이 없음을 표현하는 방법으로는 적절하지 않다.
- 자바 철학에 위배된다: 자바는 개발자로부터 모든 포인터를 숨겼지만 null 포인터는 예외
- 형식 시스템에 구멍을 만든다: 모든 참조 형식에 null을 할당할 수 있지만, 할당되기 시작해 시스템의 다른 부분으로 null이 퍼졌을 때 에초에 null이 어떤 의미로 사용되었는지 알 수 없다.
최근 그루비 같은 언어에서는 안전 내비게이션 연산자(safe navigation operator (?.))를 도입해 null 문제를 해결했다.
def carInsuranceName = person?.car?.insurance?.name
- null 값이 있거나 없음을 표현하며 참조 예외 걱정 없이 객체에 접근 가능하다.
- 호출 체인에 null인 참조가 있으면 결과로 null이 반환된다.
하스켈, 스칼라 등의 함수형 언어는 선택형 값(optional value)을 저장할 수 있는 Maybe라는 형식을 제공한다.
- optional 형식에서 제공하는 값이 있는지 여부를 명시적으로 확인해야 한다. (형식 시스템에서 이를 강제함 -> null 문제 가능성⬇️)
- 자바는 T 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 '선택형 값' 개념의 영향을 받아 java.uitlOptional<T> Class를 제공한다.
'참조'라는게 중요한 포인트인거 같다. 메모리에서 값을 복사하는 primitive type 에서 null값을 못 사용하고 defualt value가 있는 이유를 알것같다.
Optional 클래스 소개
Optional은 선택형값을 캡슐화하는 클래스로 값이 있으면 Optional 클래스로 값을 감싸고 없으면 Optional.empty 메서드로 변환한다.
- null을 참조하려 하면 NullPointException이 발생하지만 Optional.empty()는 Optional 객체이므로 다양한 방식으로 활용 가능하다.
null 대신 Optional을 사용하면 T 형식이 Optional<T>로 바뀌며 이는 값이 없을 수 있음을 명시적으로 보여준다.
public class Car {
private Optional<Insurance> insurance; //자동차가 보험에 가입되어 있을수도 아닐 수도 있으므로 Optional
public Optional<Insurance> getInsurance() {
return insurance;
}
}
- Optional이 없다면 null 참조가 할당 되었을 때 이것은 올바른 값인지 아니면 잘못된 값인지 판단할 아무 정보도 없다.
public class Insurance {
private String name;
public String getName() {
return name;
}
}
- Optional<String>이 아닌 이유는 "보험회사는 이름을 반드시 가져야 한다."라고 말하는 것과 같다.
- 이름이 없는 보험회사(null)가 발견되면 예외처리하는 코드를 추가하는 것이 아니라 이름이 없는 이유를 해결해야 한다.
모든 null 참조를 Optional로 대치하는 것은 바람직하지 않다.
- Optional은 이해하기 쉬운 API를 설계하도록 돕는다.
- 메서드의 시그니처만 보고도 선택형값인지 여부를 구별할 수 있다.
- Optional의 등장으로 이를 언랩해서 값이 없을 수 있는 상황에 적절하게 대응하도록 강제하는 효과가 있다.
Optional 적용 패턴
Optional 객체 만들기
//빈 Optional
Optional.empty();
//null이 아닌 값으로 Optional 만들기
Optional.of(entity);
null값으로 Optional 만들기
public static <T> Optional<T> ofNullable(T value) {
return value == null ? (Optional<T>) EMPTY
: new Optional<>(value);
}
Optional.ofNullable(entity);
- entity값이 null이면 빈 Optional 객체가 반환된다.
맵으로 Optional의 값을 추출하고 변환하기
String name = null;
if (insurance != null) {
name = insurance.getName();
}
이런 유형의 패턴에 사용할 수 있도록 Optional map 메서드 지원
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
- optional이 비어있으면 아무 일도 일어나지 않는다.
- 값을 포함하면 map의 인수로 제공된 함수가 바꾼다.
flatMap으로 Optional 객체 연결
여러 메서드를 안전하게 호출하면서 어떻게 활용할 수 있을까?
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
map으로만 사용하면 중첩으로 Optional 객체 구조가 되기 때문에 스트림의 flatmap과 유사하게 평준화할 수 있다.
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName) //String을 반환하기 때문에 map
.orElse("Unknown"); //결과 Optional이 비어있으면 기본값 사용
}
- Optional을 활용한 참조 체인 중 어떤 메서드가 빈 Optional을 반환한다면 전체 결과로 빈 Optional을 반환하고 아니면 이름을 포함하는 Optional이 반환된다.
- orElse를 사용하면 Optional이 비어있을 때 기본값을 제공한다.
Optional을 인수로 받거나 Optional을 반환하는 메서드를 정의한다면 이 메서드를 사용하는 모든 사람에게 이 메서드가 빈 값을 받거나 빈결과를 반환할 수 있음을 잘 문서화해서 제공하는 것과 같다.
도메인 모델에 Optional을 사용했을 때 데이터를 직렬화할 수 없는 이유
Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않는다.
객체 그래프에서 일부 또는 전체 객체가 null일 수 있는 상황에서 직렬화 모델이 필요할 때
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
- Optional로 값을 반환받을 수 있는 메서드를 추가하는 방식을 권장한다.
Optional 스트림 조작
Optional.stream: 값이 존재하면 존재하는 값만 포함하는 스트림을 반환하고, 값이 없으면 빈 스트림을 반환.
public Set<String> getCarInsuranceNames(List<Person> persons) {
return persons.stream()
.map(Person::getCar) //Optional<Car> 스트림으로 변환
.map(optCar -> optCar.flatMap(Car::getInsurance)) //반환값이 Optional이라서 flatMap
.map(optInsurance -> optInsurance.map(Insurance::getName))
.flatMap(Optional::stream) //Stream<Optional<String>>을 Stream<String>으로 flatMap이용해서 변환
.collect(toSet()); 결과 문자열을 중복되지 않은 값을 갖도록 집합으로 수집
}
- Optional 덕분에 null 걱정없이 안전하게 처리할 수 있지만 과정도 복잡하고 마지막 결과를 얻기위해 빈 Optional을 제거하고 값을 언랩해야 한다는 문제가 있다.
Stream<Optional<String>> stream = persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optInsurance -> optInsurance.map(Insurance::getName));
return stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet());
- filter, map을 순서적으로 이용해 비어있는 Optional을 건너뛰고 언랩할 수 있다.
디폴트 액션과 Optional 언랩
get() - 값을 읽는 가장 간단한 메서드면서 동시에 가장 안전하지 않은 메서드다.
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
- Optional에 값이 반드시 있다고 가정할 수 있는 상황이 아니라면 사용하지 않는것이 좋고, 중첩된 null 확인 코드를 넣는 상황과 다르지 않다.
orElse(T other) - Optional이 값을 포함하지 않을 때 기본값을 제공할 수 있다.
public T orElse(T other) {
return value != null ? value : other;
}
orElseGet(Supplier<? extends T> supplier) - orElse 메서드에 대응하는 게으른 버전
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
- default method를 만드는데 시간이 걸리거나(효율성 측면) Optional이 비어있을 때만 기본값을 생성하고 싶을 때
orElseThrow(Supplier<? extends X> exceptionSupplier)
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
public T orElseThrow() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
- 매개변수가 없는 orElseThrow()는 get() 메서드와 같지만, 매개변수가 있는 메서드는 발생시킬 예외의 종류를 선택할 수 있다.
ifPresent(Consumer<? super T> action)
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
action.accept(value);
}
}
- 값이 존재할 때 인수로 넘겨준 동작을 수행하고 값이 없으면 아무 일도 일어나지 않는다.
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) - java 9에서 추가
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
if (value != null) {
action.accept(value);
} else {
emptyAction.run();
}
}
- Optional이 비었을 때 실행할 수 있는 Runnable을 인수로 받는다는 점만 ifPresent와 다르다.
두 Optional 합치기
가장 저렴한 보험료를 제공하는 보험회사를 찾는 몇몇 복잡한 비즈니스 로직을 구현한 외부 서비스가 있다고 가정할 때
public Insurance findCheapestInsurance(Person person, Car car) {
// 다른 보험사에서 제공한 질의 서비스
// 모든 데이터 비교
Insurance cheapestCompany = new Insurance();
return cheapestCompany;
}
Optional이 값을 포함하는지 여부를 알려주는 isPresent 메서드 제공
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
if (person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
- person과 car의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적으로 보여주지만, null 확인코드와 다를게 없다.
Optional 언랩하지 않고 두 Optional 합치기
- 조건문 사용하지 않고 한 줄의 코드로 재구현할 수 있다
public Optional<Insurance> nullSafeFindCheapestInsuranceQuiz(Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
- flatMap으로 인해 첫 번째 Optional이 비어있으면 빈 Optional을 반환하고 값이 있다면 Optional<Insurance>를 반환하는 Function의 입력으로 person을 사용한다.
- 두번 째 Optional에 map을 호출하므로 car가 포함되지 않으면 빈 Optional을 반환해 최종적으로 빈 Optional을 반환하고 존재하면 map 메서드로 전달한 람다 표현식이 findCheapestInsurance 메서드를 안전하게 호출할 수 있다.
필터로 특정값 거르기
특정 값이 'some'인지 확인해야 한다고 가정할 때
if(value != null && 'some'.equals(value)) System.out.println("ok");
Optional의 filter는 인자로 받은 Predicate가 일치하면 값을 반환하고 그렇지 않으면 빈 Optional을 반환한다.
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent()) {
return this;
} else {
return predicate.test(value) ? this : empty();
}
}
Optional 객체의 filter를 이용하면 재구현할 수 있다.
Optional<String> optValue = Optional.of(value);
optValue.filter(v -> "some".equals(v))
.ifPresent(x -> System.out.println("ok"));
- Optional은 최대 한 개의 요소를 포함하는 스트림이라고 생각했을 때 Optional에 값이 있으면 ifPresent를 통해서 출력이 될 것이고 값이 없을 때는 빈 Optional이 반환되어 ifPresent에서 걸러져서 출력이 되지 않을것이다.
Optional을 사용한 실용 예제
우리 코드에 작은 유틸리티 메서드를 추가해 Optional기능을 활용해보자
잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
기존의 자바 API에서는 null을 반환하면서 요청한 값이 없거나 어떤 문제로 계산에 실패했음을 알린다.
Object value = map.get("ket");
- Map<String, Object> 형식이 있을 때 값을 찾지 못했을 때 null을 반환하기 보다는 Optional을 반환하는게 더 바람직하다.
Optional<Object> value = Optional.ofNullable(map.get("key"));
- if-then-else를 추가하기 보다는 깔끔하게 Optional.ofNullable을 이용해 null일 수 있는 값을 Optional로 안전하게 변환하자.
예외와 Optional 클래스 - 문자열 ➡️ 정수로 변환
자바 API는 어떤 이유에서 값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시킬 때도 있다.
- 값이 null일 때는 if문으로 확인했지만 예외를 발생키기는 메서드는 try/catch블록을 사용해야 한다는 점이 다르다.
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
String에 null값이 들어왔을 때도 NullPointException이 아니라 NumberFormatException을 반환하기 때문에 안전하다.
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("Cannot parse null string");
}
...
}
유틸리티 클래스 OptionalUtility를 만들어 OptionalUtility.stringToInt를 이용해서 문자열을 Optional<Integer>로 변환하자.
- 거추장스러운 try/catch 로직을 사용할 필요가 없다. (OptionalUtility에서 사용)
기본형 Optional을 사용하지 말아야 하는 이유
스트림처럼 Optional도 기본형으로 특화됱 클래스를 제공한다. (OptionalInt, OptionalLong, OptionalDouble등)
- 스트림은 많은 요소를 가질 때 기본형 특화 스트림을 이용해 (boxing/unboxing)으로 인한 성능을 저하를 방지할 수 있지만 Optional의 최대 요소 수는 한 개이므로 기본형 특화 클래스로 성능을 개선할 수 없다.
- 기본형 특화 Optional은 map, flatMap, filter 등을 지원하지 않고 기본형 특화 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다.
응용
properties에서 name으로 꺼내온 값을 초 단위의 지속 시간(duration)으로 해석한다.
- 지속시간이 음수거나 NumberFormatException이 발생할 수 있기 때문에 try/catch가 중첩되어 코드 품질⬇️
public static int readDurationImperative(Properties props, String name) {
String value = props.getProperty(name);
if (value != null) {
try {
int i = Integer.parseInt(value);
if (i > 0) {
return i;
}
} catch (NumberFormatException nfe) { }
}
return 0;
}
OptionalUtility 클래스를 만들어서 메서드 참조를 통해 깔끔한 코드 구현이 가능해진다.
public static int readDurationWithOptional(Properties props, String name) {
return ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt)
.filter(i -> i > 0).orElse(0);
}
'책 > 모던 자바 인 액션' 카테고리의 다른 글
13 디폴트 메서드 (0) | 2023.08.17 |
---|---|
12. 새로운 날짜와 시간 API (0) | 2023.08.17 |
10. 람다를 이용한 도메인 전용 언어 (1) | 2023.08.04 |
9. 리팩터링, 테스팅, 디버깅 (0) | 2023.08.02 |
8. 컬렉션 API 개선 (0) | 2023.07.28 |