본문 바로가기
쿤즈 Dev/Java

[자바 스트림 2] map으로 요소 변환하기 — 리스트를 한 방에 갈아끼우기

by Koonz:) 2026. 10. 18.
728x90
요약: map은 스트림의 각 요소를 다른 값으로 1:1 변환하는 연산입니다. 이름을 대문자로, 객체를 특정 필드로, 문자열을 길이(숫자)로 — "입력 하나당 출력 하나"가 핵심입니다. 이 글에서 map의 기본 사용법, for 루프 대비 장점, 그리고 메서드 참조(String::toUpperCase) 와 "개수는 그대로 유지된다"는 특성까지 정리합니다.

 


스트림을 만들었으면 다음은 가공입니다. 그중 가장 많이 쓰는 연산이 바로 map이죠. "상품 리스트에서 이름만 뽑아라", "가격에 부가세를 붙여라", "DTO를 엔티티로 바꿔라" 같은 요구가 전부 map 한 줄로 끝납니다.

 

면접에서도 "map과 filter의 차이를 설명하라" 는 질문이 단골입니다. 한마디로 filter는 요소를 거르고(개수가 줄 수 있음), map은 요소를 바꿉니다(개수는 그대로). 이 차이를 코드로 확실히 잡아 보겠습니다.

 

자바 스트림 map 변환 각 요소를 다른 값으로 일대일 매핑

 

문제 정의

 

문제. 소문자 이름 리스트가 주어진다. ① 모두 대문자로 바꾼 리스트와 ② 각 이름의 글자 수 리스트를 각각 만들어라.

 

  • 입력: List<String> names = ["kim", "lee", "park"]
  • 기대 출력:
  • 대문자: [KIM, LEE, PARK]
  • 길이: [3, 3, 4]

 

전통 방식(for-loop) — 그리고 스트림이 나은 이유

 

스트림이 없다면 결과를 담을 빈 리스트를 미리 만들고, 루프를 돌며 하나씩 변환해 add 해야 합니다.

 

import java.util.ArrayList;
import java.util.List;

public class Traditional {
    public static void main(String[] args) {
        List<String> names = List.of("kim", "lee", "park");

        List<String> upper = new ArrayList<>();
        for (String name : names) {
            upper.add(name.toUpperCase());   // 변환해서 새 리스트에 담기
        }
        System.out.println(upper);
    }
}

 

동작은 하지만 "빈 리스트 생성 → 루프 → add" 라는 상투적인 뼈대가 매번 반복됩니다. 정작 중요한 로직(toUpperCase)은 이 뼈대 속에 묻히죠. map은 이 과정을 "각 요소를 이렇게 바꿔라" 한 줄로 압축합니다.

 

스트림 해법 — map

 

자바 스트림 map 요소 변환 대문자 길이 매핑

 

import java.util.List;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<String> names = List.of("kim", "lee", "park");

        // ① 각 이름을 대문자로 (String -> String)
        List<String> upper = names.stream()
                                  .map(String::toUpperCase)
                                  .collect(Collectors.toList());
        System.out.println(upper);

        // ② 각 이름을 길이로 (String -> Integer)
        List<Integer> lengths = names.stream()
                                     .map(String::length)
                                     .collect(Collectors.toList());
        System.out.println(lengths);
    }
}

 

한 줄씩 설명

 

  • names.stream() — 리스트에서 스트림을 엽니다(1편 참고).
  • .map(String::toUpperCase) — 각 요소에 toUpperCase()를 적용해 새 스트림으로 흘려보냅니다. String::toUpperCase는 람다 s -> s.toUpperCase()의 메서드 참조 축약형입니다.
  • .map(String::length) — 이번엔 String을 받아 Integer를 반환합니다. 이렇게 map은 입력 타입과 출력 타입이 달라도 됩니다(String → Integer).
  • .collect(Collectors.toList()) — 변환된 요소들을 다시 리스트로 모읍니다(5편에서 자세히 다룹니다).

 

실행 결과

 

[KIM, LEE, PARK]
[3, 3, 4]

 

변형·함정·주의

 

  • map은 개수를 바꾸지 않는다. 입력 3개면 출력도 정확히 3개입니다. "거르기"가 필요하면 map이 아니라 filter(3편)입니다.
  • 숫자 변환엔 mapToInt가 유리. map(String::length)는 Stream<Integer>를 만들지만, 바로 합계를 낼 거라면 mapToInt(String::length)로 IntStream을 만들어 .sum()을 바로 쓰는 편이 박싱 오버헤드가 없습니다(6편 참고).
  • 중첩 구조엔 map이 아니라 flatMap. 한 요소가 여러 값으로 펼쳐져야 한다면(예: 문장 → 단어들) map이 아니라 flatMap을 씁니다(11편).
  • 메서드 참조 4종. String::toUpperCase(인스턴스 메서드), Integer::parseInt(정적 메서드), User::new(생성자), obj::method(특정 객체 메서드) — 람다를 더 짧게 쓰는 문법으로 익혀 두면 좋습니다.

 

관련 면접 질문

 

  • Q1. map과 filter의 차이는?

 

→ map은 각 요소를 다른 값으로 변환(개수 유지), filter는 조건에 맞는 요소만 선택(개수 감소 가능). map은 Function, filter는 Predicate를 인자로 받습니다.

 

  • Q2. map과 flatMap의 차이는?

 

→ map은 요소 하나를 값 하나로(1:1), flatMap은 요소 하나를 스트림(0..N개) 으로 펼친 뒤 평탄화합니다(1:N). 리스트의 리스트를 한 겹으로 펼 때 flatMap을 씁니다.

 

시리즈 다시보기

 

요소를 "바꾸는" map을 익혔으니, 다음 편에서는 요소를 "고르는" filter로 넘어갑니다. 이 둘을 엮으면 스트림의 절반은 끝난 셈입니다.

댓글