예상치 못한 Null Pointer Exception에 대응하기 위해, 우리는 가끔 이런 코드를 짜곤 합니다.
if (a != null && a.getA() != null) {
// TO DO
}
이렇듯 null
은 코드를 길어지게 하지만 꼭 막아야하는 불편함을 제공하죠.
여기서, 우리는 자바 8에서 도입된 Optional 을 사용할 수 있습니다.
Optional
객체 생성하기
먼저, 반드시 값이 있어야하는 객체의 경우에는 다음과 같이 생성할 수 있습니다.
Optional<String> optional = Optional.of("hi");
값이 null
일 수도 있는 경우에는 다음과 같이 객체를 생성할 수 있습니다.
Optional<String> optioanl = Optioanl.ofNullable(null);
그렇다면, 비어있는 Optional 객체는 어떻게 생성할까요?
Optional<String> emptyOptional = Optional.empty();
예제와 함께 Optional을 이용하여 코드를 더욱 효율적으로 만들어보겠습니다.
/* 학교 */
public class School {
private Student student;
private String name;
}
/* 학생 */
public class Student {
private String name;
private Long Id;
private Address address;
}
/* 주소 */
public class Address {
private String street;
private String city;
private String zipcode;
}
학교는 학생을 필드로 갖고, 학생은 주소를 필드로 갖습니다. 위와 같은 상황에서 우리는 학교의 학생이 살고있는 도시를 알고 싶습니다.
public String getCityOfStudentFromSchool(School school){
return school.getStudent().getAddress().getCity();
}
위 코드를 통해 발생할 수 있는 NPE
는 다음과 같습니다.
- 학교가
null
인 경우 school.getStudent()
가null
인 경우school.getStudent().getAddress()
가null
인 경우school.getStudent().getAddress().getCity()
가null
인 경우
우리는 이러한 어이없이 많은 NPE 발생 경우에 대비하기 위해 모든 경우에 대해 중첩 Null 체크를 하는 등의 방법으로 NPE를 방어할 수 있습니다. 하지만, 이 방법이 아닌 Optional을 이용한다면 더욱 효율적으로 이를 방어할 수 있습니다.
public String getCityOfStudentFromSchool(School school) {
return Optional.ofNullable(school) // 학교가 null 일 수 있는 경우를 감싸주었어요.
.map(School::getStudent) // map의 연쇄 호출을 통해 Optional에 담긴 객체의 타입을 바꿔 주었어요.
.map(Student::getAddress)
.map(Address::getCity)
.orElse("Empty"); // 혹시라도 Optional 객체가 비어있는 경우에는 비어있다는 디폴트 값을 넣어주었어요.
}
Optional 객체를 생성한 후 사용 가능한 메서드
- Filter
// Optional 개체가 Joanne 과 같다면 Joanne을, 아니면 No Joanne을 출력합니다.
Optional.of("Joanne").filter((val) -> "Joanne".equals(val)).orElse("NO Joanne");
- Map
// Map 함수를 통해 입력 값을 다른 값으로 변환합니다.
String stringValue = Optional.of(1).map(Integer::toString).orElseThrow(Exception::new);
- flatMap
mapper
함수를 통해 입력 값을 다른 값으로 변환하지만, 시그니처의 매개변수가 Optional(U)
인 점이 Map
과 다릅니다. 즉, flatMap()
메서드가 반환해야하는 값은 Optional
객체입니다.
- ifPresent / isPresent
/* ifPresent
* 연산을 끝낸 후 값이 비어있지 않다면 입력값으로 주어집니다.
*/
Optional.of("Joanne").ifPresent((val) -> {
// 수행합니다.
});
Optional.ofNullable(null).ifPresent((val) -> {
// 수행하지 않습니다.
});
/* isPresent
* 최종 연산을 끝낸 후 객체가 존재하는지 여부를 판별합니다.
*/
Optional.ofNullable("Joanne").isPresent(); //true
Optional.ofNullable("J").filter((v) -> "Joanne".equals(v)).isPresent(); //false
- get
최종적으로 연산을 끝낸 후 객체를 꺼냅니다. 이 때, 비어있는 객체였다면 예외가 발생합니다.
- orElse / orElseGet / orElseThrow
최종적으로 연산을 끝낸 후에도 비어있다면 ~ 이후의 내용을 수행합니다.
/* orElse
* 최종 연산 후 비어있는 객체라면 디폴트로 제공할 객체를 지정합니다.
* 여기서 result는 "default" 를 나타냅니다.
*/
String result = Optional.OfNullable(null).orElse("default");
/* orElseGet
* 최종 연산 후 비어있는 객체라면 기본값으로 제공할 Supplier를 지정합니다.
*/
Optional.ofNullable("input").filter("output"::equals).orElseGet(() -> "default");
/* orElseThrow
* 최종 연산 후 비어있는 객체라면 예외를 발생시킵니다.
*/
Optional.ofNullable("input").filter("output"::equals).orElseThrow(Exception::new);