ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 17. 자바 스트림
    스프링개발자/201 - 일반 2020. 9. 7. 04:29

    자바8부터 쓸 수 있는 Java Stream. 이게 무엇이고 어떠한 장점이 있는지, 그리고 꼭 써야할지 생각해본다.


    1. 자바 스트림이 무엇인가?

    Introduced in Java 8, the Stream API is used to process collections of objects. 
    A stream is a sequence of objects that supports various methods which can 
    be pipelined to produce the desired result.
    
    >>> 자바 8에서 소개된, 스트림 API는 List등의 collection 객체를 다루는데 유용하게 쓰인다.
    >>> 스트림이란 연속된 하나이상의 객체인데, 스트림은 여러 메소드를 지원하며
    >>> '파이프라인'이용하여 개발자가 원하는 결과를 얻도록 도와준다. (의역)
    • 스트림은 자료구조의 한 종류가 아니다. 여러 구조로부터 입력을 받을 수 있다.
    • 스트림은 자료구조를 바꾸지 않는다. 파이프라인을 새로 만들어 결과값을 뽑아낸다.
    • 스트림의 intermediate operation은 호출시에 계산이 되어 결과값을 스트림으로 반환한다. 따라서 한개 이상의 intermediate operations가 동시에 파이프라인으로 존재할 수 있다. Terminal operations은 스트림을 끝내고 결과값을 반환한다.

     

    2. 자바 스트림의 장점

    자바 스트림을 쓰면, 기존의 for 혹은 foreach 블록을 가독성 좋게 바꿀 수 있다.

    혹자는 스트림을 쓰게되면, 성능을 저하시킨다고 말하지만, 거의 미묘한 정도이며, 가독성을 향상 시키는 장점이

    유지보수성또한 좋게 만들기 때문에, 스트림은 웬만하면 쓰는 것이 좋은거 같다.

    스트림을 쓰지 않는 사람을 욕할 필요는 없지만, 나는 쓸 수 있다면 꼭 쓰는 편이다.

    기존 203 카테고리의 스프링Validation(2ndprince.tistory.com/36)에서 썼던 코드를

    스트림버전과 non-스트림 버전으로 비교해보자.

     

    아래부터 나오는 소스코드는 여기에;

    https://github.com/2ndPrince/routeOptimization/tree/spring_validation_3

     

    다음은 기존에 작성한 코드이다.

    DispatchService.java의 location 유효성 검사 메소드를 살펴보자.

    public void validateLocations(Location location, List<ValidationError> errors){
            Double longitude = location.getLongitude();
            Double latitude = location.getLatitude();
    
            boolean isLonValid = (-180d <= longitude) && ( longitude <= 180d);
            boolean isLatValid = (-90d <= latitude) && ( latitude <= 90d);
    
            if(!isLatValid) errors.add(new ValidationError(ValidationError.Type.BAD_RIDE, "Location", "Invalid Lat Location. Check your Lat", 1, latitude.toString()));
            if(!isLonValid) errors.add(new ValidationError(ValidationError.Type.BAD_RIDE, "Location", "Invalid Lon Location. Check your Lon", 1, longitude.toString()));
        }

    위의 메소드(validateLocations)를 스트림을 써서 requst를 검사하는 로직을 짜면 이렇다.

    @Service //로직1
    public class DispatchService {
        public void validate(DispatchRequest request) {
    
            List<ValidationError> errorList = new ArrayList<>();
    
            request.getRides().stream()
                    .flatMap(r -> Stream.of(r.getPickup(), r.getDropOff()))
                    .collect(Collectors.toSet())
                    .forEach(location -> validateLocations(location, errorList));
    
            if(!errorList.isEmpty()){
                throw new ValidationException(errorList, request.getId());
            }
        }

    스트림을 쓰지 않고 표현하면 이렇다.

    @Service //로직2
    public class DispatchService {
        public void validate(DispatchRequest request) {
    
            List<ValidationError> errorList = new ArrayList<>();
    
            List<Location> locationList = new ArrayList<>();
            List<RideRequest> rides = request.getRides();
            for(RideRequest ride: rides){
                locationList.add(ride.getPickup());
                locationList.add(ride.getDropOff());
            }
            for(Location location: locationList){
                validateLocations(location, errorList);
            }
    
            if(!errorList.isEmpty()){
                throw new ValidationException(errorList, request.getId());
            }
        }

    스트림을 쓰지 않더라도 조금 더 줄이면, 충분히 간편하고 성능면에서는 더 좋다.

    @Service //로직3
    public class DispatchService {
        public void validate(DispatchRequest request) {
    
            List<ValidationError> errorList = new ArrayList<>();
    
            List<RideRequest> rides = request.getRides();
            for(RideRequest ride: rides){
                validateLocations(ride.getPickup(),errorList);
                validateLocations(ride.getDropOff(),errorList);
            }
    
            if(!errorList.isEmpty()){
                throw new ValidationException(errorList, request.getId());
            }
        }

    로직1과 로직3을 놓고 본다면, 어느 하나가 다른것보다 무조건 좋다고 말하기 어렵다.

    그럼에도, 앞서 언급했지만, 가독성과 유지보수성을 위해서 조금의 성능은 포기하는것이 좋다.

     

    3. 자바 유틸 Arrays.asList

     

    DispatchServiceTest.java에 다음의 테스트를 추가한다.

    @Test
        public void test_wholeService(){
            Location okayLocation1 = new Location(19.0, -35.2);
            Location okayLocation2 = new Location(20.0, -35.0);
            Location badLocation1 = new Location(900.0, -35.0);
            Location badLocation2 = new Location(20.0, -235.0);
            RideRequest rideId1 = RideRequest.builder().rideId("rideId1").pickup(okayLocation1).dropOff(okayLocation2).build();
            RideRequest rideId2 = RideRequest.builder().rideId("rideId2").pickup(okayLocation1).dropOff(badLocation2).build();
            RideRequest rideId3 = RideRequest.builder().rideId("rideId3").pickup(badLocation1).dropOff(badLocation2).build();
    
            VehicleRequest vehicles = new VehicleRequest();
    
            DispatchRequest sampleRequest = DispatchRequest.builder()
                    .id("request1")
                    .rides(Arrays.asList(rideId1,rideId2,rideId3))
                    .vehicles(Arrays.asList(vehicles)).build();
    
    
            assertThrows(ValidationException.class, () -> {
                dispatchService.validate(sampleRequest);
            });
        }

    Arrays.asList는 자바유틸에서 제공하는 클래스인데, sorting 및 searching또한 제공한다. 

    자바유틸을 써서 request를 좀 더 깔끔하게 구현한다.

     

    4. 자바 스트림 연습

    스트림은 크게 다음과 같이 나뉜다

    1) 스트림 생성

    2) 데이터 가공(Intermediate operations) : map, filter, sorted

    3) 하고 싶은거 하기(Terminal operations): collect, forEach, reduce

     

    5. filter 연습

    if 처럼 해당 조건에 맞는 경우에만 선택하고 나머지 값들은 필터 아웃 시킨다.

    @Test
    public void stream_filter_one() {
      List<Integer> integers = Arrays.asList(5, 3, 2, 9, 10);
      List<Integer> collect = integers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());
      // 짝수만 남기고 나머지 필터
    
      assertTrue(collect.contains(Integer.valueOf(2)));
      assertTrue(collect.contains(Integer.valueOf(10)));
      assertFalse(collect.contains(Integer.valueOf(9)));
      assertEquals(2, collect.size());
    }
    @Test
    public void stream_filter_two() {
      List<String> strings = Arrays.asList("strawberry", "melon", "apple", "banana", "fruit");
      List<String> collect = strings.stream().filter(s -> s.length() == 5).collect(Collectors.toList());
      // 문자열 길이가 5인 것만 남기고 나머지 필터
    
      assertTrue(collect.contains("melon"));
      assertTrue(collect.contains("apple"));
      assertTrue(collect.contains("fruit"));
      assertEquals(3, collect.size());
    }
    @Test
    public void stream_filter_three() {
            List<String> strings = Arrays.asList("strawberry", "melon", "apple", "banana", "fruit");
            List temp = new ArrayList();
            strings.stream().filter(s -> startWith_sma(s)).collect(Collectors.toList()).forEach(l -> {
                String s = l.toUpperCase();
                temp.add(s);
                System.out.println(s);
                });
            // 's','m','a'로 시작하는 문자열만 선택하고 나머지 필터
            // forEach를 통해 block of code를 넣을 수 있다.
            assertEquals(3, temp.size());
            assertTrue(temp.contains("MELON"));
        }
    
        private boolean startWith_sma(String s) {
            boolean s1 = s.startsWith("s");
            boolean s2 = s.startsWith("m");
            boolean s3 = s.startsWith("a");
    
            return (s1 || s2 || s3);
        }

     

    6. map,sorted 연습

    각 객체의 타입이나 값을 바꾼다.

    @Test
    public void stream_map_one() {
        List<Integer> integers = Arrays.asList(5, 3, 2, 9, 10);
        List<Integer> collect = integers.stream().map(n -> n * n).collect(Collectors.toList());
        // 값을 제곱해서 모은다.
    
        assertTrue(collect.contains(Integer.valueOf(25)));
        assertTrue(collect.contains(Integer.valueOf(9)));
        assertFalse(collect.contains(Integer.valueOf(10)));
        assertEquals(5, collect.size());
    }
    @Test
    public void stream_map_two() {
        List<String> strings = Arrays.asList("strawberry", "melon", "apple", "banana", "fruit");
        List<Integer> collect = strings.stream().map(s -> sumStringAsChar(s)).collect(Collectors.toList());
        assertEquals(sumStringAsChar("strawberry"), collect.get(0).intValue());
    }
    
    private int sumStringAsChar(String s) {
        int sum = 0;
        for (int i = 0; i < s.length(); i++) {
            sum = sum + s.charAt(i);
        }
        return sum;
    }
    @Test
    public void stream_sorted_one() {
        List<String> strings = Arrays.asList("strawberry", "melon", "apple", "banana", "fruit", "pineapple");
        List<Integer> collect_asc = strings.stream().map(s -> sumStringAsChar(s))
        .sorted().collect(Collectors.toList());
        
        List<Integer> collect_desc = strings.stream().map(s -> sumStringAsChar(s))
        .sorted(Comparator.comparing(Integer::intValue).reversed()).collect(Collectors.toList());
        
        assertTrue(collect_asc.get(0).equals(collect_desc.get(collect_desc.size() - 1)));
        System.out.println(collect_asc.toString());
        System.out.println(collect_desc.toString());
    }
    
    private int sumStringAsChar(String s) {
        int sum = 0;
        for (int i = 0; i < s.length(); i++) {
            sum = sum + s.charAt(i);
        }
        return sum;
    }

    참고

    1) www.geeksforgeeks.org/stream-in-java/#:~:text=Introduced%20in%20Java%208%2C%20the,to%20produce%20the%20desired%20result.&text=A%20stream%20is%20not%20a,Arrays%20or%20I%2FO%20channels.

     

    완성된 코드

    github.com/2ndPrince/routeOptimization/tree/practice-stream-1

     

     

     

    다음 스트림 연습 (작성중)

    noneMatch

    anyMatch

Designed by Tistory.