GYUD-TECH

Stream 적용기 본문

기술-개발

Stream 적용기

GYUD 2023. 10. 30. 22:49

평소에 프로젝트를 진행할 때는 stream의 map 메서드만 사용하여 코드를 작성했다.

stream을 사용하면 코드를 더 간단하게 작성할 수 있다고 생각하여 이번 기회에 stream을 직접 사용하면서 공부하기로 하였고, 우테코 프리코스에 어떻게 stream을 적용하였고, 이 과정에서의 느낀점을 작성하였다.


Stream 적용 과정

이번 과제에는 자동차들이 이동 횟수에 따라 이동을 한 후, 우승자를 구해야 하는 기능 요구사항이 있었다.

아래 코드는 이름과 위치 정보가 담긴 자동차 객체들을 매개변수로 받아 우승 자동차의 이름을 구하는 코드이다.

 

먼저 차들의 위치 값들 중 가장 먼 위치를 구한 후, 해당하는 위치에 있는 자동차들의 이름을 얻는 방식으로 코드를 작성하였다.

    public static List<String> decideWinner(List<Car> cars) {
        List<Integer> carLocations = new ArrayList<>();
        List<String> winnerList = new ArrayList<>();

        for (Car car : cars) {
            carLocations.add(car.getLocation());
        }

        int farthestLocation = Collections.max(carLocations);

        for (Car car : cars) {
            if (car.getLocation() == farthestLocation) {
                winnerList.add(car.getName());
            }
        }
        return winnerList;
    }

 

코드를 읽어보면서 어떤 것을 바꿀 수 있을지 고민하였다.

 

코드에서는 for문이 두번이나 반복되기 때문에, map() 메서드를 활용하여 코드를 수정하였다.

    public static List<String> decideWinner(List<Car> cars) {
        List<Integer> carLocations = cars.stream().map(Car::getLocation)
                .toList();
        int farthestLocation = Collections.max(carLocations);
        return cars.stream().map(car -> {
            if (car.getLocation() == farthestLocation) {
                return car.getName();
            }
            return null;
        }).toList();
    }

 

하지만, 위의 코드는 두번째 stream의 사용에서 문제가 있었다. map 메서드를 사용하면 if 조건에 성립하지 않더라도 한가지 값을 반환해야 하기 때문에 우승자 명단에 null 값이 포함되었다. 조건의 참/거짓 여부를 확인하여 참인 객체만 포함하는 함수가 필요하였다.

 

IntelliJ의 도움을 받아 stream API가 제공하는 메서드 중 filter 메서드를 찾을 수 있었고 이를 적용하여 코드에 적용하였다.

    public static List<String> decideWinner(List<Car> cars) {
        List<Integer> carLocations = cars.stream().map(Car::getLocation)
                .toList();
        int farthestLocation = Collections.max(carLocations);
        return cars.stream()
                .filter(car -> isWinner(car, farthestLocation))
                .map(Car::getName)
                .toList();
    }

    private static boolean isWinner(Car car, int farthestLocation) {
        if (car.getLocation() == farthestLocation) {
            return true;
        }
        return false;
    }

 

다음으로는, 중요하지 않은 carLocations 변수를 없에고 바로 최장 거리를 구하고 싶어 stream API의 메서드를 찾아보았다.

 

map() 메서드는 일반적인 상황에 사용하기 위하여 Stream<T> 타입을 리턴한다. 다양한 타입을 지원하기 때문에 그만큼 타입 맞춤형 메서드를 제공하지 못한다는 단점이 있다. 지금 나에게 필요한 것은 최댓값을 구하는 것이기 때문에 IntStream을 반환하는 mapToInt()를 사용하고, IntStream의 max() 메서드를 사용하여 최대값을 구하고자 하였다.

    public static List<String> decideWinner(List<Car> cars) {
        int farthestLocation = cars.stream()
                .mapToInt(Car::getLocation)
                .max()
                .orElseThrow(() -> new IllegalArgumentException());

        return cars.stream()
                .filter(car -> isWinner(car, farthestLocation))
                .map(Car::getName)
                .toList();
    }

 

max() 메서드는 IntStream내의 요소가 없을 경우 OptionalInt를 반환한다. IntStream이 비어있다는 뜻은 car를 담은 리스트 cars가 비어있다는 뜻이기 때문에 예외처리를 통해 처리하였다.

 

결국 decideWinner() 메서드는 최장 거리를 구하고, 최장 거리를 간 차의 이름을 구하는 두가지 역할을 수행한다. 따라서 최장 거리를 구하는 기능을 분리하여 아래와 같은 최종 코드를 작성하였다.

    public static List<String> decideWinner(List<Car> cars) {
        return cars.stream()
                .filter((car) -> isWinner(car, measureFarthestLocation(cars)))
                .map(Car::getName)
                .toList();
    }

    private static int measureFarthestLocation(List<Car> cars) {
        return cars.stream()
                .mapToInt(Car::getLocation)
                .max()
                .orElseThrow(() -> new IllegalArgumentException());
    }

    private static boolean isWinner(Car car, int farthestLocation) {
        if (car.getLocation() == farthestLocation) {
            return true;
        }
        return false;
    }

 


마무리

위의 코드에서 보면 Stream이 항상 좋은 것 같지만 무작정 for문을 Stream으로 바꾸는 것만이 정답은 아니다. 아래 적힌 Stream의 특징을 잘 고려하여 적절한 상황에 사용하는 것이 좋을 것이다.

 

Stream의 특징

-  Stream은 한번 사용하고 나면 재사용이 불가능하다.

- Stream은 데이터 소스로부터 값을 읽어와서 새로운 Stream을 생성하는 것이지 데이터 소스를 변경하지 않는다.

- Stream은 Lazy Loading 방식으로 중간연산에서 값이 필요하지 않다면 최종 연산에서 값을 계산한다.

 

이러한 특징들이 있기 때문에 값을 변경한다거나, stream값을 저장해야 할 때는 stream을 사용하지 않는 것이 더 좋을 것 같다. 또한 break나 continue와 같이 반복문을 세부적으로 제어하기 위해서는 적절하지 않다.

느낀점

이전에도 Stream을 공부했었지만 이론으로만 공부하다보니 기억에 남지않고 금방 지워졌던 것 같다. 프리코스를 통해 직접 Stream을 사용하면서 Stream의 장점을 직접 느낄 수 있었고, 단점에는 어떤 것이 있을지 고민할 수 있었다.

 

Stream의 가장 큰 장점은 연결을 통한 가독성 향상이다. "Stream" 이라는 단어는 "개울"을 의미하며, 개울물이 위에서 아래로 연속해서 흐르는 것처럼 Stream 연산도 연속해서 이어붙일 수 있다. Stream 중간 연산의 반환값이 Stream이기 때문에 연산을 이어붙이는 것이 가능한 것이다. 이러한 연결성은 불필요한 변수 선언이나 길어질 수 있는 코드를 줄여주어 가독성을 향상시킨다.

 

추가적으로 Stream을 적용하며 리팩토링하는 과정에서 단위 테스트의 힘을 느낄 수 있었다. 앞서 소개한 코드 중 우승자 목록에 null 값이 포함되는 경우는 직접 출력해서 확인하지 않으면 끝까지 확인할 수 없었을 것이다. 그렇다고 처음부터 완벽하게 역할이 분리된 메서드를 작성하는 것은 어렵고 시간이 오래 걸린다. 코드 작성 - 테스트 - 리팩토링 - 테스트 순서의 코드 작성법이 복잡한 코드를 정확하고 빠르게 작성하는 가장 좋은 방법이라고 생각한다.

 

앞으로

이어지는 과제에서도 Stream과 단위테스트를 적재적소에 사용하면서 가독성이 좋은 코드를 짜기 위해 고민할 것이다. 이번 과제를 통해서, 반복문을 작성할 때 Stream이라는 좋은 무기를 얻어서 뿌듯하다.