티스토리 뷰

Chapter. 2 - 동작 파리미터화(behavior parameterization) 코드 전달

요구사항이 계속 변하는 상황

  • 녹색 사과를 모두 찾고싶어요.
  • 150그램 이상인 사과를 모두 찾고 싶어요.
  • 150그램 이상이면서 녹색인 사과를 모두 찾고 싶어요.
  • 100그램 이상이면서 빨간색인 사과를 모두 찾고 싶어요.

위와같이 변화하는 요구사항을 파라미터화하여 여러 다른 동작을 할 수 있도록 하는 것이 동작 파라미터화이다.

  • Behavior parameterization is the ability of a method to take multiple different behaviors as parameters and use them internally to accomplish different behaviors.

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

1. 녹색사과 필터링

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

fun filterGreenApples(inventory: List<Apple>): List<Apple> {
    val result = mutableListOf<Apple>()

    for (apple in inventory) {
        if (Color.GREEN == apple.color) {
            result.add(apple)
        }
    }

    return result
}

enum Color {
    RED, GREEN
}

class Apple {
    val color: Color = Color.GREEN
    val weight: Int = 100
}

위와 같이 녹색만 필터링하는 함수를 작성할 수 있을것이다. 만약 농부가 추가적으로 빨간색 사과만 원한다면 위 코드를 copy&paste하여 filterRedApples 함수를 생성하고 GREEN을 RED로 수정할 것인가?

만약 색이 연두색, 파란색, 검은색 등이 지속 추가된다면 어떻게 대응할 수 있을까? 이런 상황에서는 거의 비슷한 코드가 반복되므로 이를 추상화하여 해결해야한다.

2. 색을 파라미터화

어떻게 하면 filterGreenApples 코드를 반복하지 않고 filterRedApples를 구현할 수 있을까?

→ 색을 파라미터화할 수 있도록 메서드에 파라미터를 추가한다.

fun filterApplesByColor(inventory: List<Apple>, color: Color): List<Apple> {
    val result = mutableListOf<Apple>()

    for (apple in inventory) {
        if (color == apple.color) { // <---- 파라미터 color와 같다면 필터링
            result.add(apple)
        }
    }

    return result
}

자 이제 동적으로 요청한 색을 골라서 줄 수 있다, 다만 이제 추가적으로 무게 필터링을 원하면 어떻게 해야할까?

  • 무게 필터링을 원하는 경우, 색 필터링을 원하는 경우를 flag로 구분하고 파리미터를 받을것인가?
fun filterApplesByColorOrWeight(inventory: List<Apple>, color: Color, weight: Int, flag: Boolean): List<Apple> {
        /**
        플래그가 켜져있으면 색 비교
        플래그가 꺼져있으면 무게 비교
        */
}

정말 끔찍한 코드이다. 만약 색, 무게 이외에 모양, 출하지 등 다른 구분이 오면 또 어떻게 수정해야할것인가. 요구사항이 바뀔때마다 유연하게 대응할수가 없다.

지금까지는 문자열, 정수, 불리언 등의 으로 filterApples 메서드를 파라미터화 했다. 이를 동작 파라미터화를 통해 유연성을 얻도록 개선해보자.

3. 동작 파라미터화

변화하는 요구사항에 더 유연하게 대응할 수 있는 방법이 절실하다는걸 깨달았다.

  • 프레디케이트를 이용하여 선택 조건을 결정하는 인터페이스를 정의하자.
class AppleHeavyWeightPredicate() : ApplePredicate {
    override fun test(apple: Apple): Boolean {
        return apple.weight > 150
    }
}

class AppleGreenColorPredicate() : ApplePredicate {
    override fun test(apple: Apple): Boolean {
        return Color.GREEN == apple.color
    }
}

이제 어떤 요구사항이 원하는 선택조건에 따라 결정할 수 있게 됐는데 이러한 구조를 전략 디자인 패턴이라고 한다.

ApplePredicate는 사과 선택 전략을 동작에 따라 선택하여 적용하면 된다.

fun filterApples(inventory: List<Apple>, predicate: ApplePredicate): List<Apple> {
    val result = mutableListOf<Apple>()
    for (apple in inventory) {
        if (predicate.test(apple)) {
            result.add(apple)
        }
    }

    return result
}

이제 Apple 속성과 관련된 모든 변화에 대응할 수 있는 유연한 구조를 만들었다. 다른 추가 요구사항이 발생해도 ApplePredicate를 구현한 새로운 전략을 만들어서 predicate로 전달하면 된다.

복잡한 과정 간소화

현재는 filterApples 메서드로 새로운 동작을 전달하기 위해서는 ApplePredicate 인터페이스를 구현한 전략 생성이 필수적이다. 이는 상당히 번거로울 수 있다. 이를 람다(익명클래스)로 개선해보자.

1. 익명 클래스

익명 클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있다. 따라서 즉석에서 필요한 구현을 만들어서 사용이 가능하다.

2. 익명 클래스 사용

코틀린에서는 SAM (Single Abstract Method)라면, 즉 추상메소드를 하나만 갖는 인터페이스라면 함수형 인터페이스로 선언해야 람다를 사용할 수 있다.

fun interface ApplePredicate {
    fun test(apple: Apple): Boolean
}
  • fun interface로 함수형 인터페이스임을 명시. SAM 인 경우이다. (자바의 경우 @FunctionalInterface)
  • 이제 람다로 메서드의 구현과 인스턴스화를 동시에 할 수 있다.
fun main() {
    val inventory = listOf(Apple(), Apple(), Apple())
    val greenApples = filterApples(inventory, AppleColorPredicate(Color.GREEN))
    val heavyApples = filterApples(inventory, AppleHeavyWeightPredicate(150))
    val reds = filterApples(inventory) { apple -> Color.RED == apple.color }

    println(greenApples)
    println(heavyApples)
    println(redApples)
}

만약 인터페이스가 SAM이 아니라면, 즉 구현해야하는 메소드가 2개 이상이라면 위와 같은 형태로 작성은 불가하다.

interface ApplePredicate {
    fun test(apple: Apple): Boolean
    fun count(apple: Apple): Int
}

filterApples(inventory, object : ApplePredicate {
        override fun test(apple: Apple): Boolean {
            return Color.RED == apple.color
        }

        override fun count(apple: Apple): Int {
            return Color.RED == apple.color
        }
    }
)

// 아래처럼 람다로 구현체를 넘기는 방식으로 사용할 수 없음
filterApples(inventory) { apple -> Color.RED == apple.color }
  • 위 처럼 인터페이스의 fun 키워드를 제거해야하고,
    람다 표현식이 아닌 object : ApplePredicate를 사용하여 익명 객체를 생성하고 필요한 메서드를 오버라이딩 해야한다.

3. 리스트 형식으로 추상화

일단은 함수형 인터페이스임을 가정하고 더 진행해보자.

현재는 Apple 이라는 타입에 의존적이다. 하지만 Orange, Melon 등 다양한 과일이 추가되는 경우 각 구현체를 만들어야 할까? 이를 제네릭 타입을 통해 개선해볼 수 있다.

open class Fruit {
    open val color: Color = RED
    open val weight: Int = 100
}

class Apple(
    override val color: Color = RED,
    override val weight: Int = 100
) : Fruit()

class Orange(
    override val color: Color = GREEN,
    override val weight: Int = 150
) : Fruit()

fun interface FruitPredicate<T : Fruit> {
    fun test(t: T): Boolean
}

class FruitHeavyWeightPredicate<T : Fruit> : FruitPredicate<T> {
    override fun test(t: T): Boolean {
        return t.weight > 150
    }
}

class FruitGreenColorPredicate<T : Fruit> : FruitPredicate<T> {
    override fun test(t: T): Boolean {
        return GREEN == t.color
    }
}

fun <T : Fruit> filterFruit(inventory: List<T>, predicate: FruitPredicate<T>): List<T> {
    val result = mutableListOf<T>()

    for (apple in inventory) {
        if (predicate.test(apple)) {
            result.add(apple)
        }
    }

    return result
}
  • 과일이라는 Fruit 상위 클래스를 만들어주고, Apple, Orange가 부모 클래스를 상속받도록 한다.
  • FruitPredicate 는 여러 과일이 올 수 있도록 제네릭 타입으로 변경하고 Fruit을 상한 타입으로 제한한다.

최종적으로 아래와 같이 여러 과일 타입에 대해 유연한 처리가 가능하다.

fun main() {
    val inventory = listOf(Apple(RED, 100), Apple(GREEN, 90), Apple(RED, 150))
    val greenApples = filterFruit(inventory, FruitGreenColorPredicate())
    val heavyApples = filterFruit(inventory, FruitHeavyWeightPredicate())
    val redApples = filterFruit(inventory) { apple -> RED == apple.color }

    filterFruit(inventory) { apple -> RED == apple.color }

    println(greenApples)
    println(heavyApples)
    println(redApples)

    val orangeInventory = listOf(Orange(RED, 50), Orange(GREEN, 100), Orange(RED, 150))
    val redOranges = filterFruit(orangeInventory) { apple -> RED == apple.color }
    val heavyWeightOranges = filterFruit(orangeInventory) { apple -> apple.weight > 150 }

    println(redOranges)
    println(heavyWeightOranges)
}

실전 예제

1. Comparator로 정렬

  • List 내부에는 sort 메소드가 있고 Comparator 함수형 인터페이스를 받는다. 따라서 아래와 같이 오버라이딩하여 전달이 가능하다.
  • 함수형 인터페이스이므로 람다로도 전달이 가능하다.
inventory.sortedWith(object : Comparator<Fruit> {
        override fun compare(o1: Fruit, o2: Fruit): Int {
            return o1.weight - o2.weight
        }
    })

inventory.sortedWith { o1, o2 -> o1.weight - o2.weight }

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

@FunctionalInterface
public interface Runnable

자바 스레드를 이용하면 병렬로 코드 블록 실행이 가능하다. 이때 Runnable은 함수형 인터페이스 이므로 아래와같이 실행이 가능하며 람다로 실행이 가능하다.

val thread = Thread(object : Runnable {
    override fun run() {
        println("Hello World")
    }
})

val thread2 = Thread { println("Hello World") }

3. Callable을 결과로 반환하기

@FunctionalInterface
public interface Callable<V>
  • ExecutorService 인터페이스는 태스크 제출과 실행 과정의 연관성을 끊어준다. 즉, 태스크를 스레드 풀로 보내고 / 결과를 Future로 저장할 수 있다.
  • 이런점이 Runnable과 Callable의 이용하는 방식의 차이점이다.
  • Callable은 함수형 인터페이스이므로 아래와 같이 구현을 바로 하여 인스턴스로 넘길 수 있으며 람라로 넘길수 있다.
val executorService = Executors.newCachedThreadPool()
executorService.submit(
    object : Callable<String> {
        override fun call(): String {
            return "Hello World"
        }
    }
)

executorService.submit { Callable { "Hello World" } }

마치며

  • 동작 파라미터화를 통해 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드의 인수로 전달할 수 있다.
    • Predicate 인터페이스를 만들어서 전략에 따른 구현체를 만들어서 선택한다.
    • 함수형 인터페이스인 경우 구현과 함께 인스턴스화하여 람라식으로 전달이 가능하다.
  • 기존 자바 API의 많은 메서드는 (정렬, 스레드, Future, GUI 처리 등) 다양한 동작을 파라미터화할 수 있다.
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday