람다의 개념 및 자바에서의 도입 배경
- 람다 표현식(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();
'프로그래밍 언어 > Java' 카테고리의 다른 글
캡슐화와 은닉화의 차이 - 객체지향 프로그래밍(OOP)의 기본 원칙 (0) | 2024.11.19 |
---|---|
예외 처리 (Exception Handling) – 안정적인 코드를 작성하는 법 (3) | 2024.11.18 |
자바 컬렉션 프레임워크 – 자바에서 데이터를 다루는 방법 (0) | 2024.11.09 |
제네릭 타입 (Generic Types) – 자바에서 타입 안정성을 확보하다 (0) | 2024.11.08 |
Java 프로그래밍 초급(5) - 컬렉션 프레임워크 : List (0) | 2022.01.26 |