티스토리 뷰

Java

Java8#05. Optional

LichKing 2016.10.31 09:32

자바8을 처음 접했을때 느꼈던 점은 자바8의 꽃은 스트림이라는 것이었다. 그러나 내 맘을 가장 매혹시키고, 언젠가 자바8을 쓸날이 오면 이것만큼은 꼭 잘 쓰고싶다고 생각했던것은 오늘 포스팅할 Optional이었다.


1. null

자바는 좀 더 쉽게 프로그래밍하자는 취지에서 개발됐다. OSMU(One Source Multi Use)를 위해 하나의 코드로 OS에 상관없이 돌아갈수 있게 개발됐고, C계열을 처음 공부할때 지옥이라 불리는 포인터를 모두 감췄다. 그러나 단 하나 감추지 못한 포인터가 있는데 그것이 null포인터다.

자바 프로그래밍에서 NullPointerException은 언제 터질지 모르는 에러의 근원이었고 이를 방지하기위해 대부분의 메서드 앞줄에선 중복적인 방어코드가 들어가는 문제가 있었다.


public void method01(Object obj){
if(obj == null){
return;
}

// 로직
}

public String method02(String str){
if(str == null){
// null을 그대로 리턴하는건 클라이언트에서 다시금 null체크를 해줘야하기떄문에 기본값을 리턴하는걸 권장한다.
return null;
return "";
}

// 로직
}

매우 익숙한 코드일것이다. 하지만 저런 null체크는 반복적이고 코드가 지저분해지는 문제가 있고, 더욱이 쉽게 까먹고 넣지 못할경우 어김없이 NPE를 만나게된다. 때문에 저런 반복적인 코드를 안빼먹고 넣어주는게 오히려 꼼꼼한 개발자라는 말을 듣게 만들었다. 지금 저 예제는 그나마 인자로 넘어오는 객체의 레퍼런스만 체크하고있지만 이런 코드는 어떨까?


public void method01(Object obj) {
// getter를 이용해 내부 필드를 깊게들어가서 값을 가져옴
Object obj1 = obj.getObject().getObject.getObject();

// 로직
}

이런코드는 obj의 레퍼런스만 검사한다고해서 NPE를 벗어날수는 없다. 이런 코드가 나오게 된다.


public void method01(Object obj) {
Object obj1 = null;
if(obj != null){
if(obj.getObject() != null){
if(obj.getObject().getObject() != null){
obj1 = obj.getObject().getObject().getObject();
}
}
}

// 로직
}

깊은 의심(Deep Doubt) 패턴


보기만해도 뭔가 아닌거같다. 리팩토링을 해야할것같다. 그나마 해본게 이거다.


public void method01(Object obj) {
Object obj1 = null;
if (obj != null && obj.getObject() != null && obj.getObject().getObject() != null) {
obj1 = obj.getObject().getObject().getObject();
}

// 로직
}

if문은 한줄로 줄었지만 보기엔 역시 좋지 않다. 헬퍼메서드를 사용하는게 최선인것 같다.


public void method01(Object obj) {
Object obj1 = null;
if (isValidObject(obj)) {
obj1 = obj.getObject().getObject().getObject();
}

// 로직
}

private boolean isValidObject(Object obj){
return obj != null && obj.getObject() != null && obj.getObject().getObject() != null;
}


2. Optional

이러한 반복적인 null 체크를 없애기위해 Java8에 Optional<T>이라는 클래스가 추가됐다. 기술적으로 크게 어려운점은 없다. 실제 레퍼런스를 한번 감싸는 래퍼 객체를 만들어 null 체크를 내부로 숨겼기에 외부코드에선 null 체크가 보이지않게 감춘것에 불과하다.


class Optional<T>{
private T t;
}


Stream과 마찬가지로 생성자가 아니라 Static Factory 메서드를 이용해 객체를 생성한다.


public static void main(String args[]) {
String str = "hello";
Optional<String> o1 = Optional.of(str); // str이 null이면 NPE 발생
Optional<String> o2 = Optional.ofNullable(str); // str이 null이면 빈 Optional 객체 반환
Optional<String> o3 = Optional.empty(); // 빈 Optional 객체 반환
}

of() 같은 경우는 인자가 null이면 NPE를 발생시키기때문에 Optional을 쓸때 크게 쓸일이 없는것 같고.. of()를 쓰는 경우는 null을 예방하기위한다기보다는 Optional이 제공하는 API를 이용하고자 할때 쓸듯 하다. 슬슬 실무에 Optional을 활용하고있는데 ofNullable()만 사용하고 있는듯...


한번 더 감싸고있는 형태이기때문에 API는 내부 객체에대한 연산이 주를 이룬다.


2-1. Optional API

-boolean isPresent()

내부객체가 null이 아닌지 확인한다. null이면 false를 반환한다.


-void ifPresent(Consumer<T>)

Consumer<T>는 함수형 인터페이스 포스팅에서 봤듯 void 추상메서드를 갖고있다. null이 아닐때만 실행된다.


-Optional<T> filter(Predicate<T>)

스트림은 여러 데이터를 들고있는 객체다보니 filter로 걸러지는 데이터들이 반환됐지만, Optional은 내부객체가 단일객체인만큼 해당 조건을 만족하는지만 확인하는 정도로 사용할 수 있을 것 같다.


-Optional<U> map(Function<T, U>)

스트림과 같다. 내부 객체를 변환하는 용도로 사용한다.


-T get()

내부 객체를 반환한다. 다만 내부 객체가 null이면 NPE가 발생한다. null이 아니라는 확실한 경우에만 사용을 권장한다.


-T orElse(T)

내부 객체를 반환한다. 내부 객체가 null이면 인자로 들어간 기본값을 반환한다.


-T orElseGet(Supplier<T>)

orElse()와 동일한데 orElse()가 기본값 레퍼런스를 인자로 받는다면 orElseGet()은 내부 객체가 null일때 기본값을 반환할 객체를 인자로 받는다.


-T orElseThrow(Supplier<U>)

내부 객체가 null이면 인자로 전달받은 예외를 발생시킨다.


2-2. Anti Pattern

Optional을 처음 접하면 익숙치 않은맘에 이런 코드를 작성하게 된다.

Optional<Integer> integer = Optional.empty();

if(integer.isPresent()){
// ...
}

// ...

기존의 == null 체크와 다를게 없는 코드다. 이런코드가 나온다면 Optional을 잘못쓰고있을 확률이 높다. 가장 주의하고, 절대 지양해야할 패턴이다.


2-3. orElse() / orElseGet()

둘다 내부객체가 null일때 뭘 줄건지를 지정하는 메서드이다. 둘이 차이가 뭘까? null 이면 Object 객체를 반환한다고 가정해보자.


Optional<Object> objectOptional = Optional.empty();

Object object1 = objectOptional.orElse(new Object());
Object object2 = objectOptional.orElseGet(() -> new Object());
Object object3 = objectOptional.orElseGet(Object::new);

3개가 모두 같은 표현이다. 2번째와 3번째는 람다를 메서드 레퍼런스로 바꾼것이다. 이건 지극이 개인적인 생각이라 잘못 이해했을수도있는데 내가 이해하기로 orElse()는 값을 지정할때 사용하고 orElseGet()은 레퍼런스를 지정할때 사용하는것이 좋은것같다. 즉, 좀 더 Lazy하게 사용할 수 있는게 orElseGet()이라는 것이다.


orElse(new Object()); 같은 구문은 실제 내부 객체가 null이든 아니든 일단 Object 의 객체는 생성하게된다. 객체를 생성해서 해당 레퍼런스를 전달하고 orElse() 내부에서 null인지 아닌지 판별 후 이미 생성된 Object 객체를 사용하거나 사용하지않는것을 결정한다는 것이다. 그에 반해 orElseGet()은 객체를 생성하는 행위를 하는 메서드를 전달한다. orElseGet() 내부에서는 내부객체가 null일때만 해당 메서드를 실행하기때문에 null이 아니면 객체를 생성되지 않는다. 차이가 이해되는가? 또는 단순히 빈객체만 생성해서 반환하는게 아니라 객체가 생성될때 실행되어야할 행위들이 있다면? 그럴때도 orElseGet()이 유용할 것이다. 간단히 setter를 실행한다고 생각해보자.


Object object2 = objectOptional.orElseGet(() -> {
Object obj = new Object();
obj.setItem();
obj.setName();
return obj;
});

orElse()로 이런것들을 하려면 미리 setter를 다 호출해서 값을 넣어두고 그 객체를 인자로 보내야한다. null일때만 사용할, 실제 사용할지말지 알지도못하는 객체를 위해 그런 행위들이 무조건적으로 수행되고 바깥 스코프에 변수가 생기는건 좋은 코드라고 보기 힘들것같다. 그에 반해 값들은 orElse()를 쓰는것이 좀 더 좋을것같다.


Optional<Integer> integerOptional = Optional.empty();

Integer number1 = integerOptional.orElse(5);
Integer number2 = integerOptional.orElseGet(() -> 5);

이 경우는 orElse()가 좀 더 바람직한 방법이라고 보인다.


2-4. 활용

이제 위에서 봤던 깊은 의심 패턴 예제를 Optional로 바꿔보자.

Optional<Object> objectOptional = Optional.empty();

Object obj = objectOptional.map(Object::getObject)
.map(Object::getObject)
.orElse(null); // orElse(new Object), orElseGet(Object::new)

Optional 내부에있는 Object에 getter를 연속적으로 호출한다. 메서드가 진행되면서 중간에 null이 발견되면 NPE없이 orElse()가 호출되게된다. 어차피 값이 1개뿐인 filter보다 Optional에서는 map이 거의 주로 사용될 듯하다.


3. 기본형 Optional

Stream은 Auto Boxing, Auto Unboxing비용을 줄이기위해, 그리고 좀 더 기본형에 특화된 API 제공을 위해 기본형 스트림을 제공한다. IntStream, DoubleStream이 대표적인데 Optional도 기본형 Optional을 제공한다.


OptionalInt, OptionalDouble, OptionalLong이 있으며 사용법은 크게 다르지 않다.

OptionalInt optionalInt1 = OptionalInt.empty();
OptionalDouble optionalDouble1 = OptionalDouble.empty();
OptionalLong optionalLong1 = OptionalLong.empty();

다만 스트림같은경우 자료구조를 다루는 API이기때문에 대량의 형변환 비용을 줄이는 이점이 있는 반면 Optional은 어차피 내부에 1개의 레퍼런스만을 갖고있기때문에 기본형 사용의 이점이 크게 반감된다. 거기다 기본형 Optional과 Optional은 관계도 없어 형변환이 되는것도 아니기때문에 혼용한다면 불편함만 생길수있어 기본형스트림에 비해서는 그다지 환영받지 못하는 것 같다.

'Java' 카테고리의 다른 글

토비의봄#02. Super Type Token  (0) 2016.11.12
토비의봄#01. Double Dispatch  (4) 2016.11.07
Java8#05. Optional  (0) 2016.10.31
리스트돌릴땐 무조건 foreach를 사용하자  (4) 2016.10.30
Java8#04. 스트림(Stream)  (2) 2016.10.19
Java8#03. 메서드 레퍼런스(Method Reference)  (2) 2016.10.17
댓글
댓글쓰기 폼