Java 8에서는 함수형 프로그래밍을 지원하기 위해 스트림(Stream)이라는 새로운 개념이 도입되었습니다. 스트림은 컬렉션과 배열의 요소를 반복적으로 처리하고, 데이터를 조작하고, 쿼리할 수 있는 기능을 제공합니다.
스트림은 요소들의 연속적인 흐름으로 생각할 수 있으며, 이를 통해 데이터를 한 번에 하나씩 처리할 수 있습니다.
내부 반복(Internal Iteration): 스트림은 내부적으로 반복을 처리하므로 사용자는 요소를 명시적으로 반복할 필요가 없습니다. 이를 통해 코드가 간결해지고, 병렬 처리가 용이해집니다.
지연 평가(Lazy Evaluation): 중간 연산(intermediate operation)과 최종 연산(terminal operation)으로 구성된 스트림 파이프라인은 최종 연산이 호출될 때까지 실행되지 않습니다. 이를 통해 필요한 연산만 수행하여 효율적인 처리가 가능합니다.
함수형 프로그래밍 지원: 스트림은 함수형 인터페이스와 람다식을 활용하여 데이터를 처리하므로, 함수형 프로그래밍의 장점을 취할 수 있습니다.
병렬 처리 지원: 스트림은 병렬 스트림(parallel stream)을 통해 멀티코어 환경에서 손쉽게 병렬 처리를 수행할 수 있습니다.
스트림을 사용하면 컬렉션과 배열을 보다 간결하고 효율적으로 처리할 수 있으며, 코드의 가독성과 유지 보수성을 높일 수 있습니다. 예를 들어, 컬렉션의 모든 요소를 필터링하거나 변환하는 작업을 한 줄의 코드로 간단하게 표현할 수 있습니다.
Stream은 Iterator와 비교하여 처리 속도가 빠르고 병렬 처리에 효율적입니다. 이는 Stream이 내부적으로 최적화되어있고, 요소를 처리하는 방식이 다르기 때문입니다.
Stream은 다양한 요소 처리를 정의할 수 있습니다. 예를 들어, 매핑(mapping), 필터링(filtering), 정렬(sorting), 그룹화(grouping), 집계(aggregating) 등 다양한 연산을 제공합니다. 이러한 연산을 사용하여 요소를 변환하고 조작할 수 있습니다.
또한, Stream은 중간 처리(intermediate operations)와 최종 처리(terminal operations)를 수행하도록 파이프라인을 형성할 수 있습니다. 중간 처리 연산은 스트림을 변환하고 필터링하며, 최종 처리 연산은 스트림의 요소를 소모하고 결과를 생성합니다. 이를 통해 사용자는 간단하고 유연한 방식으로 요소를 처리할 수 있습니다.
Stream의 이러한 특성들은 코드를 간결하게 작성하고 가독성을 높이며, 병렬 처리를 쉽게 수행할 수 있도록 도와줍니다.
스트림은 하나 이상의 중간 연산(intermediate operations)과 최종 연산(terminal operation)을 연결하여 스트림 파이프라인(Stream pipeline)을 형성할 수 있습니다. 이러한 연결된 스트림을 스트림 파이프라인이라고 합니다.
스트림 소스 -> 중간 연산1 -> 중간 연산2 -> ... -> 중간 연산N -> 최종 연산
여기서 "스트림 소스"는 데이터를 제공하는 원본이 되는 요소이고, "중간 연산"은 스트림을 변환하거나 필터링하는 작업을 수행하며, "최종 연산"은 스트림의 요소를 소모하고 결과를 생성하는 작업을 수행합니다.
중간 연산은 연결된 스트림을 반환하므로 여러 개의 중간 연산을 연속적으로 연결할 수 있습니다. 최종 연산은 연결된 스트림 파이프라인을 처리하고 결과를 생성하며, 최종 연산이 호출되기 전까지는 중간 연산이 실제로 실행되지 않습니다. 이를 통해 지연 평가(lazy evaluation)가 가능하며, 필요한 연산만 수행하여 효율적인 처리가 가능합니다.
스트림 파이프라인을 사용하면 데이터를 한 번에 여러 단계로 처리할 수 있으며, 코드의 가독성과 유지 보수성을 높일 수 있습니다. 또한, 병렬 처리를 쉽게 수행할 수 있어서 대용량 데이터를 효율적으로 처리할 수 있습니다.
중간 연산(intermediate operations)은 스트림을 변환하거나 필터링하는 작업을 수행합니다. 이러한 작업은 스트림의 요소를 걸러내거나 변환하여 새로운 스트림을 생성합니다. 중간 연산은 여러 단계로 연결될 수 있으며, 이를 통해 다양한 작업을 조합하여 복잡한 데이터 처리를 수행할 수 있습니다. 일반적으로 중간 연산은 요소를 정제하거나 변환하는 작업을 수행합니다. 예를 들어, 매핑(mapping), 필터링(filtering), 정렬(sorting), 그룹화(grouping) 등의 작업이 중간 연산에 해당합니다.
최종 연산(terminal operation)은 연결된 스트림을 처리하고 결과를 생성합니다. 최종 연산은 스트림의 요소를 소모하며, 더 이상 스트림을 반환하지 않습니다. 일반적으로 최종 연산은 중간 연산에서 정제된 요소들을 반복하거나 집계하는 작업을 수행합니다. 예를 들어, 요소를 반복하여 출력하거나 리스트로 수집(collecting)하는 작업이 최종 연산에 해당합니다.
스트림(Stream) 인터페이스의 구현 객체는 다양한 리소스로부터 얻을 수 있습니다.
컬렉션(Collection): 기존에 존재하는 자바 컬렉션 클래스인 List, Set, Map 등의 컬렉션에서 스트림을 생성할 수 있습니다. 컬렉션 인터페이스에 추가된 stream()
메서드를 이용하여 컬렉션으로부터 스트림을 생성할 수 있습니다.
배열(Array): 배열에서 스트림을 생성할 수 있습니다. Arrays
클래스의 stream()
메서드를 이용하여 배열로부터 스트림을 생성할 수 있습니다.
파일(File): 파일에서 데이터를 읽어와 스트림을 생성할 수 있습니다. Files
클래스의 lines()
메서드를 이용하여 파일로부터 스트림을 생성할 수 있습니다.
텍스트 파일, 데이터베이스 등 외부 리소스: 텍스트 파일이나 데이터베이스와 같은 외부 리소스에서 데이터를 읽어와 스트림을 생성할 수 있습니다. 이를 통해 대용량 데이터를 스트림으로 처리할 수 있습니다.
커스텀 데이터 소스: 사용자가 직접 구현한 커스텀한 데이터 소스에서 데이터를 읽어와 스트림을 생성할 수 있습니다. 이를 통해 다양한 데이터 소스에서 스트림을 생성하여 처리할 수 있습니다.
자바 컬렉션 프레임워크(Collection Framework)는 스트림(Stream)을 지원하기 위해 컬렉션 인터페이스들에 stream() 메서드를 추가하였습니다. 또한, 병렬 스트림(parallel stream)을 생성하기 위한 parallelStream() 메서드도 추가되었습니다.
따라서 컬렉션 프레임워크의 모든 구현체들이 stream() 메서드와 parallelStream() 메서드를 제공하는 것은 아니지만, 컬렉션 인터페이스를 구현한 List, Set 등의 자식 인터페이스들은 이러한 메서드들을 상속하고 있습니다. 따라서 List와 Set 인터페이스를 구현한 모든 컬렉션에서 객체 스트림을 얻을 수 있습니다.
예를 들어, ArrayList나 HashSet과 같은 List와 Set의 구현체들은 컬렉션 인터페이스의 메서드들을 상속하고 있기 때문에 stream() 메서드와 parallelStream() 메서드를 호출하여 객체 스트림을 얻을 수 있습니다. 이를 통해 컬렉션의 요소를 스트림으로 손쉽게 처리할 수 있습니다.
자바의 Arrays
클래스를 이용하면 다양한 종류의 배열로부터 스트림을 얻을 수 있습니다. Arrays
클래스는 배열을 다루는 유틸리티 메서드를 제공하며, 이를 활용하여 배열로부터 스트림을 생성할 수 있습니다.
가장 일반적인 방법은 배열을 직접 스트림으로 변환하는 것입니다. Arrays
클래스의 stream()
메서드를 사용하여 배열을 스트림으로 변환할 수 있습니다. 이 메서드를 사용하면 배열의 모든 요소를 포함하는 스트림을 얻을 수 있습니다.
int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers);
또는 객체 배열인 Object[]
배열로부터 스트림을 얻는 경우에도 Arrays.stream()
메서드를 사용할 수 있습니다:
String[] words = {"apple", "banana", "orange"};
Stream<String> stream = Arrays.stream(words);
또한, 배열의 일부분만을 스트림으로 변환하거나 특정 범위의 요소들만을 스트림으로 얻을 수도 있습니다. 이를 위해서는 Arrays
클래스의 stream(T[] array, int startInclusive, int endExclusive)
메서드를 사용하면 됩니다.
int[] numbers = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(numbers, 1, 4); // index 1부터 3까지의 요소를 스트림으로 변환
배열을 다룰 때 자주 사용되는 유틸리티 메서드를 제공하는 Arrays
클래스를 이용하여 배열로부터 스트림을 쉽게 얻을 수 있습니다. 이를 통해 배열을 스트림으로 간편하게 변환하여 다양한 작업을 수행할 수 있습니다.
IntStream
인터페이스는 range()
및 rangeClosed()
와 같은 메서드를 제공하여 특정 범위의 정수 스트림을 생성할 수 있습니다.
range(int startInclusive, int endExclusive)
: 주어진 시작값(startInclusive)부터 종료값(endExclusive) 전까지의 정수 스트림을 생성합니다. 시작값은 포함되지만 종료값은 포함되지 않습니다.IntStream.range(1, 5); // 1, 2, 3, 4
rangeClosed(int startInclusive, int endInclusive)
: 주어진 시작값(startInclusive)부터 종료값(endInclusive)까지의 정수 스트림을 생성합니다. 시작값과 종료값 모두 포함됩니다.IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String filePath = "example.txt"; // 텍스트 파일의 경로
try {
// 파일에서 각 행을 스트림으로 읽어옴
Stream<String> lines = Files.lines(Paths.get(filePath));
// 각 행을 출력
lines.forEach(System.out::println);
// 스트림을 닫음
lines.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
필터링은 스트림에서 원하는 요소만 선택하여 추출하는 중간 처리 기능 중 하나입니다. 이를 통해 스트림에서 특정 조건을 만족하는 요소들을 걸러낼 수 있습니다. Java의 스트림 API에서는 주로 distinct()
와 filter()
메서드를 사용하여 필터링 작업을 수행합니다.
distinct()
: 중복된 요소를 제거하는 메서드입니다. 스트림에서 중복된 요소를 한 번만 포함하도록 합니다.Stream<String> distinctElements = stream.distinct();
filter(Predicate<? super T> predicate)
: 주어진 조건에 따라 요소를 걸러내는 메서드입니다. Predicate를 매개변수로 받아 조건에 맞는 요소만을 포함하는 새로운 스트림을 반환합니다.Stream<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0);
위의 코드에서 filter()
메서드는 주어진 람다식에 따라 스트림에서 조건을 만족하는 요소들을 걸러내고, 해당 요소들로 구성된 새로운 스트림을 반환합니다.
이렇게 distinct()
와 filter()
메서드를 사용하여 스트림에서 필요한 요소들을 추출할 수 있습니다. 이를 통해 데이터를 원하는 조건에 맞게 걸러내거나 중복을 제거할 수 있습니다.
매핑은 스트림의 요소를 다른 요소로 변환하는 중간 처리 기능 중 하나입니다. 스트림의 각 요소에 대해 특정한 작업을 수행하여 원하는 형태로 변환할 수 있습니다. Java의 스트림 API에서는 다양한 매핑 메서드들이 제공되며, 주요한 매핑 메서드로는 map()
, flatMap()
등이 있습니다.
map(Function<? super T, ? extends R> mapper)
: 스트림의 각 요소를 지정된 함수에 따라 변환하는 메서드입니다. Function을 매개변수로 받아 스트림의 각 요소를 해당 함수에 적용하여 새로운 요소로 매핑한 후, 해당 요소들로 구성된 새로운 스트림을 반환합니다.Stream<Integer> squaredNumbers = numbers.stream().map(n -> n * n);
flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
: 스트림의 각 요소에 대해 지정된 함수를 적용하고, 결과로 얻은 각각의 스트림을 하나의 스트림으로 평면화하여 반환하는 메서드입니다.List<String> words = Arrays.asList("Hello", "World");
Stream<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(c -> (char) c));
스트림 API에는 요소를 정렬하는 메소드가 존재합니다. 이를 통해 스트림의 요소를 정렬하여 원하는 순서로 처리할 수 있습니다.
가장 일반적으로 사용되는 요소 정렬 메서드는 sorted() 메서드입니다. 이 메서드는 기본적으로 요소의 자연 순서(natural order)에 따라 정렬합니다. 예를 들어, 숫자인 경우 오름차순으로 정렬됩니다.
List<Integer> numbers = Arrays.asList(3, 1, 2, 5, 4);
Stream<Integer> sortedNumbers = numbers.stream().sorted();
루핑(looping)은 스트림에서 요소를 하나씩 반복해서 가져와 처리하는 것을 의미합니다. Java의 스트림 API에서는 루핑을 수행하는 메서드로 주로 peek()
과 forEach()
를 사용합니다.
peek(Consumer<? super T> action)
: 스트림의 각 요소를 소비자(Consumer)에게 전달하여 해당 요소를 처리하는 메서드입니다. 이 메서드는 각 요소를 가져오는 것이 주 목적이지만, 중간 처리를 수행할 때 사용될 수 있습니다. 주로 디버깅 목적이나 로깅 등에 활용됩니다.List<String> words = Arrays.asList("apple", "banana", "orange");
words.stream()
.peek(word -> System.out.println("Processing word: " + word))
.forEach(System.out::println);
forEach(Consumer<? super T> action)
: 스트림의 각 요소를 소비자(Consumer)에게 전달하여 해당 요소를 처리하는 메서드입니다. 이 메서드는 스트림의 각 요소를 반복하면서 해당 요소를 처리하는 역할을 합니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().forEach(System.out::println);
매칭(matching)은 스트림의 요소들이 특정 조건을 만족하는지 여부를 조사하는 최종 처리 기능 중 하나입니다. Java의 스트림 API에서는 매칭을 수행하는 데에 사용되는 메서드로 allMatch()
, anyMatch()
, noneMatch()
세 가지가 주로 사용됩니다.
allMatch(Predicate<? super T> predicate)
: 스트림의 모든 요소가 주어진 조건을 만족하는지 여부를 확인하는 메서드입니다. 모든 요소가 주어진 조건을 만족하면 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
System.out.println("Are all numbers positive? " + allPositive); // 출력: true
anyMatch(Predicate<? super T> predicate)
: 스트림의 요소 중에서 하나라도 주어진 조건을 만족하는지 여부를 확인하는 메서드입니다. 하나라도 조건을 만족하면 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0);
System.out.println("Is there any even number? " + anyEven); // 출력: true
noneMatch(Predicate<? super T> predicate)
: 스트림의 모든 요소가 주어진 조건을 만족하지 않는지 여부를 확인하는 메서드입니다. 모든 요소가 주어진 조건을 만족하지 않으면 true
를 반환하고, 그렇지 않으면 false
를 반환합니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0);
System.out.println("Are there no negative numbers? " + noneNegative); // 출력: true
집계(aggregation)는 스트림의 요소들을 처리하여 하나의 값으로 산출하는 최종 처리 기능 중 하나입니다. 스트림의 모든 요소를 하나의 값으로 줄이거나 통계적 정보를 계산하는 등의 작업을 수행할 때 사용됩니다.
Java의 스트림 API에서는 다양한 집계 연산을 제공합니다. 일반적으로 사용되는 집계 메서드로는 count()
, sum()
, average()
, min()
, max()
등이 있습니다.
count()
: 스트림의 요소의 개수를 계산하는 메서드입니다. 스트림에 포함된 요소의 개수를 반환합니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().count();
System.out.println("Number of elements: " + count); // 출력: 5
sum()
: 스트림의 요소들의 합을 계산하는 메서드입니다. 요소들이 정수나 소수형일 경우에만 사용할 수 있습니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
System.out.println("Sum of elements: " + sum); // 출력: 15
average()
: 스트림의 요소들의 평균값을 계산하는 메서드입니다. 요소들이 정수나 소수형일 경우에만 사용할 수 있습니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
OptionalDouble average = numbers.stream().mapToInt(Integer::intValue).average();
System.out.println("Average of elements: " + average.orElse(0.0)); // 출력: 3.0
min()
및 max()
: 스트림의 요소들 중에서 최솟값 또는 최댓값을 찾는 메서드입니다.List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> min = numbers.stream().min(Comparator.naturalOrder());
Optional<Integer> max = numbers.stream().max(Comparator.naturalOrder());
System.out.println("Minimum element: " + min.orElse(0)); // 출력: 1
System.out.println("Maximum element: " + max.orElse(0)); // 출력: 5
Optional
클래스는 값의 존재 여부를 표현하는 데 사용되며, 주로 메서드의 반환 값으로 사용됩니다. 이 클래스는 값이 존재할 수도 있고 없을 수도 있는 상황을 다룰 때 유용하게 사용됩니다.
Optional
클래스는 집계된 값뿐만 아니라 값의 존재 여부에 대한 정보를 담고 있습니다. 이를 통해 값이 없는 경우에 대비한 다양한 처리를 할 수 있습니다. 또한, Optional
클래스는 디폴트 값을 설정하거나 값이 존재할 때 특정 동작을 수행하는 Consumer
를 등록할 수 있는 메서드를 제공합니다.
orElse(T other)
: 값이 존재하지 않는 경우 디폴트 값을 반환합니다.Optional<String> optional = Optional.empty();
String result = optional.orElse("Default Value");
System.out.println(result); // 출력: "Default Value"
ifPresent(Consumer<? super T> consumer)
: 값이 존재하는 경우에만 주어진 동작을 수행합니다.Optional<String> optional = Optional.of("Value");
optional.ifPresent(value -> System.out.println("Value is: " + value)); // 출력: "Value is: Value"
orElseGet(Supplier<? extends T> supplier)
: 값이 존재하지 않는 경우 디폴트 값을 생성하는 Supplier
를 통해 값을 가져옵니다.Optional<String> optional = Optional.empty();
String result = optional.orElseGet(() -> "Default Value");
System.out.println(result); // 출력: "Default Value"
orElseThrow(Supplier<? extends X> exceptionSupplier)
: 값이 존재하지 않는 경우 주어진 예외를 발생시킵니다.Optional<String> optional = Optional.empty();
optional.orElseThrow(() -> new NoSuchElementException("No value present"));
Optional
클래스를 사용하여 값의 존재 여부를 안전하게 처리할 수 있으며, 이를 통해 NullPointerException 등의 예외를 방지할 수 있습니다. 또한, 디폴트 값을 설정하거나 값이 존재할 때 특정 동작을 수행하는 등의 유연한 처리가 가능합니다.
스트림은 기본적인 집계 기능을 제공하는데, 이는 요소들을 하나의 값으로 리듀스할 수 있는 여러 메서드를 포함합니다. 그러나 때로는 기본 집계 메서드로는 원하는 동작을 수행하기 어려울 수 있습니다. 이런 경우에 reduce()
메서드를 사용하여 사용자 정의 집계 작업을 수행할 수 있습니다.
reduce()
메서드는 스트림의 요소를 하나의 값으로 줄이는 연산을 수행합니다. 이 메서드는 초기값(initial value)과 BinaryOperator를 인자로 받습니다. BinaryOperator는 두 개의 동일한 타입을 받아 하나의 타입을 반환하는 함수입니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // 출력: Sum: 15
reduce()
메서드를 사용하여 다양한 집계 작업을 수행할 수 있습니다. 예를 들어, 최댓값이나 최솟값을 찾는 작업, 문자열을 연결하는 작업 등을 구현할 수 있습니다. 또한, 병렬 스트림에서도 사용할 수 있어 많은 요소를 처리하는 데 유용합니다.
스트림은 요소들을 필터링하거나 매핑한 후, 그 결과를 수집하는 최종 처리 메서드로 collect()
를 제공합니다. 이 메서드를 사용하여 스트림의 요소들을 원하는 형식으로 수집하고 그 결과를 반환할 수 있습니다.
collect()
메서드는 Collector
인터페이스를 구현한 객체를 매개변수로 받습니다. 이를 통해 요소들을 수집하는 방식을 지정할 수 있습니다. 자주 사용되는 Collector
구현체에는 toList()
, toSet()
, toMap()
등이 있습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // 출력: [2, 4, 6]
List<String> words = Arrays.asList("Hello", "World", "Java");
String result = words.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // 출력: Hello, World, Java
collect()
메서드는 컬렉션의 요소들을 그룹화하여 Map
객체를 생성하는 기능도 제공합니다. 이를 통해 스트림의 요소들을 특정 기준에 따라 그룹화하고, 각 그룹별로 처리할 수 있습니다.
Collectors.groupingBy()
메서드를 사용하여 그룹화를 수행할 수 있습니다. 이 메서드는 기준이 되는 함수를 매개변수로 받고, 해당 함수의 결과에 따라 요소들을 그룹화한 후 각 그룹을 Map
객체로 반환합니다.
List<String> words = Arrays.asList("apple", "banana", "orange", "grape", "pear");
Map<Integer, List<String>> groupedByLength = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
{5=[apple, grape], 6=[orange], 7=[banana], 4=[pear]}
동시성(concurrency)은 여러 작업을 동시에 처리하는 것을 의미합니다. 이는 단일 코어에서 여러 스레드가 번갈아가며 실행되는 것을 포함합니다. 동시성은 단일 코어 시스템에서도 유용하며, 여러 작업을 효율적으로 관리하고 동시에 진행할 수 있도록 합니다.
병렬성(parallelism)은 여러 작업을 동시에 처리하는 것을 의미합니다. 이는 다중 코어 시스템에서 각각의 코어가 병렬적으로 작업을 처리하는 것을 포함합니다. 병렬성은 여러 코어를 활용하여 작업을 동시에 처리함으로써 전체적인 처리 속도를 향상시킬 수 있습니다.
요약하면, 동시성은 단일 코어에서 여러 작업을 번갈아가며 실행하는 것을 의미하고, 병렬성은 여러 코어를 사용하여 작업을 동시에 실행하는 것을 의미합니다. 동시성과 병렬성은 모두 여러 작업을 효율적으로 처리하기 위한 방법이지만, 그 실행 방식에 있어서 차이가 있습니다.