공부한 내용을 정리한 글입니다.
틀린 정보가 있을 수 있습니다.
동작 파라미터화
- 기존 문제 : 변화에 대응하기 힘들다
- ex ) 특정 옵션에 대한 필터링 메서드가 존재할 때 필터링 옵션을 추가하거나 수정하고 싶다면 계속해서 코드를 수정해야한다. 이는 코드의 유연성이 떨어진다고 볼 수 있다.
/*
* 조건에 따라 코드가 계속해서 수정되어야 한다.
* 유언성이 떨어지는 코드라고 할 수 있다.
*/
/* 파란 사과 필터링 */
public List<Apple> filterBlueApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(BLUE.equals(apple.color()))
result.add(apple);
}
return result;
}
/* 입력된 색으로 사과 필터링 */
public List<Apple> filterApplesByColor(List<Apple> inventory, Color color){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(color.equals(apple.color()))
result.add(apple);
}
return result;
}
/* 색과 무게로 사과 필터링 */
public List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag){
List<Apple> result = new ArrayList<>();
boolean colorFilter = flag && color.equals(apple.color());
boolean weightFilter = !flag && weight < apple.weight();
for(Apple apple : inventory){
if(colorFilter || weightFilter)
result.add(apple);
}
return result;
}
- 해결 : 동작 파라미터화
- 어떻게 실행될지 결정되지 않은 코드 블럭. 이 코드 블럭의 실행은 나중으로 미뤄진다.
- 즉, 제공된 코드 블럭에 따라 메서드의 동작이 바뀐다.
- 위 코드처럼 조건에 따라 코드가 변화해야 할 때, 주어지는 조건(참 or 거짓을 반환하는 함수) 을 “predicate”라고 한다. ‘함수형 인터페이스’ 라고도 한다.
- 함수형 인터페이스의 종류 : Predicate, Consumer, Supplier, Function, Comparator 등..
- 함수형 인터페이스는 람다 함수로 일회성으로 간단히 구현되어 사용될 수 있다.
- 전략 패턴 : 런타임에 동작 방식을 선택하는 디자인 패턴 기법
/* 선택 조건을 결정하는 인터페이스 : predicate */
public interface ApplePredicate{
boolean test(Apple apple);
}
/* predicate 구현 */
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Ovverride
public boolean test(Apple apple){
return apple.weight() > 200;
}
}
public class AppleBlueColorPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple){
return BLUE.equals(apple.color());
}
}
/* predicate 사용한 사과 필터링 구현 */
public List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(predicate.test(apple))
result.add(apple);
}
return result;
}
/* predicate 사과 필터링 사용 예시 */
List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> blueApples = filterApples(inventory, new AppleBlueColorPredicate());
/* 람다 함수로 함수 인터페이스를 구현한 사과 필터링 사용 예시 */
List<Apple> sweetApples = filterApples(inventory, (apple) -> apple.sugar() > 10); // 당도 10 이상
List<Apple> bigApples = filterApples(inventory, (apple) -> apple.radius() > 5); // cm
---
함수형 인터페이스 & 람다 함수
함수형 인터페이스
- 추상화 메서드가 하나만 존재하는 인터페이스
- ex ) Consumer, Supplier, Function, Comaparator, Predicate 등
- Consumer : 입력값을 받아 소비하고 반환값이 없는 함수형 인터페이스
- Supplier : 입력값 없이 값을 생성하여 반환하는 함수형 인터페이스
- Comparator : 두 객체를 비교하여 정렬 기준을 제공하는 함수형 인터페이스
- Predicate : 조건을 평가하여 true 또는 false 를 반환하는 함수형 인터페이스
람다 함수 (표현식)
- 익명 클래스를 대신해 함수형 인터페이스를 간단히 구현하여 사용할 수 있는 문법
- 인자 타입이 함수형 인터페이스가 아니면 람다를 인자로 전달할 수 없다.
@FunctionalInterface
- 함수형 인터페이스임을 가리키는 어노테이션
- 추상메서드가 한 개 이상인 인터페이스에 사용하면 컴파일 타임에 ‘Multiple nonoverriding abstract methods found in interface Foo’ 같은 에러가 발생할 수 있다.
- 내 생각 : 함수형 인터페이스 형식을 강제하기 위한 수단으로 보인다. 함수형 인터페이스를 만들고 사용할 때 @FunctionalInterface 를 사용하면 실수를 방지할 수 있을 것 같다.
함수 디스크립터
- 함수의 파라미터 리스트와 반환 타입을 보여주는 식이다.
- (Apple apple) → apple.getColor().euqals(RED);
- 위 람다 함수의 디스크립터는 ‘Apple → boolean’ 이다.
람다의 형식 검사, 형식 추론 (type)
- 문제 : 람다는 함수형 인터페이스의 인스턴스와 같다. 하지만 람다식만으로는 어떤 함수형 인터페이스를 구현하는지 알 수 없다.
- 해결 : 이는 람다가 사용되는 context 로 람다의 형식을 추론할 수 있기 때문이다.
- 형식 검사 및 추론 :
- 람다를 대입하는 ‘변수’, 람다가 전달되는 메서드의 ‘파라미터’, 사용된 ‘추상메서드’ 에 따라 람다의 형식이 결정된다.
- 형식 검사 및 추론 과정 예시 :
- 람다가 사용된 메서드 정의 확인 → 파라미터 Predicate
확인 → 추상메서드 test(Apple apple) 확인 → 람다의 함수 디스크립터 ‘Apple → boolean’ 결정
- 람다가 사용된 메서드 정의 확인 → 파라미터 Predicate
같은 람다로 여러 함수 인터페이스 구현
- 형식 추론에 의해 같은 람다로 여러 함수 인터페이스를 구현할 수 있다.
- 다음 두 람다 함수는 ‘() → int’ 로 함수 디스크립터가 같다.
- Callable
c = () → 42; - PrivilegedAction
p = () → 42;
- Callable
람다의 지역변수 사용 (람다 캡쳐링)
- 람다는 자신에게 주어진 인자를 제외하고도 외부에서 정의된 ‘한 번만 할당되는 지역변수’를 사용할 수 있다. 이를 람다 캡쳐링이라고 한다.
- 즉, 외부 지역변수는 ‘final 이거나 선언 후 값이 바뀌지 않아야 한다.’
- 인스턴스 변수 : 클래스 전역에 선언된 static (클래스 변수) 이 아닌 멤버 변수
- 지역 변수 : 메서드 내부에 선언된 변수
- 이유 :
- 람다가 스레드A에서 실행되는 경우, 지역 변수를 다른 스레드B에서 할당했다면, 스레드A가 실행되는 동안 스레드B가 사라졌을 때 변수 할당이 해제될 수 있다. 따라서 람다는 지역 변수의 원본이 아닌 복사본을 사용한다.
- 예를 들어, 새로운 스레드에서 수행할 동작을 람다 함수(Runnable)로 만들고 이 스레드 객체를 생성할 메서드를 다른 스레드에서 호출한다면 람다 함수 내부 로직과 람다 함수가 사용할 지역 변수가 소속된 스레드는 서로 달라진다. (스택의 스레드 영역이 다름)
- 스레드1 : 람다함수 로직 실행
- 스레드2 : 스레드1 객체 생성을 위한 메서드 호출, 지역 변수 포함
- 따라서 람다 함수는 동기화 문제 방지를 위해 외부 지역변수의 복사본을 사용해야 하고, 이 복사본의 값이 바뀌지 않아야 하므로 한 번만 할당되는 지역변수를 사용해야 하는 것이다.
- 이 말은 람다가 외부 지역 변수의 값을 바꿀 수 없다는 것과 같고, 람다는 변수가 아닌 ‘값’에 국한되어 어떤 동작을 수행한다는 것을 의미한다.
- 인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없다.
람다 함수를 사용하여 함수형 인터페이스 구현 및 사용 예시
/* Consumer */
import java.util.function.Consumer;
Consumer<String> printConsumer = s -> System.out.println(s);
printConsumer.accept("Hello, Consumer!"); // Consumer 의 추상메서드 aceept 구현 및 호출
/* Supplier */
import java.util.function.Supplier;
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Supplier 의 추상메서드 get 구현 및 호출
/* Comparator */
import java.util.function.Comparator;
String[] animals = {"cat", "elephant", "giraffe", "hippo"};
Arrays.sort(animals, (a, b) -> a.length() - b.length()); // sort() 내부에서 Comparator 사용
System.out.println(Arrays.toString(animals));
/* Predicate */
import java.util.function.Predicate;
Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println(isEven.test(4)); // Predicate 의 추상메서드 test 구현 및 호출
동작 파라미터화 & 람다 연습
import java.util.*;
import java.util.function.Predicate;
public class Main{
public static void main(String[] args){
List<Apple> list = new ArrayList<>();
list.add(new Apple(5, new Color("red"), "1"));
list.add(new Apple(10, new Color("blue"), "2"));
list.add(new Apple(15, new Color("green"), "3"));
list.add(new Apple(20, new Color("red"), "4"));
list.add(new Apple(13, new Color("blue"), "5"));
list.add(new Apple(6, new Color("green"), "6"));
Predicate<Apple> pred1 = apple -> apple.getRadius() > 10;
Predicate<Apple> pred2 = apple -> apple.getName().equals("4");
Predicate<Apple> pred3 = apple -> apple.getColor().getColor().equals("red");
List<Apple> list2 = filter(list, pred3);
for(Apple a : list2){
System.out.print(a.getName() + "\t");
}
System.out.println();
}
// filter 함수
public static List<Apple> filter(List<Apple> inventory, Predicate<Apple> pred){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(pred.test(apple))
result.add(apple);
}
return result;
}
// apple 클래스
static class Apple{
private int radius;
private Color color;
private String name;
public Apple(int radius, Color color, String name){
this.radius = radius;
this.color = color;
this.name = name;
}
public int getRadius(){
return radius;
}
public Color getColor(){
return color;
}
public String getName(){
return name;
}
}
// color 클래스
static class Color{
private String color;
public Color(String color){
this.color = color;
}
public String getColor(){
return color;
}
}
}
람다 연습: ExecutorService, Callable, 실행 스레드 이름 출력
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableThreadTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> threadNames = new ArrayList<>();
for(int i = 0; i < 100; i++) {
threadNames.add(executorService.submit(() -> {
Thread.sleep(100); // 스레드 생성 확인을 위해 지연 발생
return Thread.currentThread().getName();
}));
}
for(Future<String> threadName : threadNames){
try {
System.out.println(threadName.get());
} catch (Exception e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
}
/* 실행 결과 : 지연발생 전에는 물리적 코어 개수 만큼만 스레드 생성
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-6
...
pool-1-thread-94
pool-1-thread-95
pool-1-thread-96
pool-1-thread-97
pool-1-thread-98
pool-1-thread-99
pool-1-thread-100
*/
동작 파라미터화 / 람다 리뷰
- 동작 파라미터화 없이는 요구사항이 계속 변화함에 따라 중복된 메서드를 계속해서 추가해야 했다. (filterGreenApple, filterRedApple …)
- 이런 중복 코드와 개발 비용을 줄이기 위해 동작 파라미터화를 도입했다.
- 하지면 이것 역시 언어적 한계로 Predicate 같은 것을 클래스로 구현해서 인스턴스화한 뒤 메서드 인자로 넣어줘야했다.
- 이렇게 되면 이전에 중복 코드를 계속 만들어주는 것과 비슷하게 새로운 Predicate 구현체를 만들어 줘야 하거나 보기에 좋지 않은 익명 클래스를 작성해야했다.
- 이런 문제는 일회성 함수를 간단히, 가독성 좋게 만들 수 있는 람다 함수가 도입되어 해결됐다.
- 이런 과정을 보면 결국 람다 함수의 등장이 필수적이었을 거라는 생각을 하게된다.
- 만약 인자로 들어가야 할 함수가 이미 작성되어 있다면 람다 함수를 작성하는 것보다 간결하게 메서드 참조 (::) 를 사용해서 표현할 수 있다.
'학습 기록 > 자바' 카테고리의 다른 글
Enum 비교는 '==' 을 사용하자 (0) | 2024.02.22 |
---|---|
UnsupportedOperationException / List.copyOf / List.of 에 대하여 (1) | 2024.01.22 |
자바는 call by value 만 존재한다 (0) | 2023.07.02 |