Computer Science/디자인 패턴

Strategy Pattern (전략 패턴)

조용우 2025. 4. 22. 20:45

전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴입니다.

한 코드에서 다르게 동작해야 하는 부분들이 존재할 때, 그 부분들을 따로 빼는 것 

 

Ex)

기존 Navigator 클래스는

public class Navigator {

	public Route roadRoute() {
    	// ... 도로 루트 로직
    	return route;
    }
}

와 같은 형태로 이루어져 있었는데, 어플리케이션이 확장되면서 대중교통, 걷기 루트가 추가됨.

public class Navigator {

    public Route roadRoute() {
    	// ... 도로 루트 로직
    	return route;
    }
    
    public Route walkingRoute() {
    	// ... 걷기 루트 로직
    	return route;
    }
    
    public Route publicTransportRoute() {
    	// ... 대중교통 루트 로직
    	return route;
    }
}

 

문제점:

  • 새 경로 구축 알고리즘을 추가할 때마다 내비게이터의 메인 클래스의 크기가 늘어남.
  • 알고리즘 중 하나를 변경하면 전체 클래스에 영향이 미쳐 이미 작동하는 코드에서 오류가 발생할 가능성이 높아짐.
  • 새로운 기능을 구현하려면 거대한 동일 클래스를 변경해야 했는데 거대한 동일 클래스를 변경해야 하기 때문에  병합 충돌이 생김.

해결책:

전략 패턴은 특정 작업을 다양한 방식으로 수행하는 클래스를 선택한 후 모든 알고리즘을 (strategies)​이라는 별도의 클래스들로 추출.

(context)​라는 원래 클래스에는 전략 중 하나에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 콘텍스트는 작업을 자체적으로 실행하는 대신 연결된 전략 객체에 위임

 

Strategy

public interface Strategy {
    void 변하는_부분();
}

 

public class Strategy1 implements Strategy {
	
    @Override
    public void 변하는_부분() {
    	System.out.println("변하는 부분 - Strategy 1");
    }
}


public class Strategy2 implements Strategy {
	
    @Override
    public void 변하는_부분() {
    	System.out.println("변하는 부분 - Strategy 2");
    }
}


Context

public class Context {

    private Strategy strategy;
    
    public StrategyPattern(Strategy strategy) {
    	this.strategy = strategy;
    }

    public void 변하지_않는_부분() {
        System.out.println("변하지 않는 부분 시작");

        strategy.변하는_부분();

        System.out.println("변하지 않는 부분 종료");
    }
}

 

Context는 변하지 않는 로직을 가지고 있는 템플릿 역할을 하는 코드. 이 코드 안에서 Strategy를 통해 일부 전략이 변경된다. Strategy 인터페이스에만 의존하기 때문에 구현체를 주입하기만 하면 된다.

새로운 전략이 필요한 경우, 새로운 Strategy 구현체를 구현하기만 하면 됨.

 

사용 예제

public class StrategyPattern {
	
    void test() {
    	Strategy strategy1 = new Strategy1();
        Strategy strategy2 = new Strategy2();
        
    	Context context1 = new Context(strategy1):
        context1.변하지_않는_부분();
        
        Context context2 = new Context(strategy2):
        context2.변하지_않는_부분();
    }
}

// 결과

// 변하지 않는 부분 시작
// 변하는 부분 - Strategy 1
// 변하지 않는 부분 종료

// 변하지 않는 부분 시작
// 변하는 부분 - Strategy 2
// 변하지 않는 부분 종료

 

public class TemplateMethodPattern {
	
    void test() {
    	Context context1 = new Context(() -> System.out.println("변하는 부분 - Strategy 1")):
        context1.변하지_않는_부분();
        
    	Context context1 = new Context(() -> System.out.println("변하는 부분 - Strategy 2")):
        context2.변하지_않는_부분();
    }
}

 

익명 클래스 및 람다를 이용해서 더 깔끔하게 사용할 수 있음. (인터페이스에 메소드가 1개만 있을 경우 람다 사용 가능)

 

 

 

스프링 의존관계 주입에서도 이 전략 패턴을 사용한다.

 

장점:

바뀌는 부분들이 인터페이스에만 의존하고 있기 때문에, Context가 바뀌더라도 전략들에는 영향이 가지 않음

 

단점:

Context와 Strategy를 한번 조립하고 나면 전략을 변경하기가 어렵다. 

-> Setter를 제공해서 strategy를 넘겨줄 수도 있지만, 싱글턴 인스턴스의 경우 동시성 이슈 등 고려할 점이 많다.

 

해당 단점을 보완하기 위해, 생성 시점이 아닌 메소드 실행 시점에 Strategy를 선택하도록 하는 방식을 사용할 수 있다.

public class Context {

    public void 변하지_않는_부분(Strategy strategy) {
        System.out.println("변하지 않는 부분 시작");

        strategy.변하는_부분();

        System.out.println("변하지 않는 부분 종료");
    }
}

 

public class StrategyPattern {
	
    void test() {
    	Context context = new Context():
        context1.변하지_않는_부분(new Strategy1());
        context1.변하지_않는_부분(new Strategy2());
    }
    
    // Lambda
    void test() {
    	Context context = new Context():
        context1.변하지_않는_부분(() -> System.out.println("바뀌는 부분 - Strategy 1"));
        context1.변하지_않는_부분(() -> System.out.println("바뀌는 부분 - Strategy 2"));
    }
}

 

단점으로는, 실행할 때마다 전략을 계속 지정해주어야 한다는 것이다.