티스토리 뷰
728x90
반응형
💡 Null 탄생의 비하인드 스토리?
- 1965년 토니 호어라는 영국 컴퓨터 과학자가 힙에 할당되는 레코드를 사용하며, 형식을 갖는 최초의 프로그래밍 언어 중 하나인 알골을 설계하면서 처음 Null 참조가 등장하였다고 합니다. 토니 호어는 "구현하기 쉬웠기 때문에 Null을 도입했다" 라고 말했으며 컴파일러의 자동확인 기능으로 모든 참조를 안전하게 사용할 수 있을 것을 목표로 정했다고 합니다.
그 당시 Null 참조 및 예외로 같이 없는 상황을 가장 단순히 구현할 수 있다고 판단했고 그렇게 탄생을 하게 되었습니다. 후에 Null을 만든 결정을 십억 달러짜리의 실수라고 표현을 했다고 합니다.
값이 없는 상황을 어떻게 처리할까?
- 우선 사람은 차를 가질 수 있고 차는 브랜드를 가질 수 있습니다.
@Getter
public class Person {
private String name;
private Car car;
}
@Getter
public class Car {
private String name;
private Brand brand;
}
@Getter
public class Brand {
private String name;
}
🧨 문제가 발생할 수 있는 코드
- 다음 같은 경우 최종적으로 사람이 소유하고 있는 자동차 브랜드의 이름을 가져올려고 NPE의 발생 가능성이 있습니다. getCar() 메서드 호출 시 Null 이거나 getBrand() 메서드 호출 시 Null일 가능성이 있습니다.
Person person = new Person();
person.getCar().getBrand().getName(); // NPE 발생 가능성!
NPE 발생 방지 1) 깊은 의심
- 해당 코드에서는 각 메서드마다 Null 인지 의심하므로 변수에 접근할 때마다 중첩된 if가 추가되면서 들여쓰기 수준이 증가하게 됩니다. 따라서 이와 같은 반복 패턴 코드를 깊은 의심이라 부릅니다.
Person person = new Person();
if (person != null) {
Car car = person.getCar();
if (car != null) {
Brand brand = car.getBrand();
if (brand != null) {
brand.getName();
}
}
}
NPE 발생 방지 2) 너무 많은 출구
- 아래 코드는 중첩 if 블럭을 없앴습니다. 변수가 Null 이라면 즉시 "Unknown"이라는 값을 반환하지만 그렇게 좋은 코드는 아닙니다.
Person person = new Person();
String result = null;
if (person == null) {
result = "Unknown";
}
Car car = person.getCar();
if (car == null) {
result = "Unknown";
}
Brand brand = car.getBrand();
if (brand == null) {
result = "Unknown";
}
result = brand.getName();
💡 Null로 인해 발생할 수 있는 문제들
- 에러의 근원입니다.
- 코드를 어지럽힙니다. -> 중첩 if로 인해 가독성을 떨어트릴 수 있습니다.
- 아무 의미가 없습니다. -> Null은 아무 의미도 표현하지 않습니다.
- 자바 철학에 위배됩니다. -> 자바는 개발자로부터 모든 포인터를 숨겼습니다. 하지만 예외가 있는데 그것은 바로 Null 포인터입니다.
- 형식 시스템에 구멍을 만듭니다.
Optional 클래스 소개
- 자바 8부터는 java.util.Optional<T> 클래스를 제공하고 있습니다.
- Optional<T>는 값이 있으면 값을 감싸고, 값이 없다면 Optional.empty 메서드로 Optional을 반환하는데 Optional.empty는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드 입니다.
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}
Optional 적용 패턴
- Optional.of(), Optional.ofNullable(), Optional.of() 로 Optional 객체를 만들 수 있습니다.
💡 map() 메서드를 사용하여 Optional의 값을 추출하고 변환하기
- Person 클래스의 Car를 Optional로 포장하고, Car 클래스의 Brand를 Optional로 포장했습니다. 이때 getBrandName() 메서드는 제대로 컴파일이 되지 않습니다.
@Getter
public class Person {
private String name;
private Optional<Car> car; // Optional
}
@Getter
public class Car {
private String name;
private Optional<Brand> brand; // Optional
}
@Getter
public class Brand {
private String name;
}
// reason: no instance(s) of type variable(s) exist so that Optional<Car> conforms to Car
public static Optional<String> getBrandName(Person person) {
Optional<Person> personOptional = Optional.of(person);
Optional<String> brandName = personOptional.map(Person::getCar)
.map(Car::getBrand)
.map(Brand::getName);
return brandName;
}
🤔 map 메서드를 이용하면 컴파일 에러가 발생하는 이유
- map 메서드를 사용하면 처음 map 메서드에서 Optional<Optional<Car>> 처럼 래핑이 되고 두번째 map에서는 Optional<Optional<Brand>>처럼 래핑이 됩니다. 그렇기 때문에 flatMap 메서드를 사용하여 평준화를 시켜줘야 합니다.
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}
💡 map() 대신 flatMap() 사용
- flatMap 연산으로 Optional을 평준화할 수 있습니다. 평준화란 이론적으로 두 Optional을 합치는 기능을 수행하면서 둘 중 하나라도 Null이면 빈 Optional을 생성하는 연산입니다.
- flatMap을 빈 Optional에 호출하면 아무일도 일어나지 않고 그대로 반환됩니다. 반면 Optional이 Person을 감싸고 있다면
flatMap에 전달된 Function에 Person이 적용됩니다. 마지막 map은 Optional을 반환하는게 아닌 String을 반환하기 때문에
flatMap을 사용할 필요가 없습니다.
// Optional 클래스의 flatMap 메서드
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
@SuppressWarnings("unchecked")
Optional<U> r = (Optional<U>) mapper.apply(value);
return Objects.requireNonNull(r);
}
}
public static Optional<String> getBrandName(Person person) {
Optional<Person> personOptional = Optional.of(person);
Optional<String> brandName = personOptional.flatMap(Person::getCar)
.flatMap(Car::getBrand)
.map(Brand::getName);
return brandName;
}
💡 Optional 스트림 조작
- 자바 9에서는 Optional을 포함하는 스트림을 쉽게 처리할 수 있도록 Optional에 stream() 메서드를 추가하였습니다.
public static Set<String> getBrandName(List<Person> person) {
return person.stream()
.map(Person::getCar) // return: Stream<Optional<Car>>
.map(car -> car.flatMap(Car::getBrand)) // return: Stream<Optional<Brand>>
.map(brand -> brand.map(Brand::getName)) // return: Stream<Optional<String>>
.flatMap(Optional::stream)// Stream<Optional<String>> 을 Stream<String> 으로 평준화
.collect(Collectors.toSet());
}
- Optional과 filter사용
public static Set<String> getBrandName(List<Person> person) {
return person.stream()
.map(Person::getCar)
.map(car -> car.flatMap(Car::getBrand))
.map(brand -> brand.map(Brand::getName))
.filter(Optional::isPresent) // name이 null이 아닌것만 필터링
.map(Optional::get) // 값이 있는것만 필터링했기 때문에 비로 get해도 무방
.collect(Collectors.toSet());
}
💡 Optional의 orElse와 orElseGet의 차이
- Optional 클래스의 orElse 메서드와 orElseGet 메서드입니다.
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
공통점
- 둘다 value가 null이 아닐경우 value를 반환하고 있습니다.
차이점
- orElse 메서드는 값이 null인 경우 T 타입의 other를 반환합니다.
- orElse 메서드는 해당 값이 null 이든 아니든 관계없이 항상 실행됩니다.
- orElseGet 메서드는 값이 null인 경우 Supplier 인터페이스의 get 메서드를 통해 값을 반환합니다.(Lazy)
- orElseGet 메서드는 값이 항상 null인 경우만 실행됩니다.
예제 코드
- 아래 코드를 보면 text는 Null이 아니지만 orElse 메서드에서 getText() 메서드를 호출했을 경우 실행되는 것을 알 수 있습니다.
- 반면 text가 Null인 경우 orElse, orElseGet 메서드 모두 getText() 메서드가 호출되는 것을 알 수 있습니다.
- 중요한 사실은 orElse, orElseGet 메서드 모두 value가 Null이든 아니든 둘다 호출 됩니다. orElse의 경우 value가 Null인 경우 바로 T 타입의 값을 반환하지만 orElseGet 메서드는 Supplier 제네릭으로 래핑되어 있기 때문에 Null이 아닐 때 인스턴스가 생성되고 값을 반환하기 때문입니다.
// text가 Null이 아닌 경우
public static void main(String[] args) {
String text = "Not Null";
String result1 = Optional.ofNullable(text).orElse(getText());
System.out.println(result1);
// Output -> 과연 호출될까요?
// Output -> Not Null
String result2 = Optional.ofNullable(text).orElseGet(() -> getText());
System.out.println(result2);
// Output -> Not Null
}
// text가 Null인 경우
public static void main(String[] args) {
String text = null;
String result1 = Optional.ofNullable(text).orElse(getText());
System.out.println(result1);
// Output -> 과연 호출될까요?
// Output -> Text is Null
String result2 = Optional.ofNullable(text).orElseGet(() -> getText());
System.out.println(result2);
// Output -> 과연 호출될까요?
// Output -> Text is Null
}
private static String getText() {
System.out.println("과연 호출될까요?");
return "Text is Null";
}
기본형 Optional은 사용하지 말자
- 스트림에서는 기본형으로 특화된 DoubleStream, IntStream, LongStream을 제공하고 있습니다. Optional에서도 기본형에 특화된 OptionalDouble, OptionalInt 등을 제공하고 있는데 Optional에서는 최대 요소 수는 한 개 이므로 기본 특화형 Optional을 사용한다고 해서 성능을 개선할 수 없습니다.
- 기본형 특화 Optional은 Optional 클래스에서 유용한 map, flatMap, filter 등을 지원하지 않으므로 기본형 특화 Optional을 사용하는 것을 권장하지 않습니다.
- 기본형 특화 Optional로 생성한 결과는 다른 일반 Optional과 혼용할 수 없습니다.
728x90
반응형
'스터디 > 모던 자바 인 액션' 카테고리의 다른 글
모던 자바 인 액션 - 16장 CompletableFuture: 안정적 비동기 프로그래밍 (2) | 2022.09.20 |
---|---|
모던 자바 인 액션 - 13장 디폴트 메서드 (0) | 2022.09.17 |
모던 자바 인 액션 - 9장 리펙터링, 테스팅, 디버깅 (0) | 2022.09.11 |
모던 자바 인 액션 - 8장 컬렉션 API 개선 (0) | 2022.09.10 |
모던 자바 인 액션 - 6장 스트림으로 데이터 수집 (0) | 2022.09.10 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- redis sorted set
- @ControllerAdvice
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot poi excel download
- 공간 기반 아키텍처
- transactional outbox pattern spring boot
- microkernel architecture
- pipe and filter architecture
- service based architecture
- java userThread와 DaemonThread
- 레이어드 아키텍처란
- java ThreadLocal
- spring boot redisson destributed lock
- space based architecture
- spring boot redisson 분산락 구현
- redis 대기열 구현
- spring boot excel download oom
- 람다 표현식
- spring boot redis 대기열 구현
- redis sorted set으로 대기열 구현
- spring boot excel download paging
- transactional outbox pattern
- spring boot redisson sorted set
- 서비스 기반 아키텍처
- polling publisher spring boot
- 자바 백엔드 개발자 추천 도서
- spring boot 엑셀 다운로드
- 트랜잭셔널 아웃박스 패턴 스프링부트
- pipeline architecture
- JDK Dynamic Proxy와 CGLIB의 차이
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함