Java

Chaper 2 : 동작 파라미터화 코드 전달하기

내용

  • 변화하는 요구사항에 대응
  • 동작 파라미터화
  • 익명 클래스
  • 람다 표현식 미리보기
  • 실전 예제: Comparator, Runnable, GUI

소비자 요구사항은 항상 바뀐다.

💡
시시각각 변하는 사용자 요구 사항에 어떻게 대응해야 할까? 특히 엔지니어링 비용이 가장 최소화될 수 있으면 좋을 것이다.
💡
그 뿐 아니라 새로 추가한 기능은 쉽게 구현할 수 있어야하며, 장기적인 관점에서 유지보수가 쉬어야 한다.

동작 파라미터화

아직 어떻게 실행할 것인지 결정하지 않은 코드 블록
  • 이 코드 블록의 실행은 나중으로 미뤄진다.
예로, 나중에 실행될 메서드의 인수로 코드 블록을 전달할 수 있다.
→ 코드 블록에 따라 메서드의 동작이 파라미터화 된다.

Ex

💡
컬렉션을 처리할 때, 다음과 같은 메서드를 구현한다고 가정하자.
  • 리스트의 모든 요소에 대해서 ‘어떤 동작’을 수행할 수 있음
  • 리스트 관련 작업을 끝낸 다음에 ‘어떤 다른 동작’을 수행할 수 있음
  • 에러가 발생하면 ‘정해진 어떤 다른 동작’을 수행할 수 있음

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

변화에 대응하는 코드를 구현하는 것은 어려운 일이다. 하나의 예제를 선정하고, 코드를 점차 개선하면서 유연한 코드를 만드는 사례를 보자.
💡
기존의 농장 재고 목록 애플리케이션에 리스트에서 초록색 사과만 필터링하는 기능을 추가한다 가정하자

첫번째 시도: 초록색 사과 필터링

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

농부의 변심

하지만 갑자기 농부가 변심하여 빨간색 사과도 필터링하고 싶어졌다면?
새로운 메서드를 만들고 if 문의 조건을 빨간색 사과로 바꿀 수도 있지만, 색 선택이 더 늘어난다면 변화에 적절하게 대응할 수 없다.
이런 상황에서는 다음과 같은 좋은 규칙이 있다.
💡
거의 비슷한 코드가 반복하여 존재한다면, 그 코드를 추상화한다.

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

색을 파라미터화 할 수 있도록 메서드에 파라미터를 추가하면 변화하는 요구사항에 좀 더 유연하게 대응하는 코드를 만들 수 있다.
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; }
이제 농부도 만족할 것이다. 이제 다음처럼 구현한 메서드를 호출할 수 있다.
List<Apple> greenApples = filterApplesByColor(inventory, Color.GREEN); List<Apple> redApples = filterApplesByColor(inventory, Color.RED);

농부의 변심

그런데 갑자기 농부가 다시 나타나서 요구한다.
색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있다면 정말 좋겠네요. 보통 무게가 150그램 이상인 사과가 무거운 사과입니다.
그래서 앞으로 바뀔 수 있는 다양한 무게에 대응할 수 있도록 무게 정보 파라미터도 추가했다.
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) 원칙을 어기는 것이다.
이 때 탐색 성능을 개선하려고 한다면 한 줄이 아니라 모든 메서드의 구현을 고쳐야 한다.
엔지니어링적으로 비싼 대가를 치러야 한다.

동작 파라미터화 사용

전략 디자인 패턴(Strategy Design Pattern)

참 또는 거짓을 반환하는 함수 Predicate와 선택 조건을 결정하는 Interface를 사용해보자.
public interface ApplePredicate { boolean test(Apple a); }
다양한 선택 조건을 대표하는 여러 버전의 ApplePredicate를 정의하자.
public class AppleWeightPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.getWeight() > 150; } } public class AppleColorPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.getColor().equals(Color.GREEN); } }
notion image
위 조건에 따라 test 메서드가 다르게 동작할 것이라 예상할 수 있다.
이를 전략 디자인 패턴(Strategy Design Pattern)이라고 부른다.
전략이라 불리는 각 알고리즘을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
 
그런데 ApplePredicate는 어떻게 다양한 동작을 수행할 수 있을까?
filterApples 메서드가 ApplePredicate 객체를 받아 사과의 조건을 검사하도록 메서드를 고쳐야 한다.
이렇게 동작 파라미터화, 즉 메서드가 다양한 동작(전략)을 받아서 내부적으로 다양한 동작을 수행할 수 있다.
 
이제 filterApples 메서드가 ApplePredicate 객체를 인수로 받도록 고치자.
이렇게 하면 filterApples 메서드 내부에서 컬렉션을 반복하는 로직컬렉션의 각 요소에 적용할 동작분리할 수 있다는 점에서 소프트웨어 엔지니어링적으로 큰 이득을 얻는다.

세번째 시도: 추상적 조건으로 필터링

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; }
Predicate 객체로 사과 검사 조건을 캡슐화 했다.

코드/동작 전달하기

public class AppleRedAndHeavyPredicate implements ApplePredicate { @Override public boolean test(Apple apple) { return apple.getColor().equals(Color.RED) && apple.getWeight() > 150; } }
우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다.
즉, filterApples 메서드의 동작을 파라미터화 한 것이다.

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

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

복잡한 과정 간소화

현재 filterApples 메서드로 새로운 동작을 전달하려면 ApplePredicate 인터페이스를 구현하는 여러 클래스들을 정의한 다음에 인스턴스화 해야한다.
이는 상당히 번거로운 작업이며, 시간 낭비다.

익명 클래스(Anonymous Class)

Java의 지역 클래스(Local Class: 블록 내부에 선언된 클래스)와 비슷한 개념
  • 이름이 없다.
  • 선언과 인스턴스화를 동시에
  • 즉석에서 필요한 구현을 만들어서 사용
Java는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스(Anonymous Class)라는 기법을 제공한다.

네번째 시도: 익명 클래스 사용

List<Apple> redApples2 = filter(inventory, new ApplePredicate() { @Override public boolean test(Apple a) { return a.getColor().equals(Color.RED); } });
filterApples 메서드의 동작을 직접 파라미터화 했다.
 
GUI 애플리케이션에서 이벤트 핸들러 객체를 구현할 때 익명 클래스를 종종 사용한다.

아쉬운 점

  • 여전히 많은 코드 공간 차지
  • 많은 프로그래머가 익명 클래스 사용에 익숙하지 않다.

다섯번째 시도: 람다 표현식 사용

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor());
코드의 장황함(Verbosity)는 나쁜 특성이다.
하지만 람다 표현식을 사용하면 간결해지면서 문제를 더 잘 설명하는 코드가 되었다.

여섯번째 시도: 리스트 형식으로 추상화

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);
이렇게 해서 유연성과 간결함이라는 두마리 토끼를 모두 잡을 수 있었다.

실전 예제

  • Comparator로 정렬하기
  • Runnable로 코드 블록 실행하기
  • Callable을 결과로 반환하기
  • GUI 이벤트 처리하기

Comparator로 정렬하기

다음과 같은 인터페이스를 갖는 java.util.Comparator 객체를 이용해서 sort의 동작을 파라미터화 할 수 있다.
public interface Comparator<T> { int compare(T o1, T o2); }
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()) );

Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 병렬로 코드 블록을 실행할 수 있다.
어떤 코드를 실행할 것인지를 스레드에게 알려줄 수 있을까?
public interface Runnable { void run(); }
Runnable을 이용해서 다양한 동작을 스레드로 실행할 수 있다.
Thread t = new Thread(new Runnable() { public void run() { System.out.println("Hello, World!"); } }
람다 표현식으로 구현하면
Thread t = new Thread(() -> System.out.println("Hello, World!"));

결론

  • 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달하는 것을 동작 파라미터화라고 한다.
  • 동작 파라미터화를 통해 변화하는 요구사항에 잘 대응할 수 있으며, 엔지니어링 비용을 줄일 수 있다.
  • 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달할 수 있다.
    • 익명 클래스
    • 람다
  • Java API의 많은 메서드는 정렬, 스레드, GUI 처리 등을 포함한 다양한 동작으로 파라미터화 할 수 있다.

출처