동작 파라미터화를 이용하면 자주 바뀌는 요구사항에 효과적으로 대응 가능
동작 파라미터화란 하직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미 (filter 개념으로 이해하면 될듯)
예를 들어 컬렉션을 처리할 때 다음과 같은 메서드를 구현한다고 가정하자
리스트의 모든 요소에 대해서 ‘어떤 동작’을 수행할 수 있음 리스트 관련 작업을 끝낸 다음에 ‘어떤 다른 동작’을 수행할 수 있음 에러가 발생하면 ‘정해진 어떤 다른 동작’을 수행할 수 있음
- 동적 파라미터화는 ‘특정 상황’에 ‘특정 행동’이 필요한 경우
- 내부 객체로를 처리 불가능 한 경우 (해당 동작 책임을 가진 객체가 없는 경우)
- ‘특정 상황’이 존재하지 않는다면 파라미터화 하지 않고 처리하는 것이 좋아보임
내가 이해하기로는 약간 레고를 쌓는것처럼 동작 하나하나를 파라미터화 시켜 함수에 전달 → 함수는 해당 동작을 받아 수행하는 역활.
함수의 추상화라고 볼 수 있을듯
2.1 변화하는 요구사항에 대응하기
기존의 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링 하는 기능을 추가한다고 가정하자.
2.1.1 첫 번째 시도 : 녹색 사과 필터링
저번에 한번 쳐본 코드다.
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (GREEN.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
지금 코드는 녹색 사과만 필터링 할 수 있다.
여기서 조건을 추가해 확장하려면 어떤 방법을 사용할 수 있을까?
2.1.2 두 번째 시도 : 색을 파라미터화
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor().equals(color)) {
result.add(apple);
}
}
return result;
}
내가 예전에 자주 사용하던 방법.
하지만 해당 방법은 확장성 측면에서 아주 취약하다.
예를들어 색 이외에도 무게로 필터링하고싶다면? 메소드를 하나 더 선언해줘야할것.
실제로 이것때문에 삽질좀 많이 했다.
2.1.3 세 번째 시도 : 가능한 모든 속성으로 필터링
public static List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if ((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)) {
result.add(apple);
}
}
return result;
}
진짜 레전드 우끼끼 코딩법
부끄럽지만 내가 예전에 이런식으로 많이 코딩했다.
대체 true와 false는 뭘 의미하는것이며 앞으로 요구사항이 변경되면 또 우끼끼 코딩해야한다.
여기서 날으는 코딩몽키가 되지 않기 위해 동적 파라미터화를 사용할것이다.
2.2 동작 파라미터화
예전에 응용해봤던 프레디케이트를 사용해 선택 조건을 결정하는 인터페이스를 정의해보자
public interface ApplePredicate {
boolean test (Apple apple);
}
// 무거운 사과만
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
// 녹색 사과만
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}
Predicate를 상속하는 조건 2개를 만들어줬다.
사과를 선택하는 전략을 캡슐화 해 filter 메서드를 다르게 동작시킬 것이다.
이를 전략 디자인 패턴이라고 부른다.
전략 디자인 패턴은 각 알고리즘을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
위 예시에서는 ApplePredicate가 알고리즘 패밀리고 AppleHeavyWeightPredicate와 AppleGreenColorPredicate가 전략이다.
이렇게 동작 파리마터화, 즉 메서드가 다양한 동작을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
2.2.1 네 번째 시도 : 추상적 조건으로 필터링
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
// Predicate 객체로 사과 검사 조건을 캡슐화
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
가독성도 좋아지고 사용하기도 편해졌다!
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = inventory.stream()
.filter(p::test)
.toList();
return result;
}
stream을 사용하면 좀 더 깔끔해지는 느낌
이제 우리는 150그램이 넘는 빨간 사과를 검색해달라고 하면 ApplePredicate를 추가하기만 하면 된다!
public class AppleRedAndHeavyPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor()) && apple.getWeight() > 150;
}
}
바로 이렇게!
이렇게 되면 filterApples 함수를 변경하지 않고 Predicate 만 추가해주면 메서드의 동작이 결정된다.
즉 우리는 filterApples 메서드의 동작을 파라미터화한 것이다!
위 그림에서 보여주는 것처럼 위 예제에서 가장 중요한 구현은 test 메서드다.
filterApples 메서드의 새로운 동작을 정의하는 것이 test 메서드다.
안타깝게도 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야 한다.
이는 ‘코드를 전달’ 할 수 있는 것이다 다름없다.
람다를 사용하면 ApplePredicate 클래스를 정의하지 않고도 여러 표현식을 filterApples 메서드로 전달하는 방법을 설명한다.
한 개의 파라미터, 다양한 동작
지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리(어떤 동작) 할 수 있다는 것이 동작 파라미터화의 강점이다.
퀴즈 2-1
유연한 prettyPrintApple 메서드 구현하기
public interface AppleFormatter {
String accept(Apple apple);
}
public class checkWeight implements AppleFormatter {
@Override
public String accept(Apple apple) {
return apple.getWeight() + "Kg";
}
}
public class heavyWeight implements AppleFormatter {
@Override
public String accept(Apple apple) {
return apple.getWeight() > 150 ? "Heavy weight" : "light weight";
}
}
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter appleFormatter) {
for (Apple apple : inventory) {
String output = appleFormatter.accept(apple);
System.out.println(output);
}
}
동작 파라미터화 성공!
2.3 복잡한 과정 간소화
사실 지금 이 과정도 ApplePredicate 인터페이스를 구현해야해서 불필요한 작업이 추가된다.
계속해서 재사용되는 조건이면 Predicate를 캡슐화하는것이 좋지만 로직과 관련없는 코드가 많이 추가되는것도 어쩔 수 없다.
이를 해결하기 위해 익명 클래스 를 제공한다.
2.3.1 익명 클래스
익명 클래스 는 자바의 지역클래스와 비슷한 개념이다.
익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
2.3.2 다섯 번째 시도 : 익명 클래스 사용
다음은 익명 클래스를 이용해서 ApplePredicate를 구현하는 객체를 만드는 방법으로 필터링 예제를 다시 구현할것
List<Apple> apples = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
진짜 레전드 문법 절대 안쓸듯
실제로 익명 클래스는 가독성도 좋지 않고 많은 프로그래머가 사용에 익숙치 않다고 한다.
코드의 장황함 은 나쁜 특성이다. 유지보수하는데 오래걸리고(그래보임) 읽는 즐거움을 빼앗는 요소다(그래보임) ****
이 문제를 해결하기 위해 람다 표현식을 사용한다.
2.3.3 여섯 번째 시도 : 람다 표현식 사용
자바 8의 람다 표현식을 이용해서 위 예제코드를 다음처럼 간단하게 재구현할 수 있다.
List<Apple> apples = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
절 대 익 명 클 래 스 를 사 용 하 지 마
2.3.4 일곱 번째 시도 : 리스트 형식으로 추상화
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
result.add(t);
}
}
return result;
}
이제 사과 말고 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다.
그리고 일일히 구현하지 않아도 java.util.function.Predicate 여기에 잘 구현되어있는듯
이 함수를 람다 표현식으로 사용해보자.
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);
딸깍으로 적용 가능
2.4 실전 예제
동작 파라미터화 패턴은 동작을 캡슐화(Predicate) 한 다음에 메서드로 전달해서 메서드의 동작을 파라미터화 한다.
다음으로는 Comparator로 정렬하기, Runnable로 코드 블록 실행하기, GUI 이벤트 처리하기 등 세 가지 예제를 소개한다.
2.4.1 Comparator로 정렬하기
이제 정렬에 대해 알아볼것이다.
filter 조건이 바뀌듯 정렬 조건도 시시각각 바뀔 수 있다.
이를 대비해 다양한 정렬 동작을 수행할 수 있는 코드가 필요하다.
자바 8의 List에는 sort 메서드가 포함되어 있다.
다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.
public interface Comparator<T> {
int compare(T a, T b);
}
PintOS에서 정렬 조건을 설정해주던 코드가 생각이 난다.
Comparator를 구현해서 sort 메서드의 동작을 다양화 할 수 있다.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a, Apple b) {
return a.getWeight().compareTo(b.getWeight());
}
});
이처럼 Comparator 객체를 사전에 정의해둔다면 동작 파라미터와 동일하게 여러 조건으로 사과를 비교할 수 있을것이다.
이 문법 또한 lambda식으로 간단하게 변경할 수 있다.
inventory.sort((Apple a, Apple b) -> a.getWeight().compareTo(b.getWeight()));
절 대 익 명 함 수 를 사 용 하 지 마
2.4.2 Runnable로 코드 블록 실행하기
병렬로 코드 블록일 실행하는 도중, 어떤 코드를 실행할 것인지 설정할 수 있을까?
나중에 실행할 수 있는 코드를 구현할 방법이 필요하다.
이때 사용할 수 있는 방법이 Runnable 인터페이스를 구현하는것
자바에서는 Runnable 인터페이스를 이용해서 실행할 코드 블록을 지정할 수 있다.
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello World");
}
});
이제 쓱 보면 알겠지만 이 방식도 람다 표현식으로 구현 가능하다.
Thread t = new Thread(() -> System.out.println("Hello World"));
PintOS에서 구현해봤던 clone system call이 생각난다.
2.4.3 Callable을 결과로 반환하기
ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다.
이 인터페이스를 사용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다는 점이 Runnable을 이용하는 방식과는 다르다.
Callable 인터페이스는 Runnable의 업그레이드 버젼이라고 생각해두자.
- 태스크란??
- 작업 단위: 태스크는 독립적으로 실행 가능한 코드의 단위입니다. 이는 하나의 메서드나 일련의 연산들을 포함할 수 있습니다.
- 비동기 실행: 태스크는 주로 비동기적으로 실행됩니다. 즉, 메인 프로그램의 흐름과 별개로 실행될 수 있습니다.
- 병렬 처리: 여러 태스크를 동시에 실행함으로써 병렬 처리를 가능하게 합니다.
- 결과 반환: Callable 인터페이스를 사용하면 태스크가 결과를 반환할 수 있습니다. 이는 Runnable과의 주요 차이점입니다.
- 유연성: ExecutorService를 사용하면 태스크의 실행을 더 유연하게 관리할 수 있습니다. 예를 들어, 태스크를 스레드 풀에 제출하여 효율적으로 실행할 수 있습니다.
- 추상화: 태스크는 "무엇을 할 것인가"를 정의하며, 실행 방식(어떤 스레드에서, 언제 실행될지)과는 분리됩니다.
- 에러 처리: Callable을 사용하면 태스크 실행 중 발생한 예외를 더 쉽게 처리할 수 있습니다.
- 취소 가능: ExecutorService와 Future를 사용하면 실행 중인 태스크를 취소할 수 있습니다.
- 리턴 값:
- Runnable: 리턴 값이 없습니다 (void run() 메서드).
- Callable: 제네릭 타입의 값을 리턴합니다 (V call() 메서드).
- 예외 처리:
- Runnable: checked 예외를 던질 수 없습니다.
- Callable: checked 예외를 던질 수 있습니다 (throws Exception).
- 사용 방식:
- Runnable: Thread 클래스의 생성자에 직접 전달할 수 있습니다.
- Callable: ExecutorService를 통해 실행해야 합니다.
- 결과 획득:
- Runnable: 결과를 얻으려면 별도의 방법을 사용해야 합니다 (예: 공유 변수).
- Callable: Future 객체를 통해 비동기적으로 결과를 얻을 수 있습니다.
- 함수형 인터페이스:
- 둘 다 함수형 인터페이스이지만, 메서드 시그니처가 다릅니다.
- Java 버전:
- Runnable: Java 1.0부터 존재
- Callable: Java 5에서 추가됨
Runnable과 Callable의 차이
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
이렇게 쓰레드풀에서 자원을 가져와 해당 쓰레드의 이름을 반환하는 작업을 동시성을 활용해서 수행할 수 있다.
이 코드도 람다를 사용해 줄일 수 있다.
Future<String> submit = executorService.submit(() -> Thread.currentThread().getName());
레전드 ㅋㅋ
2.4.4 GUI 이벤트 처리하기
GUI 프로그래밍에서도 변화에 대응할 수 있는 유연한 코드가 필요하다.
자바FX에서는 setOnAction 메서드에 EventHandler를 전달함으로써 이벤트에 어떻게 반응할지 설정할 수 있다.
2.5 마치며
- 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메소드 인수로 전달한다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.동작 파라미터화란 하직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미 (filter 개념으로 이해하면 될듯)리스트의 모든 요소에 대해서 ‘어떤 동작’을 수행할 수 있음 리스트 관련 작업을 끝낸 다음에 ‘어떤 다른 동작’을 수행할 수 있음 에러가 발생하면 ‘정해진 어떤 다른 동작’을 수행할 수 있음
'코딩딩 > Java' 카테고리의 다른 글
Synchronized와 Reentrantlock (0) | 2024.08.31 |
---|---|
모던 자바 인 액션 Chapter 3 (0) | 2024.08.26 |
모던 자바 인 액션 Chapter 1 (0) | 2024.08.15 |
List에 담긴 Entity를 DTO로 변환하며 정렬하는 방법 (0) | 2024.01.11 |
자바의 다형성 (0) | 2023.08.18 |