티스토리 뷰

반응형

저번에는 성능 향상과 Stream에 동작 순서에 대해 알아보았다면 이제는 Optional에 대해 알아보자

 

Stream은 지연 처리를 통해 연산을 하여 값을 도출해낸다. 그렇다면 정확한 이유와 흐름에 대해 파악해보자

 

Stream 지연 처리

예제 코드를 먼저 살펴보자

public class StreamLazyCal {

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7};

        int sum = Arrays.stream(arr)
                .filter(n -> n % 2 == 1)
                .sum();

        System.out.println(sum);
    }
}

 

 

이 예제 코드는 배열을 filter를 통해 홀수만 걸러내어 그 합을 출력하는 코드이다. Stream은 지연 처리 방식으로 동작하기 때문에 최종 연산이 수행되기 전까지 중간 연산이 수행되지 않는다. 

 

sum이 호출 될 때까지 filter의 호출 결과는 스트림에 아무런 영향을 주지 않는다. 그러다가 최종 연산 함수인 sum이 호출되면 그때 호출 결과가 Stream에 반영되어 결과가 출력되는 것이다.

 

지연 처리로 인해 불필요한 연산을 피할 수 있다는 장점이 존재한다. 전 글에 자세히 나와 있지만 여기서 다시 설명하였으니 여기선 생략하겠다.

 

Eager Evaluation
Lazy Evaluation

위 두 개의 애니메이션을 보면 더 이해가 쉬울 것이다. 즉시 처리를 한다면 연산을 하여 중간 연산에서 나온 요소들을 담은 다음에 영역 크기만큼을 출력하겠지만 Stream을 사용하면 결과를 출력할 때 연산이 진행되어 딱 필요한 만큼의 연산만 할 수 있는 것이다.

 

Null-Safe 한 Stream

자바 개발자라면 항상 Null-safe에 대한 고민을 해왔을 것이다. Null로 인해 NullPointerException이 발생한다면 개발자 입장에선 매우 골치 아픈 상황이다. 그렇기에 null 여부를 검사하는 코드를 작성한다면 지저분해질 수 있기에 Stream으로 그런 것을 최대한 방지해 볼 거다.

 

우선 코드를 보자!!

public class NullSafeStream {

    public static <T> Stream<T> collectionToStream(Collection<T> collection) {
        return Optional
                .ofNullable(collection).stream()
                .flatMap(Collection::stream);
    }

    public static void main(String[] args) {
        List<Integer> list = null;
        collectionToStream(list)
                .forEach(System.out::println);
    }
}

위 예제는 null인 List 배열을 넘겨주었지만 NPE가 발생하지 않고 정상적으로 동작한다. (꼭 직접 컴파일 해보길 추천한다.) 이렇게 우리는 Optional을 이용해 Null로부터 안전한 깔끔한 코드를 작성할 수 있다. 

 

하지만 Optinal은 코드의 가독성을 높여주지만 Wrapper 클래스를 사용하는 것일 뿐이다. Stream이 생성해야 하는 대상이 Null이 발생할 확률이 높을 때만 적용해야 한다는 것에 주의하자 무의미한 Optional 남발은 결코 바람직한 코드가 아니다. 

 

그렇다면 마지막으로 줄여 쓰기에 대해 알아보자 

 

Stream 줄여쓰기

Lamda도 마찬가지지만 Stream을 줄여 써 가독성 좋은 코드를 작성할 수 있다. 위에서 볼 수 있는 stream도 가독성을 높인 코드라고 할 수 있다.

 

그럼 바로 코드를 비교하며 설명하겠다.

public class StreamCode {

    public static void main(String[] args) {

        // Stream
        List<Integer> collectWithStream =
                Stream.of(1, 16, 16, 7, 18, 19, 10, 11, 23, 45)
                        .sorted(Comparator.reverseOrder()).filter(i -> i > 10)
                        .filter(i -> i % 2 == 1)
                        .collect(Collectors.toList());


        // for-loop
        int[] arr = {1, 16, 16, 7, 18, 19, 10, 11, 23, 45};

        List<Integer> collectWithFor = new ArrayList<>();

        Arrays.sort(arr);

        for (int i : arr) {
            if (i > 10 && i % 2 == 1) {
                collectWithFor.add(i);
            }
        }
    }
}

위 코드는 동일한 동작을 하는 코드이다. 정렬 후 배열 요소들을 10보다 크고 홀수인 값을 모아 List배열로 만들어주는 동작을 한다. 

 

하지만 같은 동작이지만 코드의 가독성은 훨씬 좋아진다. 첫 번째로 현재 이 코드가 무슨 동작을 할 것인지 명확하게 보여준다. filter를 통해 여기서 중간에 값을 조건에 따라 필터링해준다는 것을 알려주고 sorted()로 정렬해주고 collect로 뭔갈 모아서 만들어준다는 것을 명확하게 알 수 있다.

 

for문도 물론 for-each문을 사용해서 당장은 보기 쉬울 순 있지만 더 코드가 길어지거나 요소가 많아진다면 Stream을 쓰는 편이 헷갈리지도 않고 보기 좋은 코드를 만들 수 있을 것이다.

 

이것이 Stream이 갖는 특별한 장점이다. 하지만 성능은 보장할 수 없으니 충분히 고려 후 알맞은 상황에 Stream을 사용하는 것을 추천한다.

 

자 이제 Stream이 끝났다.

 

자 아직 한 가지가 더 남았다. Null-Safe Stream에서 Optional에 대해 잠깐 봤을 것이다. 그렇다면 이젠 Optional에 대해 알아보자

 

Null이 끼치는 악영향

자 한 예제를 보면서 설명해 보겠다.

public class Person {
    
    private Car car;

    public Car getCar() {
        return car;
    }
}

public class Car {

    private Insurance insurance;

    public Insurance getInsurance() {
        return insurance;
    }
}

public class Insurance {

    private String name;

    public String getName() {
        return name;
    }
}

이 코드에서 만약에 이러한 메서드가 실행된다고 생각해보자

 

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}

만약 차를 갖고 있지 않다면 null을 뱉어낼 것이다. 그리고 이러한 null 은 다음과 같은 문제를 야기한다.

 

  • 에러의 근원이다 : NullPointerException을 발생시킨다.
  • 코드를 어지럽힌다 : 때로는 중첩된 null 확인 코드를 추가해야 하므로 null 때문에 코드 가독성이 떨어진다. 
  • 아무 의미가 없다 : null은 아무 의미가 없는 값이다. 정적 형식 언어에서 이러한 표현은 적절하지 않다.
  • 자바 철학에 위배된다 : 자바는 개발자로부터 모든 포인터를 숨겼다. 하지만 예외가 있는데 그것이 바로 null이다. 
  • 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 할당할 수 있다.  이런 식으로 여러 시스템으로 null이 퍼지면 어떤 의미로 사용되었는지 알 수 없다.

자바는 이러한 문제를 해결하기 위해 Java 8부터 Optional이라는 클래스를 제공하기 시작했다. 그렇다면 사용법에 대해 알아보자.

 

Null 대신 Optional 

Optional은 null 대신에 Optinal.empty 메서드로 Optional을 반환한다. Optional.empty는 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드이다. 

 

이 것을 그림으로 보자면 

이렇게 표현할 수 있다. 이것은 null이 아닌 값이 없는 Optional인데 일단 이렇게 알고 다음을 보자

 

public class Person {

    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }
}

public class Car {

    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}

public class Insurance {

    private String name;

    public String getName() {
        return name;
    }
}

자 Null이 될 수 있는 요소들은 모두 Optional로 감싸주었다. 이로써 Null로부터 안전해졌을 뿐만 아니라 사람이 자동차를 소유하지 않을 수도 있다. 보험 가입이 안되어있을 수 있다는 것을 명확히 설명해 주어 알기 쉬운 코드가 되었다.

 

이렇게 Optional은 Null-Safe 한 역할도 있지만 더 이해하기 쉽게 API를 설계하도록 돕고 상황에 대해 개발자가 대응할 수 있게 도와준다.

 

Optional 적용 패턴

이 예제 또한 생성하고 만들기 값을 꺼내 오는 단계로 메서드를 설명할 것이다. 

public class OptionalCreate {

    public static void main(String[] args) {
        // 빈 Optional 생성
        Optional<Integer> opInt = Optional.empty();
        
        // null이 아닌 Optional 생성
        String str = "Hello";
        Optional<String> notNull = Optional.of(str);
        
        // null인 값으로 Optional 생성
        String nullStr = null;
        Optional<String> opNull = Optional.ofNullable(nullStr);
    }
}

empty() 함수를 통해 빈 Optional 객체를 얻을 수 있고, of() 함수를 통해 특정한 null이 아닌 Optional 객체를 만들 수 있다. 마지막으로 ofNullable() 함수를 통해 null인 값으로 Optional 객체를 생성한다.

 

그러면 이제 Optional의 값을 추출하고 변환해보자

 

보통 객체의 정보를 추출할 때 이 값이 null인지 확인해야 한다. 

String name = null;

if(insurance != null) {
    name = insurance.getName();
}

Optional은 이러한 패턴에 사용할 수 있도록 map 메서드를 지원한다. 

public class OptionalExtraction {

    public static void main(String[] args) {
    	// 이전 코드 ..
        
        Optional<Insurance> insuranceOp = Optional.of(insurance);
        Optional<String> name = insuranceOp.map(Insurance::getName);
    }
}

Optional의 map 메서드는 Stream map 메서드와 개념적으로 비슷하다고 할 수 있다. Optional이 비어있으면 아무 일도 일어나지 않지만 값이 있다면 해당 타입에 맞는 Optional을 반환해 준다.

 

자 그럼 map을 이용하여 기존에 보험사 이름을 연결하는 메서드를 재구현 해보자

 

기존 코드 

public Optional<String> getInsuranceName(Person person) {
        Optional<Person> optPerson = Optional.of(person);

        return optPerson
        	.map(Person::getCar)
                .map(Car::getInsurance)
                .map(Insurance::getName);
    }

하지만 위 코드는 컴파일되지 않는다. optPerson의 형식은 Optional <Person>이므로 map 메서드를 호출할 수 있지만 getCar는 Optional <Optional <Car>>형식을 반환한다 그렇기 때문에 map으로는 풀어낼 수가 없다.

 

그렇다면 이것을 우린 어떻게 해결할까? 우린 이미 답을 알고 있다. 그건 바로 전에 학습했었던 flatMap이다. flatMap은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 반환해준다는 사실을 기억할 것이다. 바로 이 메서드를 이용해 해결할 것이다.

public Optional<String> getInsuranceName(Person person) {
        Optional<Person> optPerson = Optional.of(person);

        return optPerson.flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName);
}

 

자 그러면 Optional을 바꿔줄 때가 왔다.

 

이때 우리는 orElse 함수를 사용해서 값을 Optional에서 String 객체를 꺼내올 것이다. 

 

public String getInsuranceName(Person person) {
        Optional<Person> optPerson = Optional.of(person);

        return optPerson
                .flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName)
                .orElse("Unknown");
    }

 

여기서 orElse()는 Null일 경우엔 "Unknown"을 반환할 것이고 그게 아니라면 Optional에 감싸져 있는 값을 반환할 것이다.

 

뿐만 아니라 null을 반환하는 대신에 Exception을 던질 수도 있다. 한번 코드를 보자

public String getInsuranceName(Person person) {
        Optional<Person> optPerson = Optional.of(person);

        return optPerson
                .flatMap(Person::getCar)
                .flatMap(Car::getInsurance)
                .map(Insurance::getName)
                .orElseThrow(() -> new IllegalArgumentException("개체가 존재하지 않습니다."));
    }

이런 식으로 오류를 던져 null이 들어왔을 때 예외를 발생시켜 null 값이 더 이상 다른 객체로 이동하는 것을 막을 수 있다.

 

 

자 Optional에 대한 설명까지 모두 마쳤다. Stream부터 Optional까지 자바에서 가독성과 Null-safe 한 문제를 효과적으로 다룰 수 있는 좋은 API 지만 주의하여 사용하자!!

 

Reference.

https://codippa.com/optional-in-java-8-with-example/

 

Java 8 Optional with example

Learn what is Optional class in java, how to create it using of, ofNullable and empty methods. Also learn about or, orElse, orElseThrow methods.

codippa.com

https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/

 

Java 스트림 Stream (2) 고급

이전 포스트에 이어서 Java 8의 스트림(Stream)을 살펴봅니다. 자바 8 스트림은 총 두 개의 포스트로, 기본적인 내용을 총정리하는 이전 포스트와 좀 더 고급 내용을 다루는 이번 포스트로 나뉘어져

futurecreator.github.io

https://velog.io/@new_wisdom/Java-Stream-1

 

[Java] Stream 부수기

[Java] Stream을 부숴봅시다 👀

velog.io

https://mangkyu.tistory.com/115

 

[Java] Stream API의 활용 및 사용법 - 고급 (4/5)

1. FlatMap을 통한 중첩 구조 제거 [ FlatMap이란? ] 만약 우리가 처리해야 하는 데이터가 2중 배열 또는 2중 리스트로 되어 있고, 이를 1차원으로 처리해야 한다면 어떻게 해야 할까? 이러한 경우에 map

mangkyu.tistory.com

 

[도서] 모던 자바 인 액션 : Optional

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함