Java Optional을 알아보자

박진규·2022년 6월 4일
0
post-thumbnail

Java 8 환경에서 개발하다보면 생소한 개념이나 클래스들이 많이 등장하는데
오늘은 Java 8에서 처음 추가된 Optional클래스에 대해 알아보자

Optional은 어떤 Type의 클래스를 한번 더 감싼 Wrapper Class이다.

Wrapper Class ?

Java에서 변수 사용시에 크게 두가지 타입이 존재한다.
1. Literal Type(int, char 와 같은 원시 타입)
2. Reference Type (Integer, String 과 같은 참조형 타입)

Literal타입은 해당 변수의 값을 직접 저장하고 참조타입은 해당 값이 저장되어있는 메모리 주소값을 저장한다.
이를 사용할때도 차이가 발생하는데

1.Literal 변수는 값을 직접 저장하므로 바로 사용 가능
2.Reference 타입의 변수는 참조하는 메모리 공간의 값을 읽는 과정을 거쳐야 한다.

2의 과정을 Unboxing이라 하고 반대의 과정은 Boxing이라 하는데 당연하게도 Literal을 사용할때보다 느리다.

Java에는 원시타입 8가지에 매핑되는 Wrapper Class가 정의되어 있는데 우리가 이 두가지를 마구 혼용해서 사용해도 문제가 되지 않는 이유는 리터럴/래퍼를 컴파일러가 자동으로 박싱 / 언박싱 해주기 때문이다.

int i = 0 ;
Integer j = i; //Integer 변수가 리터럴 변수 i를 참조해도 컴파일 에러가 발생하지 않는다.

Optional을 알아본다면서 조금 멀리까지 왔지만 하고싶은 말은 Wrapper class의 사용은 프로그램의 성능을 저하시킨다는 이야기다.(박싱, 언박싱 과정을 거치므로)

Optional Class ?

Integer가 리터럴 타입에 대한 Wrapper라면
Optional은 참조 타입에 대한 Wrapper이다.

public final class Optional<T> {

    private static final Optional<?> EMPTY = new Optional<>();
    private final T value;
    private Optional() {
        this.value = null;
    }
...
    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }
...    
    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }
...
    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }
...    
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
        if (value != null) {
            return value;
        } else {
            throw exceptionSupplier.get();
        }
    }
}
...

Optional 클래스의 구현부를 보면 제네릭 타입의 value 필드를 하나 가지고 있으며
value를 null로 초기화하는 기본생성자, value값을 받아 null일 경우 NPE(Null Pointer Exception)을 던지거나 초기화하는 생성자 2개가 정의되어있다. Java 8의 Stream API를 사용해서 특정 요소 하나를 가져오는 연산자를 사용하면(null 포인터의 발생 가능성이 있는 경우) Optional 타입이 반환되는데, 이 반환된 Optional을 언박싱하는 코드를 살펴보자. 다양한 메소드가 정의되어 있지만 오늘은 개발하면서 내가 자주 사용했었던 orElseGet, orElseThrow 두가지를 알아보자(사실 다른 메소드를 써볼 일이 없었다.)

class Product {
   private String name; //상품명
   private String mnftrdDt; //제조일자
   private long price; //가격
   
   public Product(String name){
   	this.name = name;
   }
   ...
}

위와 같은 Product 클래스가 있고, 아래와 같은 products List에서 targetDt에 제조일이 매칭되는 제품 하나를 가져와보자

    public static void main(String[] args) throws Exception {
        final String targetDt = "20220604";                        

		//먼저 가상의 상품 리스트를 하나 생성한다.
        List<Product> products;
        products = Stream.of(new Product("V1") 
                            ,new Product("V2") 
                            ,new Product("V3"))
                         .collect(Collectors.toList());
        products.get(0).setMnftrdDt("20220604"); //요소 하나를 매칭되게 만든다.
        
		//tagetDt에 매칭되는 제조일을 가진 제품을 filter하기 위한 Predicate 함수형 인터페이스를 정의한다.                         
        Predicate<Product> findMatchDt = prd -> targetDt.equals(prd.getMnftrdDt());
        
        // tagerDt에 매칭되는 제조일을 가진 제품을 하나 선택해보자, findFirst 연산은 Optional을 반환한다.
        Optional<Product> tgtPrdt = products.stream().filter(findMatchDt).findFirst();
        
        // Optional<Product>에서 언박싱을 통해 실제 객체를 가져와보자
        Supplier<Product> newProduct = Product::new; //Prodcut를 생성하는 Supplier
        Supplier<RuntimeException> newRTE = () -> new RuntimeException("상품정보가 존재하지 않습니다."); // RTE를 생성하는 Supplier

        Product result = tgtPrdt.orElseGet(newProduct); //매개변수로 default 객체 Supplier를 받는다
        result = tgtPrdt.orElseThrow(newRTE); //매개변수로 Throwable Supplier를 받는다.
    }

Optional안에는 null값이 존재할 가능성이 있는데, 일반적으로 우리에게 두가지 선택지가 주어진다.
1. null 대신 빈 객체 하나를 참조한다 ( orElseGet)
2. Exception을 던진다 (orElseThrow)

직접 null을 검사해서 무언가 다른 분기를 정할 수 있겠지만 이러면 Optional을 사용하는 의미가 없다(개인적인 생각이다)
정리하면
1.orElseGet은 같은 타입의 요소를 반환하는 Supplier를 매개변수로 받는다.( null대신 기본값의 사용)
2.orElseThrow는 Throwable을 구현한 요소를 반환하는 Supplier를 매개변수로 받는다. (예외처리 사용)

Optional이 뭔지 알았으니 마음껏 써볼까 ?

Optional을 이용해 Wrapping해서 반환하면 이 로직을 사용하는 사용자는 자연스럽게 null 발생 가능성을 염두에 두고
분기 또는 예외처리를 작성하면 되니 명시적이다. 또한 null체크 코드가 필요 없으니 사용하는 입장에서도 코드가 간결해진다. 그러면 이제 모든 Object Type을 Optional로 Wrapping해서 사용해볼까?

Optional을 남발하면 안되는 이유

일단 글 처음에 얘기했던 것 처럼
1. Wrapper는 박싱 / 언박싱 비용이 발생하므로 당연히 남발하면 안된다.
2. Optional을 return 타입이 아닌 다른 경우에 사용하면 null체크 로직이 들어가므로 가독성이 떨어지며, 중복으로 null 검사를 해야한다.
3. Serializable을 구현하지 않는다

Optional의 이런 특징은 범용적 사용보다는 제한적 상황에서 null가능성을 명시하고 null에 대한 처리를 매끄럽게 하기 위한 수단으로 만들어진 느낌을 받는다.

0개의 댓글