# 메서드 참조 (Method Reference)
- Java 8부터 도입 된 람다 표현식(Lambda Expression)을 좀 더 간결하게 표현할 수 있도록 해주는 문법
- 메서드 참조는 특정 메서드만을 호출하는 람다 표현식을 단순화하는 방법으로, 4가지 주요 형식이 있다
1. **정적 메서드 참조** - `클래스명::메서드명` 형식을 사용한다. 예를 들어, Integer 클래스의 parseInt 메서드를 참조하려면 `Integer::parseInt`라고 작성할 수 있다.
```java
Function<String, Integer> f1 = s -> Integer.parseInt(s);
Function<String, Integer> f2 = Integer::parseInt;
```
2. **특정 객체의 인스턴스 메서드 참조** - `객체변수명::메서드명` 형식을 사용한다. 예를 들어, 특정 String 객체 str의 length 메서드를 참조하려면 `str::length`라고 작성할 수 있다.
```java
String str = "Hello";
Supplier<Integer> s1 = () -> str.length();
Supplier<Integer> s2 = str::length;
```
3. **임의 객체의 인스턴스 메서드 참조** - `클래스명::메서드명` 형식을 사용하지만, 이 메서드는 인스턴스 메서드이다. 예를 들어, String 클래스의 length 메서드를 참조하려면 `String::length`라고 작성할 수 있다.
```java
Function<String, Integer> f1 = s -> s.length();
Function<String, Integer> f2 = String::length;
```
4. **생성자 참조** - `클래스명::new` 형식을 사용한다. 예를 들어, ArrayList의 생성자를 참조하려면 `ArrayList::new`라고 작성할 수 있다.
```java
Supplier<List<String>> s1 = () -> new ArrayList<>();
Supplier<List<String>> s2 = ArrayList::new;
```
## 메서드 참조와 스트림
- 메서드 참조는 람다 표현식의 더 간결한 버전이다. 메서드 참조는 특정 메서드만 호출하는 람다 표현식에 대한 간단한 대체품으로 생각할 수 있다
- 스트림 API의 많은 메서드들이 함수형 인터페이스를 매개변수로 받기 때문에, 이 인터페이스를 구현하는 람다 표현식 대신 메서드 참조를 사용할 수 있다.
- 이것은 코드의 가독성을 향상시키고, 코드의 길이를 줄여준다.
### 사용 예시
리스트의 각 요소를 출력하려고 할 때, 람다 표현식을 사용해서 다음과 같이 할 수 있다
**메서드 참조를 사용하지 않은 경우**
```java
List<String> list = Arrays.asList("A", "B", "C");
list.forEach(s -> System.out.println(s));
```
**메서드 참조를 사용한 경우**
```java
List<String> list = Arrays.asList("A", "B", "C");
list.forEach(System.out::println);
```
> 위의 두 코드는 동일한 동작을 하지만, 메서드 참조를 사용한 코드가 더 간결하고 읽기 쉽다.
```java
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
class MethodReferenceExample {
public static void main(String[] args) {
// 1. 정적 메서드 참조
List<String> strings = Arrays.asList("one", "two", "three");
strings.forEach(System.out::println);
// 2. 특정 객체의 인스턴스 메서드 참조
Consumer<String> printer = System.out::println;
strings.forEach(printer);
// 3. 임의 객체의 인스턴스 메서드 참조
List<Integer> numbers = Arrays.asList(5, 3, 50, 24, 40);
numbers.sort(Integer::compareTo);
numbers.forEach(System.out::println);
// 4. 생성자 참조
List<String> elements = Arrays.asList("a", "b", "c");
List<Element> objects = elements.stream()
.map(Element::new)
.collect(Collectors.toList());
objects.forEach(System.out::println);
}
}
class Element {
String value;
public Element(String value) {
this.value = value;
}
@Override
public String toString() {
return "Element{" +
"value='" + value + '\'' +
'}';
}
}
```
---
# 병렬프로그래밍

- 주로 여러 쓰레드를 이용해 여러 작업을 동시에 수행하는 것을 의미한다
- 여러 작업을 동시에 실행하는 방법으로, 멀티 코어나 멀티 프로세서를 가진 시스템에서 프로그램의 성능을 향상시키는 데 주로 사용된다.
- 많은 연산을 동시에 처리하므로 큰 데이터 셋에 대한 작업을 빠르게 수행할 수 있다
- 병렬 프로그래밍은 사용하기 어려우며 동시성 문제(concurrency issues)를 처리해야 한다는 단점도 존재한다
- 예를 들어, 여러 쓰레드가 같은 데이터에 동시에 접근하려고 할 때 발생하는 데이터 레이스 조건(race conditions), 쓰레드의 데드락(deadlocks) 등이 있다
- Java 7에서는 Fork/Join 프레임워크를 도입하여, 병렬로 수행되는 재귀적인 태스크를 더 효율적으로 처리할 수 있게 되었다.
- Java 8에서는 스트림 API를 도입하였고, 스트림 API에서 제공하는 기능을 통해 병렬 처리를 더욱 간편하게 만들었다.
## Fork/Join 프레임워크

- Java 7에 도입된 동시성 프로그래밍 모델로서, 병렬 처리를 용이하게 해주는 API다.
- 주로 '분할 정복' 알고리즘을 구현할 때 유용하다.
- 핵심 개념은 큰 작업을 작은 서브작업으로 분할(fork)한 다음, 각각의 작은 작업들을 병렬로 처리하고, 그 결과를 다시 합쳐(join) 원래의 큰 작업을 완료하는 것이다.
- 많은 데이터를 처리하거나 CPU 집중적인 작업을 병렬로 처리해야 할 때 유용하다. 그러나 동기화와 스레드 관리와 같은 멀티 스레드 프로그래밍의 복잡성을 주의해야 한다.
<details>
<summary>예시 코드</summary>
<div markdown="1">
```java
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
class SumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 100; // 임계값
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// 임계값 이하의 작업인 경우 직접 계산
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// 임계값보다 큰 작업인 경우 분할하여 병렬 실행
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork(); // 왼쪽 부분 작업을 비동기로 실행
long rightResult = rightTask.compute(); // 오른쪽 부분 작업을 동기적으로 실행
long leftResult = leftTask.join(); // 왼쪽 부분 작업의 결과를 가져옴
return leftResult + rightResult;
}
}
}
public class Main {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
ForkJoinPool forkJoinPool = new ForkJoinPool();
long result = forkJoinPool.invoke(new SumTask(array, 0, array.length));
System.out.println("Sum: " + result);
}
}
```
</div>
</details>
---
## 병렬스트림

- Java 8에서 소개된 스트림 API는 데이터 처리를 간결하고 효과적으로 만다. 그러나 기본적인 스트림 연산은 순차적이다. 즉, 스트림의 각 요소는 한 번에 하나씩 처리되며, 다음 요소는 이전 요소가 처리된 후에 처리된다.
- 병렬 스트림은 이러한 제한을 극복하기 위해 도입되었다. 병렬 스트림은 여러 코어에서 동시에 데이터를 처리하므로 데이터 처리 작업을 가속화할 수 있다.
- 병렬 스트림은 스트림을 여러 부분으로 분할하고, 각 부분을 독립적으로 처리한 후, 결과를 다시 결합한다.
- 이 과정은 '분할-정복(divide-and-conquer)' 알고리즘과 유사하다.
병렬 스트림을 생성하는 방법은 매우 간단하다. 기존의 순차 스트림에 `parallel()` 메소드를 호출하면 된다.
```java
List<String> list = Arrays.asList("A", "B", "C", "D");
list.stream().parallel().forEach(System.out::println);
```
> 위의 코드에서, `parallel()` 메소드 호출은 순차 스트림을 병렬 스트림으로 변환한다. 그런 다음 `forEach` 메소드를 사용하여 각 요소를 출력한다.
### 병렬 스트림의 성능 비교
////////////////
**10,000,000까지의 자연수 중 소수를 찾는 코드**
```java
public static void main(String[] args) {
// Sequential stream
long t0 = System.nanoTime();
long primeCount = IntStream.range(1, 10_000_000)
.filter(TempTest::isPrime)
.count();
long t1 = System.nanoTime();
System.out.println("Sequential Stream Prime Count: " + primeCount + ", Time Taken: " + (t1 - t0) / 1_000_000 + " ms");
// Parallel stream
t0 = System.nanoTime();
primeCount = IntStream.range(1, 10_000_000)
.parallel()
.filter(TempTest::isPrime)
.count();
t1 = System.nanoTime();
System.out.println("Parallel Stream Prime Count: " + primeCount + ", Time Taken: " + (t1 - t0) / 1_000_000 + " ms");
}
public static boolean isPrime(int num) {
if (num <= 1) return false;
for (int i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) return false;
}
return true;
}
```
## 병렬스트림의 장점
- **성능 향상** : 병렬 스트림은 여러 스레드에서 동시에 데이터를 처리하므로, 많은 양의 데이터를 처리하거나 CPU 집중적인 작업을 수행할 때 성능을 향상시킬 수 있다.
- **간결한 코드**: Java의 스트림 API를 사용하면, 멀티스레딩을 위한 복잡한 코드 없이도 쉽게 병렬 처리를 구현할 수 있다.
### 병렬스트림을 사용하면 좋은 경우
- **대량의 데이터 처리**: 병렬 스트림은 데이터를 여러 스레드에 분산시켜 처리하므로, 처리해야 할 데이터의 양이 많을 때 효과적이다. 데이터의 양이 작다면, 스레드를 생성하고 관리하는 오버헤드가 성능 저하를 일으킬 수 있다.
- **CPU 집중적인 연산**: 각 데이터를 처리하는 데 많은 CPU 시간이 필요한 경우, 병렬 스트림이 더 효과적일 수 있다. 각 작업이 CPU가 아니라 I/O에 의해 제한되는 경우(예: 네트워크 또는 디스크 접근), 병렬 스트림의 이점은 크게 감소하거나 없어질 수 있다.
- **데이터 소스가 병렬 처리에 적합할 때**: 데이터 소스가 쉽게 분할할 수 있고, 각 부분이 독립적으로 처리될 수 있을 때 병렬 스트림이 잘 동작한다. 예를 들어, ArrayList는 쉽게 분할할 수 있지만, LinkedList는 그렇지 않다.
## 주의 사항
- 병렬 스트림은 대량의 데이터를 다룰 때만 성능 향상을 기대할 수 있다. 데이터가 충분히 많지 않다면 병렬 처리를 위한 추가적인 비용(작업 분할, 스레드 컨텍스트 전환 등)이 일반 순차 처리보다 더 클 수 있다.
- 병렬 스트림은 쓰레드 안전성(thread-safety)에 주의해야 한다. 다수의 스레드가 동일한 데이터에 동시에 접근하려고 하면 데이터 무결성이 깨질 수 있다.
- 병렬 스트림은 순서를 보장하지 않는다. 병렬 스트림에서 처리 순서는 쓰레드 스케줄링에 따라 달라지므로, 순서가 중요한 작업에는 적합하지 않을 수 있다.
---
###### tags: `과외(하희영)`