함수형 프로그래밍이 주는 이점을 이해해보자
동작 파라미터화란?
public void printString(String input) {
System.out.println("hello " + input);
}
본래 인수는 항상 객체를 받는다. 위의 예시에서는 String input 객체가 해당된다. 우리는 함수형 프로그래밍을 위해 동작 (함수) 를 전달하는 방법을 몰색한다. 그 방법은 간단하게 보면 다음과 같다.
Thread 인스턴스 생성을 예시로 보자.
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run!");
}
});
우리는 run() 이라는 동작 (함수) 를 전달하고 싶다. 그러나 인수는 함수가 아니라 객체가 되어야한다. 그래서 방법은 간단하게 객체 안에 함수를 넣는 것이다. 여전히 Runnable 이라는 객체를 전달하고 있고 그 안에 run() 메소드를 전달한다.
위의 예시에서는 Runnable 클래스를 선언하자마자 인스턴스화 하였다. 이를 익명클래스라고 부른다. 한 번만 사용될 클래스의 경우 이렇게 선언하자마자 소비해주는 형태가 바람직한다.
동작 파라미터화가 얼마나 간결하게 만들어주는지 직접 보자
public static class Apple {
String color;
int weight;
public Apple(String color, int weight) {
this.color = color;
this.weight = weight;
}
}
사과 클래스 선언은 다음과 같다. 이젠 사과의 다양한 property를 print 하고 싶다. 그렇다면 대개는 아래의 코드를 작성하게 된다.
public static void main(String[] args) {
List<Apple> lst = Arrays.asList(new Apple("black", 10), new Apple("red", 20));
// color 프린트
for(Apple apple: lst) {
System.out.println(apple.color);
}
// weight 프린트
for(Apple apple: lst) {
System.out.println(apple.weight);
}
}
하지만 만약에 apple 수확날짜인 date 도 출력을 하고 싶어졌다. 그렇다면 또 for문을 돌려야하는 걸까?
사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성 (커스터마이즈된 다양한 toString 메서드와 같이) 할 수 있도록 파라미터화된 prettyPrintApple 메서드를 구현해보자.
동작 파라미터화 첫 번째 시도 - 추상적 조건으로 필터링
prettyPrintApple 이라는 메소드에 어떻게 프린트해야될지 동작을 인수로 전달해주고 싶다. 하지만 여전히 우리는 객체만 인수로 전달이 가능하다. 이럴 때 객체 안에 동작 함수를 넣으면 해결된다.
그래서 Interface로 추상화하며 인수로 오는 모든 객체는 메소드 toPrettyString() 이 있을 것을 보장한다.
public class Main {
public static void main(String[] args) {
List<Apple> lst = Arrays.asList(new Apple("black", 10), new Apple("red", 20));
prettyPrintApple(lst, new prettyPrintAppleColor());
prettyPrintApple(lst, new prettyPrintAppleWeight());
}
public static void prettyPrintApple(List<Apple> inventory, PrettyPrintInterface condition) {
for(Apple apple: inventory) {
String s = condition.toPrettyString(apple);
System.out.println("s = " + s);
}
}
public static class prettyPrintAppleColor implements PrettyPrintInterface{
@Override
public String toPrettyString(Apple apple) {
return apple.color;
}
}
public static class prettyPrintAppleWeight implements PrettyPrintInterface {
@Override
public String toPrettyString(Apple apple) {
return String.valueOf(apple.weight);
}
}
public static interface PrettyPrintInterface {
String toPrettyString(Apple apple);
}
}
❗추상화와 구현을 썼지만 문제점이 있다
- 인터페이스 정의와 그것에 대한 구현 코드 때문에 코드가 복잡해짐
- 가독성 떨어짐
동작 파라미터화 두 번째 시도 - 익명클래스 사용
앞에서 설명했던 익명 클래스를 사용하면 선언과 동시에 바로 인스턴스화하여 사용이 가능하다. 얼마나 간단해지는지 코드로 확인해보자.
public class Main {
public static void main(String[] args) throws IOException {
List<Apple> lst = Arrays.asList(new Apple("black", 10), new Apple("red", 20));
prettyPrintApple(lst, new PrettyPrintInterface() {
@Override
public String toPrettyString(Apple apple) {
return apple.color;
}
});
prettyPrintApple(lst, new PrettyPrintInterface() {
@Override
public String toPrettyString(Apple apple) {
return String.valueOf(apple.weight);
}
});
}
public static interface PrettyPrintInterface {
String toPrettyString(Apple apple);
}
}
인터페이스에 대한 구현 부분이 없으니 확연히 간단해짐을 확인할 수 있다.
여기서 더 간단하게 만들어버릴 수 있다.
동작 파라미터화 세 번째 시도 - 람다 사용
public class Main {
public static void main(String[] args) throws IOException {
List<Apple> lst = Arrays.asList(new Apple("black", 10), new Apple("red", 20));
prettyPrintAppleLambda(lst, (Apple apple) -> {
System.out.println(apple.color);
});
prettyPrintAppleLambda(lst, (Apple apple) -> {
System.out.println(apple.weight);
});
}
private static void prettyPrintAppleLambda(List<Apple> inventory, Consumer<Apple> o) {
for(Apple apple: inventory) {
o.accept(apple);
}
}
}
이게 코드 끝! 람다가 있다는 것만 알아두고 다음 장에서 람다에 대해 알아보도록 한다.
'📗Java' 카테고리의 다른 글
[Java] Reflection API (5) | 2023.09.24 |
---|---|
[모던자바인액션] 람다 표현식 (1) | 2023.09.14 |
[Java] Garbage Collection (0) | 2023.09.07 |
[Java] 자바 가상 머신 (JVM) 과 GC (1) | 2023.09.06 |
[Java] 쓰레드 동기화 (0) | 2023.08.29 |