자바 람다와 스트림 – 효율적인 코드 작성

2024. 11. 9. 15:29·프로그래밍 언어/Java
728x90

람다의 개념 및 자바에서의 도입 배경

  • 람다 표현식(Lambda Expression)은 자바 8에서 도입된 기능이다.
  • 익명 함수(anonymous function)를 간단하게 표현할 수 있는 방식입니다.
  • 자바는 람다 도입 이전에 익명 클래스(anonymous class)를 통해 함수형 스타일로 코드를 작성했지만, 코드가 길어지고 읽기 어려워져 이를 개선하기 위해 도입되었다.

람다 표현식의 목적은

  • 코드의 간결함과 가독성을 높이고 함수형 프로그래밍을 지원하여 데이터 처리와 이벤트 처리를 더 직관적으로 만들기 위함이다.

사용 방법

(매개변수) -> { 표현식 }

사용 예시를 보자면,

List<String> names = Arrays.asList("Avery", "Bobby", "Caley");

// 기존
names.forEach(new Consumer<String>() {
    public void accept(String name) {
        System.out.println(name);
    }
});

// 람다 표현식으로 변환
names.forEach(name -> System.out.println(name));

 


스트림의 핵심 개념과 사용 이유

  • 스트림(Stream) API는 자바 8에서 도입된 또 하나의 기능이다.
  • 컬렉션 데이터를 함수형 스타일로 처리할 수 있도록 도와준다.
  • 스트림을 사용하면 필터링, 매핑, 집계 등의 작업을 간단하고 효율적으로 수행할 수 있다.

특히 람다와 스트림을 조합하면 코드가 직관적이고 간결해진다!

스트림의 핵심은 데이터를 한 번만 순회하면서 원하는 결과를 생성하는 것이다. 예를 들어, 리스트에서 특정 조건을 만족하는 요소만 필터링하거나, 모든 요소에 변환을 적용할 수 있다.

스트림 주요 메서드

 

  • filter: 조건에 맞는 요소만 걸러낸다.
  • map: 요소에 함수를 적용하여 새로운 요소로 변환한다.
  • reduce: 모든 요소를 결합하여 하나의 결과를 만든다.

사용 예시를 보면,

  • 데이터베이스에서 엔티티 리스트를 조회한 후, 특정 필드(ID 등)만 추출하여 사용해야 하는 경우가 많다. 예로, 사용자 리스트에서 사용자 ID만 추출할 때 Stream을 활용하면 간결하게 처리할 수 있다.
List<User> users = userRepository.findAll(); // User 엔티티 리스트 조회
List<Long> userIds = users.stream()
                          .map(User::getId) // 각 사용자 엔티티에서 ID 필드 추출
                          .collect(Collectors.toList());

 

  • 엔티티 리스트에서 특정 조건을 만족하는 요소들만 필터링해야 할 때 Stream의 filter를 사용하면 편리하다. 예를 들어, 활성화된 사용자만 추출하는 경우를 보면 아래와 같다.
List<User> activeUsers = users.stream()
                              .filter(User::isActive) // 활성화된 사용자만 필터링
                              .collect(Collectors.toList());
  • 사용자를 역할별로 그룹화하거나, 특정 필드를 기준으로 그룹화하는 작업도 Stream을 통해 간결하게 처리할 수 있다. Spring에서는 이런 그룹화된 데이터를 필요로 할 때가 많다. 이 코드는 사용자 리스트를 역할(role) 기준으로 그룹화하여 Map에 저장하는 예제이다. 결과는 역할을 키로 하고, 해당 역할을 가진 사용자 리스트를 값으로 갖는 구조이다.
Map<String, List<User>> usersByRole = users.stream()
                                           .collect(Collectors.groupingBy(User::getRole));
  • 특정 필드 값의 합계나 평균 등을 계산해야 할 때 Stream의 mapToInt와 average 같은 집계 메서드를 자주 사용한다. 예를 들어, 사용자 리스트에서 평균 연령을 구할 수 있다.
double averageAge = users.stream()
                         .mapToInt(User::getAge) // 나이 필드를 int로 변환
                         .average()              // 평균 계산
                         .orElse(0.0);           // 데이터가 없을 때 기본값 설정
  • 사용자들의 나이 합 구하기 예제이다.
int totalAge = users.stream()
                    .map(User::getAge)
                    .reduce(0, Integer::sum); // 누적하여 합계 계산

System.out.println("Total Age: " + totalAge);
  • 사용자들의 이름 합치기 예제이다.
String names = users.stream()
                    .map(User::getName)
                    .reduce((name1, name2) -> name1 + ", " + name2)
                    .orElse("No users");

System.out.println("User Names: " + names);

람다 & 스트림에 관련된 질문 리스트

람다 표현식과 익명 클래스의 차이는 무엇인가?

 

  • 람다는 코드를 더 간결하게 작성할 수 있어 가독성이 높아집니다.
  • 람다에서 this는 람다가 선언된 외부 객체를 가리키지만, 익명 클래스에서 this는 익명 클래스 자체를 가리킵니다.
  • 람다 표현식은 타입 추론이 가능하지만, 익명 클래스는 반드시 타입을 명시해야 합니다.

스트림 사용 시 성능상의 고려사항에 대해 말씀해보세요. (병렬 스트림, lazy evaluation 등)

스트림은 기본적으로 순차적으로 동작하지만, parallelStream()로 다중 스레드를 활용하여 대용량 데이터 처리 시 성능을 높일 수 있습니다.

하지만, 적은 데이터 또는 순서 보장이 꼭 필요한 경우에는 parallelStream() 사용을 피해야 합니다.

또한, 스트림은 lazy evaluation를 지원합니다. 이는 필요한 시점까지 연산을 미루는 방식으로, 최종 연산(예: collect)이 호출되기 전까지는 중간 연산(filter, map 등)이 실제로 실행되지 않습니다.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 중간 연산인 filter와 map은 lazy evaluation으로 최종 연산이 호출될 때까지 실행되지 않음
List<String> result = names.stream()
                           .filter(name -> {
                               System.out.println("Filter: " + name);
                               return name.startsWith("A");
                           })
                           .map(name -> {
                               System.out.println("Map: " + name);
                               return name.toUpperCase();
                           })
                           .collect(Collectors.toList()); // 최종 연산이 호출되는 순간 모든 연산이 실행

심화 질문

  • 병렬 스트림은 어떤 경우에 사용하는 것이 좋을까?
    • 병렬 스트림은 대량의 데이터를 처리할 때나, 각 요소에 대한 연산이 독립적일 때 효과적입니다. 하지만 작은 데이터나 순서가 중요한 연산에서는 오히려 성능이 저하될 수 있습니다.
  • 스트림에서 lazy evaluation이란 무엇인가?
    • 스트림 연산은 최종 연산이 호출될 때까지 실제로 실행되지 않으며, 이는 불필요한 연산을 줄이고 성능을 최적화하는 데 도움이 됩니다.

++추가 내용

1. :: 문법

::는 자바에서 메서드 참조(method reference)를 나타내는 문법이다.

 

  • 정적 메서드 참조 (ClassName::methodName)
// 예: Integer의 parseInt 메서드 참조
Function<String, Integer> parseInt = Integer::parseInt;
Integer result = parseInt.apply("123"); // 결과: 123
  • 특정 객체의 인스턴스 메서드 참조 (instance::methodName)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println); // System.out 인스턴스의 println 메서드 참조
  • 특정 타입의 임의 객체의 인스턴스 메서드 참조 (ClassName::methodName)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
     .map(String::toUpperCase) // String의 toUpperCase 메서드를 참조
     .forEach(System.out::println);
  • 생성자 참조 (ClassName::new)
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get(); // ArrayList 생성

 

메서드 참조를 통해 람다 표현식을 더 간결하게 할 수 있다.

// 람다 표현식
names.forEach(name -> System.out.println(name));

// 메서드 참조
names.forEach(System.out::println);

2. Runnable 인터페이스

Runnable은 자바에서 실행 가능한 작업을 나타내는 함수형 인터페이스이다. 즉, Runnable을 구현하면 독립적으로 실행될 수 있는 코드 블록을 정의할 수 있다.

Runnable 인터페이스는 단 하나의 추상 메서드인 run()을 가지고 있으며, 이 메서드 안에 실행하고자 하는 작업을 정의할 수 있다.

주로 아래의 경우에 사용한다.

  • 멀티스레딩: 새로운 스레드를 생성할 때 Runnable을 구현하여 스레드가 수행할 작업을 지정한다.
  • 함수형 프로그래밍: 자바 8부터 Runnable은 함수형 인터페이스로 간주되어 람다 표현식으로 간단하게 구현할 수 있다.

사용 예시를 보겠다.

  • 익명 클래스를 사용한 Runnable 구현
Runnable anonymousClass = new Runnable() {
    @Override
    public void run() {
        System.out.println("익명 클래스 실행");
        System.out.println(this); // 익명 클래스 자체를 가리킴
    }
};

anonymousClass.run();

 

  • 람다 표현식을 사용한 Runnable 구현

 

Runnable lambdaExpression = () -> {
    System.out.println("람다 표현식 실행");
    System.out.println(this); // 외부 클래스의 인스턴스를 가리킴
};

lambdaExpression.run();

이때 위에서 말한 것처럼 람다표현식을 쓴 경우의 this와 익명 클래스를 쓴 경우의 this는 다른 의미를 갖고 있는걸 유의해야 한다.

  • 스레드 생성
// Runnable 구현
Runnable task = () -> {
    System.out.println("새로운 스레드에서 실행되는 작업");
};

// 스레드 생성 및 시작
Thread thread = new Thread(task);
thread.start();
728x90
반응형
저작자표시 (새창열림)

'프로그래밍 언어 > Java' 카테고리의 다른 글

캡슐화와 은닉화의 차이 - 객체지향 프로그래밍(OOP)의 기본 원칙  (0) 2024.11.19
예외 처리 (Exception Handling) – 안정적인 코드를 작성하는 법  (4) 2024.11.18
자바 컬렉션 프레임워크 – 자바에서 데이터를 다루는 방법  (1) 2024.11.09
제네릭 타입 (Generic Types) – 자바에서 타입 안정성을 확보하다  (2) 2024.11.08
Java 프로그래밍 초급(5) - 컬렉션 프레임워크 : List  (1) 2022.01.26
'프로그래밍 언어/Java' 카테고리의 다른 글
  • 캡슐화와 은닉화의 차이 - 객체지향 프로그래밍(OOP)의 기본 원칙
  • 예외 처리 (Exception Handling) – 안정적인 코드를 작성하는 법
  • 자바 컬렉션 프레임워크 – 자바에서 데이터를 다루는 방법
  • 제네릭 타입 (Generic Types) – 자바에서 타입 안정성을 확보하다
pink_salt
pink_salt
유익함을 주는 개발자가 되도록 keep going
  • pink_salt
    KeepGoingForever
    pink_salt
  • 전체
    오늘
    어제
    • 분류 전체보기 (117)
      • Project (7)
      • WEB study (3)
        • WEB(Springboot) (10)
        • Git, GitLab (13)
        • Clean code (1)
        • FrontEnd (3)
      • Study (21)
        • Algorithm (19)
        • 면접 준비 (2)
      • Cloud Computing (2)
        • AWS (2)
      • 프로그래밍 언어 (35)
        • Java (29)
        • Python (0)
        • javascript (6)
      • 운영체제 (0)
        • Linux (0)
      • Database (4)
        • MongoDB (8)
        • SQL (8)
      • 애플리케이션 개발 (1)
        • Android (1)
      • AI (1)
        • Deeplearning (1)
        • machinelearning (0)
      • Daily (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    mysql
    티스토리챌린지
    MongoDB
    SW
    개념
    Query
    무료코딩교육
    백준
    codepresso
    Java
    BFS
    빅오표기법
    자바
    gitlab
    코드프레소
    spring boot
    언어
    무료IT교육
    Database
    오블완
    코딩이러닝
    IT교육
    대외활동
    python
    dp
    객체지향
    Git
    SWEA
    git branch
    코딩강의
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
pink_salt
자바 람다와 스트림 – 효율적인 코드 작성
상단으로

티스토리툴바