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