| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 다이나믹 프록시
- OS
- 스프링
- Reflection
- 자바
- 최소 신장 트리
- 운영체제
- 모던 자바 인 액션
- Deadlock
- Junit5
- Spring
- CS
- MST
- 알고리즘
- 약수
- java
- spring security
- redis
- 리플렉션
- 객체지향
- Python
- test
- 프록시
- 모던자바
- 스프링 시큐리티
- BOJ
- 백준
- 파이썬
- 문자열
- proxy
- Today
- Total
Dev 달팽이 @_''
[모던 자바 인 액션] 2장 - 동작 파라미터화 코드 전달하기 본문
동작 파라미터화(behavior parameterization)란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.
이 코드 블록의 실행은 나중으로 미뤄진다.
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;
}
녹색 사과를 필터링하는 메서드를 만들었다. 그러나 만약 빨간 사과도 필터링 하고 싶어진다면 filterRedApples라는 새로운 메서드를 만들어야 한다. 더 나아가 더 많은 색을 필터링을 한다고 한다면 적절하게 대응하기란 어려운 일이다. 이런 상황에서는 다음과 같은 좋은 규칙이 있다.
- 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화한다.
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;
}
색을 파라미터화하여 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들었다. 그런데 갑자기 '색 이외에도 가벼운 사과와 무거운 사과(150g 이상)로 구분할 수 있다면 좋겠다'는 요구사항이 추가되면 어떻게 해야 할까?
색과 마찬가지로 무게의 기준도 파라미터로 추가하여 대응할 수 있다.
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if ( apple.getWeight() >= weight ) {
result.add(apple);
}
}
return result;
}
위 코드도 좋은 해결책이라 할 수 있겠지만, 구현 코드를 자세히 보면 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다. 이는 소프트웨어 공학의 DRY(Don't Repeat Yourself, 같은 것을 반복하지 말 것) 원칙을 어기는 것이다.
탐색과정을 고쳐서 성능을 개선하려면 한 줄이 아니라 메서드 전체 구현을 고쳐야하므로 엔지니어링 적으로 비싼 대가를 치러야 한다.
색과 무게를 filter라는 메서드로 합치는 방법도 존재한다.(실전에서는 절대 이 방법을 사용하지 말아야 한다.)
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가 뭘 의미하는 지 알 수 가 없다. 또한 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.
지금 까지는 문자열, 정수, 불리언 등의 값으로 filter 메서드를 파라미터화 했다. 문제가 잘 정의되어 있는 상황에서는 이 방법이 잘 동작할 수 있지만, 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것이다.
다음은 동작 파라미터화를 이용해서 유연성을 얻는 방법이다.
2.2 동작 파라미터화
파라미터를 추가하는 방법이 아닌 변화하는 요구사항에 좀 더 유연하게 대응할 수 있는 방법이 필요하다.
선택의 조건을 다음처럼 결정할 수 있다.
사과의 어떤 속성에 기초해서 불리언값을 반환(예를 들어 사과가 녹색인가? 150그램 이상인가?) 하는 방법이 있다.
참 또는 거짓을 반환하는 함수를 Predicate라고 한다. 선택 조건을 결정하는 인터페이스를 정의하자.
public interface ApplePredicate {
boolean test (Apple apple);
}
다음 예제처럼 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의 할 수 있다.
public class AppleHeavyWeigthPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
public class AppleGrrenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}

위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인 패턴(Strategy design pattern)이라고 부른다. 전략 디자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
2. 2. 1 네 번째 시도 : 추상적 조건으로 필터링
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory) {
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
코드/동작 전달하기
이제 필요한 대로 다양한 ApplePredicate를 만들어서 filterApples 메서드로 전달할 수 있다.
새로운 동작을 정의하는 것은 test 메서드이지만, 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야한다.
한 개의 파라미터, 다양한 동작
컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다. 한 메서드가 다른 동작을 수행하도록 재활용할 수 있다. 따라서 유연한 API를 만들 때 동작 파라미터화가 중요한 역할은 한다.
2.3 복잡한 과정 간소화
현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredcate 인터페이스를 구성하는 여러 클래스를 정의한 다음에 인스턴스화해야 한다. 이는 상당히 번거러운 작업이며 시간 낭비이다.
자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스를 제공한다.
2.3.1 익명 클래스
익명 클래스는 자바 지역 클래스(블록 내부에 선언된 클래스)와 비슷한 개념이다. 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 즉, 즉석에서 필요한 구현을 만들어서 사용할 수 있다.
2.3.2 다섯 번째 시도 : 익명 클래스 사용
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple) {
return RED.equals(apple.getColor());
}
});
익명 클래스로도 아직 부족한 점이 있다.
- 여전히 많은 공간을 차지한다.
- 많은 프로그래머가 익명 클래스의 사용에 익숙치 않다.
2.3.3 여섯 번째 시도 : 람다 표현식 사용
List<Apple> result = 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 e : list) {
if(p.test(e)) {
result.add(e);
}
}
return result;
}
이제 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용할 수 있다. 다음은 람다 표현식을 사용한 예제이다.
List<Apple> redApples = filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i%2 == 0);
2.4 실전 예제
Comparator로 정렬하기, Runnable로 코드 블록 실행하기, Callable을 결과로 반환하기, GUI 이벤트 처리하기 예제를 소개한다.
2.4.1 Comparator로 정렬하기
Comparator를 구현해서 sort 메서드의 동작을 다양화 할 수 있다. 익명 클래스를 이용하여 사과 무게가 적은 순서로 목록에서 사과를 정렬하면 다음과 같다.
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
이를 람다 표현식으로 구현하면 다음과 같다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
2.4.2 Runnable로 코드 블록 실행하기
자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다. 자바 8까지는 Thread 생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않은 void run 메소드를 포함하는 익명 클래스가 Runnable 인터페이스를 구현하도록 하는 것이 일반적인 방법이었다.
public interface Runnable {
void run();
}
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello world");
}
});
자바 8부터 지원하는 람다 표현식을 이용하면 다음과 같이 스레드 코드를 구현할 수 있다.
Thread t = new Thread(()->System.out.println("Hello world"));
2.4.3 Callable을 결과로 반환하기
ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다. ExecutorService를 이용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다.
public interface Callable<V> {
V call();
}
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
@Override
public String call() thorws Exception {
return Thread.currentThread().getName();
}
});
이를 람다를 이용하면 다음과 같다.
Future<String> threadName = executorService.submit(()->Thread.currentThread().getName());
2.4.4 GUI 이벤트 처리하기
일반적으로 GUI 프로그래밍은 마우스 클릭이나 문자열 위로 이동하는 등의 이벤트에 대응하는 동작을 수행하는 식으로 등장한다. GUI 프로그래밍은 모든 동작에 반응할 수 있어야 하기 때문에 변화에 대응할 수 있는 유연한 코드가 필요하다.
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
람다 표현식으로 다음처럼 구현할 수 있다.
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
2.5 마치며
- 동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있다.
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다. 하지만 자바 8 이전에는 코드를 지저분하게 구현해야 했다. 익명 클래스로도 어느 정도 코드를 깔끔하게 만들 수 있지만 자바 8에서는 인터페이스를 상속받아 여러 클래스를 구현해야 하는 수고를 없앨 수 있는 방법을 제공한다.
- 자바 API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화할 수 있다.
'Java&Spring > 모던 자바 인 액션' 카테고리의 다른 글
| [모던 자바 인 액션] 4장 스트림 소개 (0) | 2022.06.28 |
|---|---|
| [모던 자바 인 액션] 3장 람다 표현식 (0) | 2022.06.13 |
| [모던 자바 인 액션] 1장 - 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? (0) | 2022.05.28 |