본문 바로가기

카테고리 없음

[JAVA] 전략 패턴 - 동작 파라미터화

[출처 : 스프링 인 액션]

 

 

 

 

 

 우리가 어떤 상황에서 일을 하던 소비자 요구사항은 바뀐다.

 예를들어 농부가 재고 목록 조사를 쉽게 할 수 있도록 돕는 애플리케이션이 있다고 가정하자.

농부는 이렇게 말할 것이다. 

 

"녹색 사과를 모두 찾고 싶어요" 

 

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

enum Color { RED, GREEN}

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

 

그 다음날 농부는 이렇게 말할 수도 있다.

 

"150 그램 이상이면서 녹색인 사과를 모두 찾으면 좋겠네요" 

 

 

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

public static List<Apple> filterAppleByColor(List<Apple> inventory, int weight) {
	List<Apple> result = new ArrayListM<>(); 
    for(Apple apple: inventory) {
    	if( apple.getWeight() > weight) {
        	result.add(apple);
        }
    }
    return result; 
}

 

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

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

 

다음처럼 위 메서드를 사용할 수 있다.

List<Apple> greenApples = filterApples(inventory, GREEN, 0, true);
List<Apple> greenApples = filterApples(inventory, null, 0, false);

 

( 정말 마음에 들지 않는 코드다. ) 

대체 true 와 false 는 무엇을 의미 하는 것일까? 게다가 앞으로 요구사항이 바뀌었을때 유연하게 대응할 수도 없다. 예를 들어 사과의 크기 모양, 출하지 등으로 사과를 필터링 하고 싶다면? 

 

 

동작 파라미터화

public interface ApplePredicate {
	boolean test (Apple apple); 
}​
public class AppleHeavyWeightPredicate implements ApplePredicate { 
	pubic boolean test(Apple apple) {
		return apple.getWeight() > 150; 
    }
}
public class AppleHeavyWeightPredicate implements ApplePredicate { 
	pubic boolean test(Apple apple) {
		return GREEN.equals(apple.getColor()); 
    }
}
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 메서드의 동작이 결정된다. 

즉, 우리는 filterApples 메서드의 동작을 파라미터화 한것이다.

 

동작 파라미터를 그림으로 그리면 아래와 같다. 

 

 

 

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

또한 이 구현 방식은 전략패턴으로도 불린다. 

전략 패턴이란, 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 전략 패턴은

  • 특정한 계열의 알고리즘들을 정의하고
  • 각 알고리즘을 캡슐화하며
  • 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

https://ko.wikipedia.org/wiki/%EC%A0%84%EB%9E%B5_%ED%8C%A8%ED%84%B4

 

전략 패턴 - 위키백과, 우리 모두의 백과사전

전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고

ko.wikipedia.org

 

 

전략 패턴은 어떻게 사용될까? 자바에서 살펴보자. 아래처럼 함수형 인터페이스가 전략 패턴에 사용된다. 

그중 Arrays 객체에서 sort 메서드는 전략적으로 필요한 요소에 따른 Comparable 객체를 파라미터로 받아 배열을 정렬한다. 

 

Comparable , Comparator 인터페이스 

클래스가 자신이 갖고 있는 객체를 정렬할 수 있으려면 Comparable 인터페이스를 구현해야 한다. 기술적인 측면으로 Comparable 인터페이스는 문자열 대 문자열, 직원 대 직원 등으로 비교 해야한다. Comparable 인터페이스는 타입 파라미터를 받기 때문이다.

public interface Comparable<T> {
	int compareTo(T other);
}

x.compareTo(Y) 에서 값이 0 이면 x 가 y 다음에, 음수이면 y 가 x 다음, 같으면 반환값이 0 이다.

|참고| 정수가 음수일 때 두 정수의 차이를 반환하면 메서드가 제대로 동작하지 않는다. 부호가 반대인 피 연산자의 값이 크면 두 수의 차이가 오버플로우를 일으킬 수 있다. 이럴 때는 어떤 정수라도 올바르게 동작하는 Integer.compare 메서드를 사용한다. 

public class Employee implements Comparable<Employee> {
	... 
    public int compareTo(Employee other) {
    	return Double.compare(salary, other.salary);
    }
}

 

String 클래스는 자바 라이브러리에 들어있는 백개 이상의 다른 클래스와 마찬가지로 Comparable 인터페이스를 구현한다. Arrays.sort 메서드는 Comparable 객체의 배열을 정렬할때 사용한다. 

String[] friends = {"Peter" , "Paul" ,"Mary"}; 
Arrays.sort(firends); // 이제 friends 가 ["Mary", "Paul", "Peter"] 로 정렬된다.

 

 

 

 

Runnable 인터페이스 

모든 프로세서가 멀티 코어를 장착하게 되면서 모든 코더들이 바쁘게 작업하길 바라게 되었다. 아마 특정 태스크(작업)을 별도의 스레드에서 실행하거나 실행 스레드 풀에 넣으려고 할 것이다. 이렇게 태스크를 정의하려면 Runnable 인터페이스를 구현해야 한다. Runnable 인터페이스는 메서드를 한개만 갖는다. 

class HelloTask implements Runnable {
	pubilc void run() {
    	for(int i = 0; i< 1000; i++) {
        	System.out.println("Hello, World!");
        }
    }
}

이 태스크를 새로운 스레드에서 실행하려면 Runnable 로 부터 스레드를 생성하고 해당 스레드를 시작 해야한다. 

 

Runnable task = new HelloTask(); 
Thread thread = new Thread(task); 
thread.start();

 

아래와 타입으로 명목적 타입 지정으로 함수의 의도를 선언하고 사용 가능하다. 

함수형 인터페이스 파라미터 타입 반환 타입 추상 메서드 이름 설명  다른 메서드 
Runnable 없음  void  run 인자나 반환값 없이 액션을 수행한다  
Supplier<T>  없음 get  T 타입 값을 공급한다.   
Consumer<T>  T void accept  T 타입 값을 소비한다 andThen
BiConsumer<T,U> T,U void  accept T 와 U 타입 값을 소비한다  andthen 
Function<T,R>  T R apply T 타입 인자를 받는 함수다  compose, andthen, idntity
BiFunction<T,U,R> T, U R apply T와 U 타입 인자를 받는 함수다 andthen
UnaryOperator<T> T T apply  T타입에 작용하는 단항연산자다. compose, andthen, idntity
BinaryOperator<T> T,T apply  T타입에 작용하는 이항연산자다. andthen, maxBy, 
minBy
Predicate<T>  boolean test  불 값을 반환하는 함수다 and, or, negate, isEqual
Predicate<T, U> T,U boolean test 두가지 인자를 받고 불 값을 반환하는 함수다 and, or, negate

 

하나의 함수만을 가진 인터페이스를 함수형 인터페이스라고 한다. 함수형 인터페이스의 구조는 람다 형식으로 쉽게 변환 가능하다.

 

자신만의 함수형 인터페이스 구현하기

 

@FunctionalInterface 
public interface PixelFunction{ 
	Color apply(int x, int y);
}

 

@FunctionalInterface 를 붙이면 컴파일러가 추상 메서드 하나에만 있는 인터페이스 인지 검사한다. 

BufferedImage createImage(int width, int height, PixelFunction f){
	BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
    
    for (int x = 0; x < width; x++) 
    	for (int y = 0; i< height; y++) {
        	Color color = f.apply(x,y);
            image.setRGB(x,y,color.getRGB());
        }
     return image();
}

 

람다 표현식으로 함수를 호출 할 수도 있다.  

BufferedImage frenchFlag = createImage(150, 100, 
(x, y) -> x < 50 ? Color.BLUE : x < 100 ? Color.WHITE : Color.RED);

 

https://wwlee94.github.io/category/study/toby-spring/template-callback/  전략 패턴 

 

[토비의 스프링] 3. 전략 패턴과 템플릿/콜백에 대해서

Java, Spring 템플릿/콜백 패턴에 대해서

wwlee94.github.io

 

 

반응형