본문 바로가기
IT

코드로 이해하는 Java Stream API: `map` 연산 완벽 정복 (예제 중심)

by Sense. 2025. 6. 14.

목차

    자바 8부터 도입된 스트림 API(Stream API)는 컬렉션 데이터를 함수형 프로그래밍 스타일로 간결하고 효율적으로 처리할 수 있는 강력한 기능을 제공합니다. 그중에서도 map 연산은 스트림의 각 요소를 변환하여 새로운 스트림을 생성하는 데 핵심적인 역할을 합니다. 오늘은 이 map 연산에 대해 다양한 예제 코드를 통해 깊이 있게 알아보겠습니다.

    1. map이란 무엇일까요? 기본 개념 잡기

    스트림의 map 연산은 입력 스트림의 각 요소를 특정 함수(Function)에 적용하여, 그 반환값으로 이루어진 새로운 스트림을 생성하는 중간 연산(intermediate operation)입니다. 마치 공장에서 컨베이어 벨트를 따라오는 재료들을 하나씩 가공하여 새로운 제품으로 만드는 과정과 유사합니다.

    • 입력: Stream<T> (T 타입 요소들의 스트림)
    • 적용: Function<T, R> (T 타입 요소를 받아 R 타입 요소로 변환하는 함수)
    • 출력: Stream<R> (R 타입 요소들의 스트림)

    핵심은 "변환"입니다. 기존 요소를 다른 값이나 다른 타입의 요소로 매핑(mapping)하는 것이죠.

    2. 가장 기본적인 map 사용법: 숫자 리스트 변환

    가장 간단한 예제로 숫자 리스트의 각 요소를 제곱하는 경우를 살펴보겠습니다.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class MapExample1 {
        public static void main(String[] args) {
            List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    
            // 각 숫자를 제곱하여 새로운 리스트 생성
            List<Integer> squaredNumbers = numbers.stream()
                                                  .map(number -> number * number) // 각 숫자를 제곱으로 매핑
                                                  .collect(Collectors.toList());
    
            System.out.println("원본 숫자 리스트: " + numbers);
            System.out.println("제곱된 숫자 리스트: " + squaredNumbers);
            // 출력:
            // 원본 숫자 리스트: [1, 2, 3, 4, 5]
            // 제곱된 숫자 리스트: [1, 4, 9, 16, 25]
        }
    }

    위 코드에서 map(number -> number * number) 부분이 핵심입니다. 리스트의 각 number 요소에 대해 number * number 연산을 수행하고, 그 결과들을 모아 새로운 스트림을 만듭니다. 마지막으로 collect(Collectors.toList())를 통해 스트림을 다시 리스트로 변환합니다.

    3. 문자열 리스트 변환: 길이 구하기 및 대문자 변환

    이번에는 문자열 리스트를 다뤄보겠습니다. 각 문자열의 길이를 구하거나, 모든 문자열을 대문자로 변환하는 예제입니다.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class MapExample2 {
        public static void main(String[] args) {
            List<String> words = Arrays.asList("apple", "banana", "cherry");
    
            // 각 단어의 길이를 구하는 리스트
            List<Integer> wordLengths = words.stream()
                                             .map(String::length) // 메서드 참조 사용
                                             .collect(Collectors.toList());
            System.out.println("단어 길이 리스트: " + wordLengths);
            // 출력: 단어 길이 리스트: [5, 6, 6]
    
            // 각 단어를 대문자로 변환하는 리스트
            List<String> upperCaseWords = words.stream()
                                               .map(String::toUpperCase) // 메서드 참조 사용
                                               .collect(Collectors.toList());
            System.out.println("대문자 단어 리스트: " + upperCaseWords);
            // 출력: 대문자 단어 리스트: [APPLE, BANANA, CHERRY]
        }
    }

    여기서는 람다 표현식 대신 메서드 참조(String::length, String::toUpperCase)를 사용했습니다. s -> s.length()String::length와 동일하게 동작하며, 코드를 더 간결하게 만들어줍니다.

    4. 객체 리스트에서 특정 정보 추출하기

    map은 객체 리스트에서 원하는 정보만 추출하여 새로운 리스트를 만들 때 매우 유용합니다. 예를 들어, User 객체 리스트에서 사용자 이름만 뽑아내거나 나이만 추출하는 경우입니다.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    class User {
        private String name;
        private int age;
    
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public int getAge() {
            return age;
        }
    
        @Override
        public String toString() {
            return "User{name='" + name + "', age=" + age + "}";
        }
    }
    
    public class MapExample3 {
        public static void main(String[] args) {
            List<User> users = Arrays.asList(
                    new User("Alice", 30),
                    new User("Bob", 25),
                    new User("Charlie", 35)
            );
    
            // 사용자 이름만 추출하여 리스트 생성
            List<String> userNames = users.stream()
                                          .map(User::getName) // User 객체에서 이름(String)으로 매핑
                                          .collect(Collectors.toList());
            System.out.println("사용자 이름 리스트: " + userNames);
            // 출력: 사용자 이름 리스트: [Alice, Bob, Charlie]
    
            // 사용자 나이만 추출하여 리스트 생성
            List<Integer> userAges = users.stream()
                                          .map(User::getAge) // User 객체에서 나이(int)로 매핑
                                          .collect(Collectors.toList());
            System.out.println("사용자 나이 리스트: " + userAges);
            // 출력: 사용자 나이 리스트: [30, 25, 35]
        }
    }

    User 객체 스트림을 map(User::getName)을 통해 String 타입의 이름 스트림으로 변환하고, map(User::getAge)를 통해 Integer 타입의 나이 스트림으로 변환했습니다. 이처럼 map은 객체의 특정 필드 값을 추출하는 데 효과적입니다.

    5. map을 이용한 타입 변환: 문자열을 숫자로

    map 연산의 강력함은 단순히 값을 변경하는 것을 넘어, 요소의 타입을 변환할 수 있다는 점에서도 드러납니다. 예를 들어, 문자열로 된 숫자 리스트를 실제 숫자(Integer) 리스트로 변환할 수 있습니다.

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class MapExample4 {
        public static void main(String[] args) {
            List<String> numberStrings = Arrays.asList("10", "25", "50", "100");
    
            // 문자열 리스트를 Integer 리스트로 변환
            List<Integer> actualNumbers = numberStrings.stream()
                                                       .map(Integer::parseInt) // String을 Integer로 매핑
                                                       .collect(Collectors.toList());
    
            System.out.println("문자열 숫자 리스트: " + numberStrings);
            System.out.println("변환된 숫자 리스트: " + actualNumbers);
    
            // 변환된 숫자로 연산 수행
            int sum = actualNumbers.stream()
                                   .mapToInt(Integer::intValue) // IntStream으로 변환 (효율적)
                                   .sum();
            System.out.println("숫자들의 합: " + sum);
            // 출력:
            // 문자열 숫자 리스트: [10, 25, 50, 100]
            // 변환된 숫자 리스트: [10, 25, 50, 100]
            // 숫자들의 합: 185
        }
    }

    map(Integer::parseInt)를 통해 각 문자열 요소를 Integer 객체로 변환했습니다. 만약 기본형(primitive type)인 int 스트림이 필요하다면 mapToInt()와 같은 특화된 map 연산을 사용할 수 있으며, 이는 숫자 연산 시 오토박싱/언박싱 오버헤드를 줄여 성능상 이점을 가질 수 있습니다.

    6. map 사용 시 주의할 점 및 팁

    1. 반환 타입: map 연산에 전달되는 람다 표현식이나 메서드 참조는 반드시 값을 반환해야 합니다. 반환된 값들이 새로운 스트림의 요소가 됩니다.
    2. 새로운 스트림 생성: map은 원본 스트림(또는 컬렉션)을 변경하지 않고, 항상 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 불변성(immutability) 원칙과 맞닿아 있습니다.
    3. 지연 연산(Lazy Evaluation): map은 중간 연산이므로, 최종 연산(e.g., collect, forEach, sum)이 호출되기 전까지는 실제로 실행되지 않습니다.
    4. Null 처리: map 함수 내부에서 null을 반환하면, 결과 스트림에는 null 요소가 포함될 수 있습니다. 만약 매핑 과정에서 NullPointerException이 발생할 수 있는 코드가 있다면, 적절한 예외 처리나 Optional 사용을 고려해야 합니다.

    마무리

    오늘은 자바 스트림 API의 핵심 연산 중 하나인 map에 대해 다양한 예제 코드를 통해 알아보았습니다. map은 스트림의 각 요소를 변형하여 새로운 형태의 데이터를 만들어내는 강력하고 유연한 도구입니다. 컬렉션 데이터를 다룰 때 map을 적재적소에 활용하면 코드를 더욱 간결하고 표현력 있게 작성할 수 있습니다. 직접 다양한 시나리오에 map을 적용해보면서 그 편리함을 느껴보시길 바랍니다!

    • 트위터 공유하기
    • 페이스북 공유하기
    • 카카오톡 공유하기