자비스 앤 빌런즈 인터뷰 프로세스를 진행하면서 면접관님께서 “Optional.of
로 왜 데이터를 저장할 때 감쌌는지” 여쭤보셨다.
나는 내가 작성한 로직 의도대로 말씀드렸지만, 실제 메서드 내부의 동작이나 어떤 파라미터가 들어오고 어떤 경우에 이를 쓰는지, 어떤 예외가 발생할 수 있는지 등등 여러가지 꼬리 질문이 들어왔고 제대로 답변 드리지 못했다.
특히, Optional을 주로 써오면서 .of().orElseThrow()
이런 방식으로 코딩을 가끔 했었는데 그동안은 예외가 발생하지 않았던 것이지 null이 들어오는 경우 반드시 NPE가 발생하는 것에 대해 면접을 통해 알게 되었다.
이를 계기로 기본적인 Optional 사용법에 대해 파악하고 앞으로는 Optional을 사용할 때 적절한 경우와 조심해서 사용해야겠다는 생각이 들어 정리하게 되었다.
Optional?
- Java 8부터 지원하는 null-safe한 코드를 도와주는 래퍼 클래스, NPE 방지를 도와준다.
- Optional은 에서
null
이 올 수 있는 값들을 감싸는 Wrapper 클래스이다. - 제너릭 타입 T에는
null
값이 올 수도 있기 때문에 Optional은 이 T를 감싸서 null-safe한 코드를 작성하게 해준다.
Optional의 효과
- null을 개발자가 직접 다루지 않아도 된다.
- null 체크 로직을 줄일 수 있다.
- 명시적으로 해당 변수가 Null일 수도 있음을 전달할 수 있다.
Optional 객체 생성
Optional.empty()
: null을 감싼 Empty Optional 객체를 가져온다.- empty() 메서드는 Optional 내부에서 EMPTY 객체를 싱글톤 형태로 만들어 공유한다.
@Test
@DisplayName("Optional.empty")
void emtpy() {
Optional<Person> empty = Optional.empty();
// Optioanl.empty는 null이 아니다.
assertThat(empty).isNotNull();
assertThat(empty).isEmpty();
assertThat(empty).isNotPresent();
}
Optional.of()
: null이 아닌 객체를 담는 Optional 객체를 생성한다.- null이 들어오는 경우 NPE가 발생하므로 메서드 사용에 주의가 필요하다!!!
@Test
@DisplayName("Optional.of with not null")
void of_with_not_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.of(person);
assertThat(optionalPerson).isNotEmpty();
assertThat(optionalPerson.get()).isInstanceOf(Person.class);
}
// Optional.of에 null이 들어온 경우
@Test
@DisplayName("Optional.of with null")
void of_with_null() {
Person person = null;
// .of는 null이 들어오는 경우 NPE를 발생시킨다!
assertThrows(NullPointerException.class, () -> Optional.of(person));
}
Optional.ofNullable()
: null인지 아닌지 확실할 수없는 OIptional 객체를 생성한다..of()
와 달리 null이 들어오는경우 NPE가 아닌 Optional.empty 객체를 반환한다.- 해당 객체가 null인지 아닌지 모르는 경우에 이를 사용한다.
@Test
@DisplayName("Optional.ofNullable with not null")
void ofNullable_with_not_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
assertThat(optionalPerson).isNotEmpty();
assertThat(optionalPerson.get()).isInstanceOf(Person.class);
}
// Optional.ofNullable()에 null이 들어오는 경우
@Test
@DisplayName("Optional.ofNullable with null")
void ofNullable_with_null() {
Person person = null;
Optional<Person> optionalPerson = Optional.ofNullable(person);
// ofNullable의 경우 null을 넣어주었지만 null이 아니고 empty 객체가 들어간다.
assertThat(optionalPerson).isNotNull();
assertThat(optionalPerson).isEqualTo(Optional.empty());
}
Optional 접근하기
.get()
: Optional 인스턴스에 접근해 감싸진 객체를 가져온다.- Optional 객체가 empty인 경우
NoSuchElementException
을 발생시킨다.
- Optional 객체가 empty인 경우
@Test
@DisplayName("Optional.get with null")
void get_with_null() {
Person person = null;
Optional<Person> optionalPerson = Optional.ofNullable(person);
// Optional객체가 empty일 때 get하면 NoSuchElementException이 발생한다.
assertThrows(NoSuchElementException.class, optionalPerson::get);
}
.orElse(T other)
: Optional 인스턴스가 empty인 경우 파라미터로 넘어온 T를 리턴한다.- empty 인스턴스가 아니더라도 .orElse의 파라미터를 실행한다.
@Test
@DisplayName("Optional.orElse")
void orElse_without_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
//원본 객체가 존재하더라도 orElse를 호출하는가?
Person getPerson = optionalPerson.orElse(callMethod());
assertThat(getPerson.getName()).isEqualTo("zayson");
assertThat(call).isTrue();
}
.orElseGet(Supplier<? extends T> other)
: Optional 인스턴스가 empty인 경우에만 파라미터로 넘어간 함수가 호출된다.- empty가 아니라면 .orElseGet의 함수 파라미터를 실행하지 않는다.
@Test
@DisplayName("Optional.orElseGet")
void orElseGet_without_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
// 원본 객체가 존재하는 경우 orElseGet()을 호출하는가?
Person getPerson = optionalPerson.orElseGet(this::callMethod);
assertThat(getPerson.getName()).isEqualTo("zayson");
assertThat(call).isFalse();
}
.orElseThrow(Supplier<? extends X> exceptionSupplier)
- Optional 인스턴스에 접근하고 empty인 경우 파라미터로 던진 예외를 던진다.
@Test
@DisplayName("Optional.orElseThrow")
void orElseThrow() {
Person person = null;
Optional<Person> optionalPerson = Optional.ofNullable(person);
assertThrows(CustomNotFoundException.class, () -> optionalPerson.orElseThrow(() -> new CustomNotFoundException("error msg")));
}
🔥 orElse
() VS orElseGet()
orElse
와orElseGet
모두 객체가 null인 경우, 즉 Optional의 인스턴스가 empty인 경우 파라미터로 넘어온 객체나, 함수 파라미터를 실행한다는 점에서는 동일하다.- 원본 객체가 null이 아닌경우 orElseGet()의 함수 파라미터는 실행되지 않지만, orElse()는 실행된다는 차이가 있다.
위에 작성한 테스트 코드를 이용해 조금 더 파헤쳐보자.
@BeforeEach
void setUp() {
call = false;
}
private static boolean call;
private Person callMethod() {
// 호출 되는 경우 true로 값을 바꾼다.
call = true;
return new Person("maeng", 30, new Address("seoul", "woosung", "street"));
}
테스트를 위한 기본 세팅을 진행했다.
- 각 테스트가 실행될 때마다 static 변수인 call을 false로 세팅해주었다.
orElse()
,orElseGet()
의 파라미터로 callMethod()를 넣어줄 것이다.
Person이 null이라면?
- Person객체가
null
인 경우orElse(), orElseGet()
모두 실행되고 파라미터가 호출되거나 리턴되므로 callMethod에서 리턴한 Person객체를 리턴한다.
@Test
@DisplayName("Optional.orElse with null")
void orElse_with_null() {
Person emptyPerson = null;
Person person = Optional.ofNullable(emptyPerson).orElse(callMethod());
assertThat(person.getName()).isEqualTo("maeng");
assertThat(person.getAddress()).isNotNull();
}
@Test
@DisplayName("Optional.orElseGet with null")
void orElseGet_with_null() {
Person emptyPerson = null;
Person person = Optional.ofNullable(emptyPerson).orElseGet(this::callMethod);
assertThat(person.getName()).isEqualTo("maeng");
assertThat(person.getAddress()).isNotNull();
}
Persondl null이 아니라면?
- 만약
orElseGet()
이 호출되었다면, callMethod를 호출하고 메서드 내부에 있는 call을 true로 변환해주었을 것이다. - 하지만
orElseGet()
은 Person이null
이 아니기 때문에 호출되지 않고 함수형 파라미터로 넘어간 callMethod 또한 호출되지 않아 call의 값은 그대로 false 이다. - 따라서, 아래의 테스트 코드는 통과한다.
@Test
@DisplayName("Optional.orElseGet")
void orElseGet_without_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
// 원본 객체가 존재하는 경우 orElseGet()을 호출하는가?
Person getPerson = optionalPerson.orElseGet(this::callMethod);
assertThat(getPerson.getName()).isEqualTo("zayson");
assertThat(call).isFalse(); // call이 false이다.
}
- 반면에
orElse()
의 경우를 보자. 위에 작성한orElseGet()
과 동일한 코드이다. orElse()
의 경우 Person이null
이 아님에도 호출되어 callMethod를 통해 call이 true로 변환된 것을 확인할 수 있다.
@Test
@DisplayName("Optional.orElse")
void orElse_without_null() {
Person person = notNullPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
//원본 객체가 존재하더라도 orElse를 호출하는가?
Person getPerson = optionalPerson.orElse(callMethod());
assertThat(getPerson.getName()).isEqualTo("zayson");
assertThat(call).isTrue(); // call이 true이다.
}
orElse()
,orElseGet()
은null
인 경우는 동일하게 작동하지만 아닌 경우는orElse
만 호출된다.- 즉,
orElseGet()
을 사용하도록 하자!orElse()
를 써야한다면null
이아닌 경우에도 호출됨을 알고 사용하자! orElseGet()
은null
인 경우에만 호출하므로 orElse()보다 성능이 좋다.
Optional을 간단하게 다뤄보자!
isPresent()
: boolean 값을 리턴해 Optional 인스턴스가 empty()인지 아닌지 판단한다.
@Test
@DisplayName("Optional.isPresent")
void ifPresent() {
Person person = new Person("maeng", 30, new Address("seoul", "woosung", "street"));
boolean present = Optional.ofNullable(person).isPresent();
assertTrue(present);
}
isPresent() 사용을 지양하자!
사실상 아래는 동일한 코드로 동작한다.
Optional을 이용해서 null 체크를 위임했는데 다시 null을 체크하는 로직은 비싼 Optional만 사용한 것이다.
따라서, ifPresent()에 로직을 넣어 처리하자!// 두 로직이 동일한 역할을 한다 if(name != null) { ... } // Optional을 사용하지 않고 null 체크 if(opt.isPresent()) { ... } // Optional을 사용해서 null체크
- ifPresent(Consumer<? super Person> action) : Optional 인스턴스가 empty()가 아닌 경우 로직을 실행한다.
- 아래의 두 코드는 동일한 역할을 하는 코드이다.
if(name != null) {
System.out.println(name.length());
}
// 동일한 코드
Optional.ofNullable(name).ifPresent(name -> System.out.println(name.length());
ifPresentOrElse
- Java 9부터 지원하는 메서드
- 첫 번째 파라미터는 존재하는 경우 (ifPresent)에 대한 로직 처리
- 두 번째 파라미터는 존재하지 않을때 실행될 action에 대해 로직 정의
@Test
@DisplayName("Optional.ifPresentOrElse")
void ifPResentOrElse() {
// 존재하는 경우
Person person = new Person("maeng", 30, new Address("seoul", "woosung", "street"));
Optional.ofNullable(person)
.ifPresentOrElse(
p -> assertThat(p.getName()).isEqualTo("maeng"),
() -> System.out.println("수행되지 않는 경우!")
);
person = null;
Optional.ofNullable(person)
.ifPresentOrElse(
p -> assertThat(p.getName()).isEqualTo("maeng"),
() -> System.out.println("=============로직이 수행된다 ===============")
);
}
- Optional은 Stream API 처럼 map, filter 메서드를 지원한다.
map()
: 다른 타입의 객체로 mapping해주는 메서드, Function이 파라미터로 들어가기 때문에 T → R 로 변환해주는 역할을 한다.
@Test
@DisplayName("Optional.map")
void map() {
Person person = new Person("maeng", 30, new Address("seoul", "woosung", "street"));
// Person -> Address -> String
String city = Optional.ofNullable(person)
.map(Person::getAddress)
.map(Address::getCity)
.orElseGet(() -> "bundang");
assertThat(city).isEqualTo("seoul");
}
filter()
- Predicate가 파라미터로 들어가므로 T → boolean값을 준다.
- filter에서 false가 나오는 경우 Optional.empty를 리턴한다.
@Test
@DisplayName("Optional.filter")
void filter() {
Person person = new Person("maeng", 30, new Address("seoul", "woosung", "street"));
String name = Optional.ofNullable(person)
.filter(p -> p.getAge() > 28)
.map(Person::getName)
.orElseGet(() -> "zayson");
assertThat(name).isEqualTo("maeng");
assertThat(
Optional.ofNullable(person).filter(p -> p.getAge() > 30)
)
.isEqualTo(Optional.empty()); // 30살 초과인경우 filter -> false
}
🏁 Github Test Code
📄 References
Baeldung : Guid To Java 8 Optional
Baeldung : orElse vs orElseGet
자바8 Optional을 다루는 방법 : Daleseo님 블로그
반응형
'Backend > Java' 카테고리의 다른 글
Stream API 무작정 연습하기! (0) | 2022.07.17 |
---|---|
ArrayList, LinkedList (0) | 2022.06.05 |
[백기선님과 함께하는 Live-Study] 10주차) 멀티쓰레드 프로그래밍 (0) | 2022.05.28 |
[백기선님과 함께하는 Live Study] 9주차) 예외 처리 (0) | 2022.05.17 |
[백기선님과 함께하는 Live Study] 8주차) 인터페이스 (0) | 2022.05.11 |