Peony의 기록 창고 🌼
반응형

이 글은 모던 자바 인 액션 책을 읽고 정리한 내용입니다.

http://www.yes24.com/Product/Goods/77125987

 

모던 자바 인 액션 - YES24

자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능과 문법, 디자인 패턴

www.yes24.com

 

시시각각 변하는 사용자 요구사항에 어떻게 대응해야 할까?

특히 우리의 엔지니어링적인 비용이 가장 최소화될 수 있으면 좋을 것이고, 새로 추가한 기능은 쉽게 구현할 수 있어야하며 장기적인 관점에서 유지보수가 쉬워야 한다.

동작 파라미터화란 아직 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다. 이 코드 블록의 실행은 나중으로 미뤄진다.

 

1. 2.1 변화하는 요구사항에 대응하기

기존의 농장 재고목록 애플리케이션에 리스트에서 녹색 사과만 필터링하는 기능을 추가한다고 가정하자.

 

1.1. 1. 첫 번째 시도 : 녹색 사과 필터링

사과 색을 정의하는 다음과 같은 Color num이 존재한다고 가정하자.

<java />
enum Color {RED, GREEN}
<java />
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; }

GREEN.equals(apple.getColor()) : 녹색 사과를 선택하는데 필요한 조건이다.

이때, 갑자기 농부가 변심하여 빨간 사과도 필터링하고 싶어졌다.

그럼 어떻게 고쳐야 할까?

 

메서드를 복사해서 filterRedApples라는 새로운 메서드를 만들고, if문의 조건을 빨간 사과로 바꾸는 방법은 나중에 농부가 좀 더 다양한 색으로 필터링하는 등의 변화에는 적절하게 대응할 수 없다. 이런 상황에서는 다음과 같은 좋은 규칙이 있다.

 

거의 비슷한 코드가 반복 존재한다면 그 코드를 추상화 한다.

 

1.2. 2. 두 번째 시도 : 색을 파라미터화

<java />
public static List<Apple> filterGreenApples(List<Apple> inventory, Color color) { List<Apple> result = new ArrayList<>(); for(Apple apple : inventory) { if(apple.getColor().equals(color)) { result.add(apple); } } return result; }

 

이렇게 하면 다음처럼 구현한 메서드를 호출할 수 있다.

<java />
List<Apple> greenApples = filterApplesByColor(inventory, GREEN); List<Apple> redApples = filterApplesByColor(inventory, RED);

 

그런데 갑자기 농부가 다시 나타나서는 "색 이외에 가벼운 사과와 무거운 사과로 구분할 수 있으면 좋겠네요. 무게가 150 이상인 사과가 무거운 사과입니다"라고 요구한다.

그래서 우리는 다양한 무게에 대응할 수 있도록 무게 정보 파라미터도 추가해보자.

<java />
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; }

 

위 코드도 좋은 해결책이라할 수 있다. 하지만, 목록을 검색하고, 각 사과에 필터링 조건을 적용하는 부분의 코드가 색 필터링 코드와 대부분 중복된다. 탐색 과정을 고쳐서 성능을 개선하려면 무슨일이 일어날까? 한 줄이 아니라 메서드 전체 구현을 고쳐야 한다.

 

1.3. 3. 세 번째 시도 : 가능한 모든 속성으로 필터링

색이나 무게 중 어떤 것을 기준으로 필터링할지 가리키는 플래그를 추가할 수 있다. (실전에서는 사용 ❌)

<java />
public static List<Apple> filterApplesByWeight(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는 뭘 의미하는걸까? 앞으로 요구사항이 바뀌었을 때 유연하게 대응할 수도 없다.

filterApples에 어떤 기준으로 사과를 필터링할 것인지 효과적으로 전달할 수 있다면 더 좋을 것이다. 이 다음에는 동작 파라미터화를 이용해서 유연성을 얻는 방법을 설명한다.

 

2. 2.2 동작 파라미터화

변화하는 요구사항에 좀 더 유연하게 대응할 수 있게 해보자. 한 걸음 물러서서 전체를 봐보자.

참 또는 거짓을 반환하는 함수를 프레디케이트라고 한다. 선택 조건을 결정하는 인터페이스를 정의하자.

<java />
public class ApplePredicate { boolean test(Apple apple); }

그러면 다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의할 수 있다.

 

<java />
public class AppleHeavyWeightPredicate implements ApplePredicate { public boolean test(Apple apple) { return apple.getWeight() > 150; } } public class AppleGreenColorPredicate implements ApplePredicate { public boolean test(Apple apple) { return GREEN.equals(apple.getColor()); } }

위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다. 이를 전략 디자인 패턴이라고 부른다.

전략 디자인 패턴은 각 알고리즘(전략)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음 런타임에 선택하는 기법이다. 우리 예제에서는 ApplePredicate가 알고리즘 패밀리고, AppleHeavyWeightPredicate 와 AppleGreenColorPredicate가 전략이다.

이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고쳐보자.

 

2.1. 1. 네 번째 시도 : 추상적 조건으로 필터링

filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고치면,

메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있게된다.

<java />
public static List<Apple> filter(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 메서드이다. filterApples 메서드의 새로운 동작을 정의하는 것이 test메서드다. 하지만, 메서드는 객체만 인수로 받으므로 test 메서드를 ApplePredicate 객체로 감싸서 전달해야한다. 이는 코드를 전달할 수 있는것과 다름없다.

 

한 개의 파라미터, 다양한 동작

지금까지 살펴본 것처럼 컬렉션 탐색 로직과 각 항목에 적용할 동작을 분리할 수 있다는 것이 동작 파라미터화의 강점이다.

 

3. 2.3 복잡한 과정 간소화

3.1. 1. 익명 클래스

익명 클래스는 자바의 지역 클래스와 비슷한 개념이다. 익명 클래스는 말 그대로 이름이 없는 클래스이다. 익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다.

 

3.2. 2. 다섯번째 시도 : 익명 클래스 사용

<java />
List<Apple> redApples = filterApples(inventory, new ApplePredicate() { public boolean test(Apple a) { return RED.equals(apple.getColor()); } });

익명 클래스로도 부족한 점이 많다. 1. 익명 클래스는 여전히 많은 공간을 차지한다. 2. 많은 프로그래머가 익명 클래스의 사용에 익숙하지 않다. 코드의 장황함은 나쁜 특성이다. 한눈에 이해할 수 있어야 좋은 코드다. 람다 표현식을 이용해서 간결하게 정리해보자.

 

3.3. 3. 여섯 번째 시도 : 람다 표현식 사용

<java />
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));

 

3.4. 4. 일곱 번째 시도 : 리스트 형식으로 추상화

<java />
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<Apple> evenNumers = filter(numbers, (Integer i) -> i % 2 == 0);

 

4. 2.4 실전 예제

4.0.1. 1. Comparator로 정렬하기

자바 8에서는 java.util.Comparator 객체를 이용해 List에 있는 sort의 동작을 파라미터화 할 수 있다.

<java />
public interface Comparator<T> { int compare(T o1, T 02); }

 

comparator를 구현해 sort 메서드의 동작을 다양화할 수 있다.

<java />
inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); } });

 

그리고 람다 표현식을 이용해 다음과 같이 간단하게 코드를 구현할 수 있다.

<java />
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

 

4.1. 2. Runnable로 코드 블록 실행하기

<java />
public interface Runnable { void run(); } Thread t = new Thread(new Runnable() { public void run() { System.out.println("Hello world"); } });

 

람다 표현식을 이용하면 다음처럼 스레드 코드를 구현할 수 있다.

<java />
Thread t = new Thread(() -> System.out.println("Hello world"));

 

4.2. 3. Callable을 결과로 반환하기

ExecutorService 인터페이스를 이용하면 태스크를 스레드 풀로 보내고 결과를 Future로 저장할 수 있다. 이 방식은 Runnable의 업그레이드 버전이라고 생각할 수 있다.

<java />
public interface Callabble<V> { V call(); }

 

아래 코드와 같이 실행 서비스에 태스크를 제출해서 위 코드를 활용할 수 있다. 다음 예제는 태슼를 실행하는 스레드의 이름을 반환한다.

<java />
ExecutorService executorService = Executors.newCachedThreadPool(); Future<String> threadName = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName(); } });

 

람다를 이용하면 다음처럼 코드를 줄일 수 있다.

<java />
Future<String> threadName = executorService.submit(() -> Thread.currentThread().getName());

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju