← Posts

GoF · Behavioral · Strategy

Strategy

기준이 늘 때마다 if/switch를 고쳐야 할 때. 알고리즘을 인터페이스로 빼 런타임에 갈아끼우는 행위 패턴.

목차
  1. 기준이 늘 때마다 코드를 고친다
  2. 전략 = 알고리즘을 갈아끼운다
  3. 알고리즘을 인터페이스로 뺀다
  4. 방식마다 구현 하나
  5. 컨텍스트가 전략에 위임한다
  6. 런타임에 갈아끼운다
  7. 상속으로 안 하는 이유
  8. 실무: Comparator와 스프링 주입
  9. 정리

기준이 늘 때마다 코드를 고친다

상품 목록을 여러 기준으로 정렬한다고 하자. 이름순, 가격순, 최신순. 조건 분기로 짜면 이렇게 된다.

java
public List<Product> sort(List<Product> products, String by) {
    if (by.equals("name")) {
        products.sort((a, b) -> a.getName().compareTo(b.getName()));
    } else if (by.equals("price")) {
        products.sort((a, b) -> a.getPrice() - b.getPrice());
    } else if (by.equals("latest")) {
        products.sort((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()));
    }
    return products;
}

정렬 기준이 하나 늘 때마다 이 if/else찾아가 고쳐야 한다. “인기순”, “리뷰 많은 순”이 생기면 분기가 계속 붙는다. 한 메서드가 모든 기준을 떠안고, 기준이 늘수록 비대해진다. 새 기능을 더하려고 기존에 돌아가던 코드를 건드리는 것 - 이게 위험하다.

전략 = 알고리즘을 갈아끼운다

스트래티지 패턴이 이걸 푼다.

갈아끼우고 싶은 알고리즘(정책)을 인터페이스로 빼고, 각 방식을 그 인터페이스의 구현으로 만든다. 그러면 런타임에 원하는 걸 골라 끼울 수 있다.

정렬하는 방법이 여러 가지라면, 그 “방법” 자체를 객체로 만든다. 이름순도 하나의 전략, 가격순도 하나의 전략. 쓰는 쪽은 어떤 전략을 넣느냐로 동작을 정한다. 분기로 고르던 것을 객체를 바꿔 끼우는 것으로 바꾼다.

알고리즘을 인터페이스로 뺀다

먼저 “정렬한다”라는 행위를 인터페이스로 세운다.

java
public interface SortStrategy {
    List<Product> sort(List<Product> products);
}

이게 전략의 규격이다. “상품 목록을 받아 정렬해 돌려준다”는 계약만 있고, 어떻게 정렬하는지는 없다. 그 방법은 구현이 채운다.

방식마다 구현 하나

각 정렬 기준을 이 인터페이스의 구현으로 만든다. 하나가 한 가지 방법만 안다.

java
public class ByName implements SortStrategy {
    public List<Product> sort(List<Product> products) {
        products.sort(Comparator.comparing(Product::getName));
        return products;
    }
}

public class ByPrice implements SortStrategy {
    public List<Product> sort(List<Product> products) {
        products.sort(Comparator.comparingInt(Product::getPrice));
        return products;
    }
}

public class ByLatest implements SortStrategy {
    public List<Product> sort(List<Product> products) {
        products.sort(Comparator.comparing(Product::getCreatedAt).reversed());
        return products;
    }
}

아까 if/else 안에 뭉쳐 있던 조각들이 각자의 클래스로 흩어졌다. 하나를 고쳐도 나머지는 건드리지 않는다. 새 기준은 새 클래스 하나를 더할 뿐, 기존 코드는 그대로다.

컨텍스트가 전략에 위임한다

전략을 쓰는 쪽을 **컨텍스트(context)**라 부른다. 컨텍스트는 전략을 참조로 들고, 실제 일은 전략에 넘긴다.

java
public class ProductSorter {
    private SortStrategy strategy;

    public ProductSorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public List<Product> sort(List<Product> products) {
        return strategy.sort(products);   // 어떻게 정렬할지는 전략이 안다
    }
}

ProductSorter는 이름순인지 가격순인지 모른다. 그냥 들고 있는 전략에게 “정렬해줘”라고 위임할 뿐이다. 무엇으로 정렬할지는 전적으로 어떤 전략을 들고 있느냐가 정한다.

diagramdiagram

런타임에 갈아끼운다

전략은 객체이니, 실행 중에 얼마든지 바꿔 끼울 수 있다.

java
ProductSorter sorter = new ProductSorter(new ByName());
sorter.sort(products);              // 이름순

sorter.setStrategy(new ByPrice());  // 전략 교체
sorter.sort(products);              // 이제 가격순

사용자가 정렬 드롭다운을 바꾸면, 그에 맞는 전략을 끼워 넣으면 된다. 컴파일 시점이 아니라 실행 중에 동작이 정해진다. 분기문을 다시 타는 게 아니라, 참조 하나를 바꾸는 것이다.

상속으로 안 하는 이유

“정렬 방식마다 서브클래스를 만들면 되지 않나” 싶을 수 있다. NameSortedSorter extends Sorter 식으로. 하지만 상속은 컴파일 시점에 고정된다. 한번 NameSortedSorter로 만든 객체를 실행 중에 가격순으로 바꿀 수 없다. 새 객체를 만들어야 한다.

스트래티지는 **조합(composition)**이라 다르다. 컨텍스트가 전략을 “가지고” 있으니, 그 전략을 바꿔 끼우면 그만이다. 여기에 이득이 하나 더 붙는다. 전략이 독립된 객체라 다른 컨텍스트에서도 재사용할 수 있고, 따로 테스트하기도 쉽다. “is-a(상속)“가 아니라 “has-a(조합)“로 가는 것 - 이 유연함이 스트래티지가 상속을 마다하는 이유다.

실무: Comparator와 스프링 주입

스트래티지는 자바를 쓰면 매일 만난다. Comparator가 곧 정렬 전략이다.

java
products.sort(Comparator.comparing(Product::getPrice));   // 전략을 넘긴다
products.sort(Comparator.comparing(Product::getName));    // 다른 전략

sort는 어떻게 비교할지 모른다. 넘겨받은 Comparator(전략)에 위임할 뿐이다. 람다로 전략을 그 자리에서 만들어 넘기는 것도 흔하다.

Spring에서는 더 자연스럽다. 같은 인터페이스의 여러 구현을 빈으로 등록해 두고, 필요한 걸 주입받는다. Map<String, SortStrategy>로 전부 주입받아 키로 골라 쓰는 패턴이 대표적이다 - 새 전략을 빈으로 추가만 하면 기존 코드를 안 고치고 자동으로 끼워진다. 스트래티지가 의존성 주입과 만나면 이렇게 확장에 열린 구조가 된다.

정리

스트래티지란알고리즘을 인터페이스로 빼 런타임에 갈아끼운다
문제기준이 늘 때마다 if/switch를 고쳐 기존 코드를 건드린다
고침각 방식을 전략 구현으로, 컨텍스트가 전략에 위임한다
효과실행 중에 전략을 바꿔 끼우고, 새 방식은 클래스만 추가한다
상속 대신조합이라 런타임 교체·재사용·독립 테스트가 된다
실무Comparator, 스프링의 같은 인터페이스 여러 구현 주입

갈아끼우고 싶은 알고리즘을 객체로 세우고, 쓰는 쪽은 그저 위임하는 것 - 그게 스트래티지다.

관련 글