티스토리 뷰
아래 내용은 '모던 자바 인 액션'을 읽고 정리한 글로 책 내용의 순서를 따라간다.
상세한 내용이나 예시는 책과 상이할 수 있다.
예제 코드에서 사용되고 있는 스펙
- Java 15 preview (record라는 새로운 클래스 개념을 사용하기 위해서 해당 프리뷰 버전을 사용. 하위 버전의 경우는 일반 클래스를 생성한 후 getter를 만들고 사용하면 됨) - 참고
- JUnit 5
- Gradle 6.7
배경
- 소프트웨어 개발에서 요구사항은 항상 변한다.
- 이러한 요구사항을 반영하면서도 엔지니어링적인 비용이 가장 최소화될 수 있으면 좋다.
- 그뿐 아니라 새로 추가한 기능은 쉽게 구현할 수 있어야 하며 장기적인 관점에서 유지보수가 쉬어야 한다.
변화에 대응하기
첫 번째 시도 : 녹색 사과 필터링
enum Color { RED, GREEN } public List<Apple> filterGreenApples(List<Apple> inventory) { final var result = new ArrayList<Apple>(); for (final var apple : inventory) { if (GREEN.equals(apple.color())) { result.add(apple); } } return result; }
- 위의 코드는 녹색 사과를 필터링하는 것이다.
- 현재의 요구사항은 녹색 사과만 필터링하는 것이지만 빨간색 사과만 필터링하라는 요구사항이 올 수도 있다.
- 이럴 경우 위의 코드와 유사하게 작성될 것이다.
- 거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화해본다.
두 번째 시도 : 색을 파라미터화
public List<Apple> filterApplesByColor(List<Apple> inventory, Color color) { final var result = new ArrayList<Apple>(); for (final var apple : inventory) { if (color.equals(apple.color())) { result.add(apple); } } return result; }
지금까지 색을 이용한 필터링을 잘 구현했다.
여기서 색 이외에 무게를 이용해서 사과를 필터링하고 싶다는 요구사항이 생기면 어떻게 해야할까?
public List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) { final var result = new ArrayList<Apple>(); for (final var apple : inventory) { if (apple.weight() > weight) { result.add(apple); } } return result; }
색을 파라미터화 했듯이 무게를 파라미터화해서 위와 같이 해결할 수 있다.
하지만 색, 무게를 각각 파라미터화해서 조건에 적용한 것 외에는 대부분 중복되는 코드다.
이는 소프트웨어 공학의 DRY(do not repeat yourself) 원칙을 어기는 것이다.
세 번째 시도 : 가능한 모든 속성으로 필터링
public List<Apple> filterApples(List<Apple> inventory, Color color, int weight, boolean flag) { final var result = new ArrayList<Apple>(); for (final var apple : inventory) { if ((flag && apple.color().equals(color)) || (!flag && apple.weight() > weight)) { result.add(apple); } } return result; }
- 색과 무게를 합쳐서 하나의 filter를 구현하는 방법도 있지만 이것 또한 어떤 상황에서 색과 무게를 구분할지 별도의 기준이 필요하다.
- 위의 코드에서는 색과 무게 중 어떤 기준에 따라 필터링할지를 결정하기 위해 boolean 타입의 flag라는 파라미터를 사용했는데, 이 메서드를 사용하는 사용자 입장에서 어떤 것이 true이고 어떤 것이 false인지 불명확하다.
- 게다가 색과 무게 이외에
Apple
클래스에 또 다른 속성(크기, 모양, 출하지 등)이 부여된다면 현재의 코드로는 유연하게 대처할 수 없다. - 심지어 빨간색 사과 중에 무거운 사과를 필터링하고 싶다면? 결국 중복된 메서드를 계속해서 만들 수 밖에 없다.
지금까지는 문자열, 정수, 불리언 등의 값으로 filterApples
메서드를 파라미터화했다.
동작 파라미터화(Behavior Parameterization)
- 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블럭을 의미한다.
- 즉, 코드 블럭의 실행은 나중으로 미뤄진다.
- 결과적으로 코드 블럭에 따라 메서드의 동작이 파라미터화된다.
위의 예시를 다시 보자
우리는 선택 조건을 다음처럼 결정할 수 있다.
사과의 어떤 속성에 기초해서 불리언값을 반환 (예를 들어 사과가 녹색인가? 200그램 이상인가?)하는 방법이 있다.
참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다.
선택 조건을 결정하는 인터페이스를 정의하자.
public interface ApplePredicate { boolean test(Apple apple); }
다음과 같이 다양한 선택 조건을 대표하는
ApplePredicate
를 정의할 수 있다.public class AppleHeavyWeightPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.weight() > 200; } }
public class AppleGreenColorPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return GREEN.equals(apple.color()); } }
위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다.
이를 전략 디자인 패턴(strategy design pattern)이라고 부른다.
- 전략 디자인 패턴은 각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
filterApples
에서ApplePredicate
객체를 받아 애플의 조건을 검사하도록 메서드를 고쳐야 한다.- 이렇게 동작 파라미터화, 즉 메서드가 다양한 동작(또는 전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
이렇게 하면
filterApples
메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작(우리 예제에서는 프레디케이트)을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.네 번째 시도 : 추상적 조건으로 필터링
public List<Apple> filterApples(List<Apple> inventory, ApplePredicate applePredicate) { final var result = new ArrayList<Apple>(); for (final var apple : inventory) { if (applePredicate.test(apple)) { result.add(apple); } } return result; }
첫 번째 코드에 비해 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워졌다.
필요한 조건에 따라
ApplePredicate
를 만들어서filterApples
메서드에 전달해 주기만 하면 된다.이제 세 번째 시도 마지막 부분에서 언급했던 빨간색 사과 중에 무거운 사과를 다음과 같이 만들 수 있다.
public class AppleRedAndHeavyPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return RED.equals(apple.color()) && apple.weight() > 180; } }
이렇게 우리가 전달한
ApplePredicate
객체에 의해filterApples
메서드의 동작이 결정된다.위의 그림에서 보여주는 것처럼
ApplePredicate
의 실제 구현은test
메서드에 있다.안타깝게도 메서드는 객체만 인수로 받으므로
test
메서드를ApplePredicate
객체로 감싸서 전달해야 한다.- Java 8에 새롭게 추가된 람다 표현식을 이용하게 되면 여러 개의
ApplePredicate
를 정의하지 않고도test
메서드의 내부 구현filter
메서드로 전달할 수 있다.
- Java 8에 새롭게 추가된 람다 표현식을 이용하게 되면 여러 개의
지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.
Q. 퀴즈-1) 유연한 prettyPrintApple 메서드 구현하기
사과 리스트를 인수로 받아 다양한 방법으로 문자열을 생성(커스터마이즈된 다양한 toString 메서드와 같이)할 수 있도록 파라미터화된
prettyPrintApple
메서드를 구현하시오.예를 들어,
prettyPrintApple
메서드가 각각의 사과 무게를 출력하도록 지시할 수 있다.혹은 각각의 사과가 무거운지, 가벼운지 출력하도록 지시할 수 있다.
prettyPrintApple
메서드는 지금까지 살펴본 필터링 예제와 비슷한 방법으로 구현할 수 있다.대략적인 코드는 다음과 같다.
public void prettyPrintApple(List<Apple> inventory, ???) { for (Apple apple : inventory) { String output = ???.???(apple); System.out.println(output); } }
???
에 들어갈 내용을 생각해보면 된다.
A. 정답
-
우선 Apple을 인수로 받아 정해진 형식의 문자열로 반환할 수단이 있어야 한다.
public interface AppleFormatter { String accept(Apple a); }
이제 다음과 같이 다양한 동작을 하는
AppleFormatter
를 만든다.public class AppleFancyFormatter implements AppleFormatter { @Override public String accept(Apple apple) { String characteristic = apple.weight() > 200 ? "heavy" : "light"; return "A " + characteristic + " " + apple.color() + " apple"; } }
public class AppleSimpleFormatter implements AppleFormatter { @Override public String accept(Apple apple) { return "An apple of " + apple.weight() + "g"; } }
마지막으로
prettyPrintApple
메서드가AppleFormatter
객체를 인수로 받아 내부적으로 사용하도록 지시한다.public void prettyPrintApple(List<Apple> inventory, AppleFormatter appleFormatter) { for (final var apple : inventory) { String output = appleFormatter.accept(apple); System.out.println(output); } }
AppleFormatter
의 구현을 객체화한 다음에prettyPrintApple
의 인수로 전달한다.다음과 같은 데이터가 있다고 하자.
private final List<Apple> inventory = List.of( new Apple(Color.GREEN, 200), new Apple(Color.GREEN, 220), new Apple(Color.RED, 180), new Apple(Color.GREEN, 200), new Apple(Color.RED, 190));
@Test @DisplayName("AppleFancyFormatter를 이용한 사과 출력") void prettyPrintAppleWithAppleFancyFormatter() { quizAnswer.prettyPrintApple(inventory, new AppleFancyFormatter()); }
다음 코드를 실행한 결과다.
@Test @DisplayName("AppleSimpleFormatter를 이용한 사과 출력") void prettyPrintAppleWithAppleSimpleFormatter() { quizAnswer.prettyPrintApple(inventory, new AppleSimpleFormatter()); }
다음 코드를 실행한 결과다.
복잡한 과정 간소화
지금까지의 작업에서
filterApples
메서드로 새로운 동작을 전달하려면ApplePredicate
인터페이스를 구현하는 여러 클래스를 정의한 다음에 인스턴스화해야 한다.로직과 관련 없는 코드가 많이 추가되었다.
- 여기서 로직은 실제로 사과를 필터링하는
test
메서드 내부의 동작 test
메서드의 내부 동작을 구현하기 위해서ApplePredicate
인터페이스를 구현한 클래스들을 만들었고 인스턴스화했다.
- 여기서 로직은 실제로 사과를 필터링하는
자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(anonymous class) 라는 기법을 제공한다.
- 익명 클래스는 자바의 지역 클래스(local class, 블럭 내부에 선언된 클래스)와 비슷한 개념이다.
- 익명 클래스는 말 그대로 이름이 없는 클래스다.
- 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.
- 즉, 즉석에서 필요한 구현ㅇ르 만들어서 사용할 수 있다.
다섯 번째 시도 : 익명 클래스 사용
@Test @DisplayName("익명 클래스를 이용한 필터링") void filterApplesWithApplePredicateUsingAnonymousClassTest() { final var expected = List.of(new Apple(Color.RED, 180), new Apple(Color.RED, 190)); final var result = behaviorParameterizationExample.filterApples(inventory, new ApplePredicate() { @Override public boolean test(Apple apple) { return Color.RED.equals(apple.color()); } }); assertIterableEquals(expected, result); }
자바 스윙이나 AWT 같은 GUI 애플리케이션에서 이벤트 핸들러 객체를 구현할 때 익명 클래스를 종종 사용한다
익명 클래스를 사용해도 아직 부족한 점이 있다.
- 여전히 많은 공간을 차지한다. (클래스의 생성과 인스턴스화를 동시에 하는 것이지만 생성을 위해 메서드 내부에 클래스를 정의해야 한다.)
- 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다.
Q. 퀴즈-2) 익명 클래스 문제
- 다음 코드를 실행한 결과는 4, 5, 6, 42 중 어느 것일까?
public class MeaningOfThis {
public final int value = 4;
public void doIt() {
final var value = 6;
Runnable r = new Runnable() {
public final int value = 5;
@Override
public void run() {
final var value = 10;
System.out.println(this.value);
}
};
r.run();
}
}
class MeaningOfThisTest {
private final MeaningOfThis meaningOfThis = new MeaningOfThis();
@Test
@DisplayName("익명 클래스 문제")
void anonymousClassTest() {
meaningOfThis.doIt();
}
}
A. 정답
-
- 코드에서
this
는MeaningOfThis
가 아니라Runnable
을 참조하고 있으므로 5가 정답이다.
- 코드에서
여섯 번째 시도 : 람다 표현식 사용
Java 8의 람다 표현식을 이용해서 익명 클래스로 만든 예제 코드를 다음과 같이 간단하게 재구현할 수 있다.
@Test @DisplayName("람다를 이용한 필터링") void filterApplesWithApplePredicateUsingLambdaTest() { final var expected = List.of(new Apple(Color.RED, 180), new Apple(Color.RED, 190)); final var result = behaviorParameterizationExample.filterApples(inventory, apple -> Color.RED.equals(apple.color())); assertIterableEquals(expected, result); }
코드가 훨씬 더 간결해졌다.
일곱 번째 시도 : 리스트 형식으로 추상화
지금까지 사과를 사용한 예제를 만들었다.
이제는 바나나, 오렌지, 정수, 문자열 등의 리스트에 필터 메서드를 사용하려고 한다.
public <T> List<T> filter(List<T> list, Predicate<T> p) { final var result = new ArrayList<T>(); for (T e : list) { if (p.test(e)) { result.add(e); } } return result; } @Test @DisplayName("리스트 요소를 추상화한 후 필터링") void filterApplesWithListElementsAbstractionTest() { final var expected = List.of("brown"); final var names = List.of("hong", "brown", "mike"); final var result = behaviorParameterizationExample.filter(names, name -> name.length() > 4); assertIterableEquals(expected, result); }
위 코드에서 리스트의 요소들을 추상화(제네릭을 사용)했다.
- 문자열 형태(이름)의 리스트를 받아서 길이가 4보다 큰 요소로 필터링했다.
실전 예제
Comparator로 정렬하기
- Java 8의
List
에는sort
메서드가 포함되어 있다. - 다음과 같은 인터페이스를 갖는
java.util.Comparator
객체를 이용해서sort
의 동작을 파라미터화할 수 있다.
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 사과를 무거운 순서대로 정렬해보자.
public class ComparatorTest {
private final List<Apple> inventory = List.of(
new Apple(Color.GREEN, 200),
new Apple(Color.GREEN, 220),
new Apple(Color.RED, 180),
new Apple(Color.GREEN, 200),
new Apple(Color.RED, 190));
@Test
@DisplayName("익명 클래스를 이용한 정렬")
void comparatorUsingAnonymousClassTest() {
final var inventory = new ArrayList<>(this.inventory);
final var expected = List.of(
new Apple(Color.GREEN, 220),
new Apple(Color.GREEN, 200),
new Apple(Color.GREEN, 200),
new Apple(Color.RED, 190),
new Apple(Color.RED, 180));
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return Integer.compare(a2.weight(), a1.weight());
}
});
assertIterableEquals(expected, inventory);
}
@Test
@DisplayName("람다를 이용한 정렬")
void comparatorUsingLambdaTest() {
final var inventory = new ArrayList<>(this.inventory);
final var expected = List.of(
new Apple(Color.GREEN, 220),
new Apple(Color.GREEN, 200),
new Apple(Color.GREEN, 200),
new Apple(Color.RED, 190),
new Apple(Color.RED, 180));
inventory.sort((a1, a2) -> Integer.compare(a2.weight(), a1.weight()));
assertIterableEquals(expected, inventory);
}
}
- 이렇게 익명 클래스나 람다를 이용해서
Comparator
를 구현한 후 요구 사항에 맞게 정렬을 할 수 있다.
Runnable로 코드 블럭 실행하기
- 자바 스레드를 이용하면 병렬로 코드 블럭을 실행할 수 있다.
- Java 8까지는
Thread
생성자에 객체만을 전달할 수 있었으므로 보통 결과를 반환하지 않는void run
메서드를 포함하는 익명 클래스가Runnable
인터페이스를 구현하도록 하는 것이 일반적인 방법이었다.
// java.lang.Runnable
public interface Runnable {
void run();
}
Runnable
을 이용해서 다양한 동작을 스레드로 실행할 수 있다.
public class RunnableTest {
@Test
@DisplayName("익명 클래스를 이용한 실행")
void runnableUsingAnonymousClassTest() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
});
thread.start();
}
@Test
@DisplayName("람다를 이용한 실행")
void runnableUsingLambdaTest() {
Thread thread = new Thread(() -> System.out.println("Hello World"));
thread.start();
}
}
ExecutorService로 실행 결과 반환받기
- Java 5부터는
ExecutorService
추상화 개념을 지원한다. ExecutorService
인터페이스는 태스크를 제출과 실행 과정의 연관성을 끊어준다.ExecutorService
를 이용하면 태스크를 스레드 풀로 보내조 결과를Future
로 저장할 수 있다는 점이 스레드와Runnable
을 이용하는 방식과는 다르다.- 여기서는
ExecutorService
의 개념에 대해서는 잘 몰라도 되고 여기서는Callable
인터페이스를 이용해 결과를 반환하는 태스크를 만들수 있다는 사실만 알면된다.
// java.util.concurrent.Callable
public interface Callable<V> {
V call();
}
- 아래 코드에서 볼 수 있듯이
ExecutorService
에 태스크를 제출해서 위 코드를 활용할 수 있다.
public class ExecutorServiceTest {
@Test
@DisplayName("익명 클래스를 이용한 서비스 실행")
void executorServiceUsingAnonymousClassTest() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
System.out.println(threadName.get());
}
@Test
@DisplayName("람다를 이용한 서비스 실행")
void executorServiceUsingLambdaTest() throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName());
System.out.println(threadName.get());
}
}
GUI 이벤트 처리하기
- 일반적으로 GUI 프로그래밍은 마우스 클릭이나 문자열 위로 이동하는 등의 이벤트에 대응하는 동작을 수행하는 식으로 동작한다.
- 자바FX(JavaFX) 에서는
setOnAction
메서드에EventHandler
를 전달함으로써 이벤트에 어떻게 반응할지 설정할 수 있다.
Button button = new Button("Send");
// 익명 클래스 이용
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
// Lambda 이용
button.setOnAction(event -> label.setText("Sent!!"));
메서드로 인자를 전달할 때 값을 파라미터화 하는 것보다 실제 동작을 파라미터화해서 전달하면 좀 더 유연한 코드를 작성할 수 있다.
동작을 파라미터화해서 전달할 때 Java 8의 람다를 사용하면 좀 더 간결한 코드를 작성할 수 있다.
'프로그래밍 > Java' 카테고리의 다른 글
[Modern Java] 람다 표현식(Lambda Expression) (3) | 2021.02.16 |
---|---|
[Stream API] 중간 연산 - sorted 메서드 (0) | 2021.01.07 |
[Stream API] 중간 연산 - peek 메서드 (0) | 2021.01.07 |
[Stream API] 중간 연산 - flatMap 메서드 (0) | 2021.01.05 |
[Stream API] 중간 연산 - map 메서드 (2) | 2021.01.04 |
- Total
- Today
- Yesterday
- import문
- 람다
- 계획
- 충북 콕! 콕!
- 스트림
- flatMapToDouble
- 다짐
- 목표
- flatMapToLong
- 변경사항
- Java8
- mapToObj
- 익명 클래스
- 자바
- flatMapToInt
- #배열 #array #map 함수
- flaMap
- java
- 개발자
- lambda
- jdk14
- #React #ReactJS #리액트
- IntelliJ
- modern java
- Stream API
- 토이 프로젝트
- java14
- 회고
- #예제 #example #가계부 #Account Book
- 중간 연산
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |