전략 패턴

jhin·2025년 4월 18일
0

템플릿 메소드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴

변하지 않는 부분을 'Context'에
변하는 부분을 'Strategy'라는 인터페이스에

Context는 변하지 않는 템플릿 역할
전략을 사용하는 클래스. 실제로 어떤 전략을 사용할지는 외부에서 주입받음

Strategy는 변하는 알고리즘 역할
알고리즘의 공통 인터페이스(혹은 추상 클래스)

🤔왜 전략 패턴을 써야 할까?

  1. 조건문/분기문 제거
  2. OCP 원칙 준수 (Open/Closed Principle)
  3. 유지보수 용이
  4. 동적 변경 가능

🤔언제 쓰면 좋을까?

여러 개의 유사한 알고리즘이 존재할 때
실행 중 전략(행위)을 바꿔야 할 때
if-else 문이 너무 많아져서 코드가 복잡해질 때

💻실전 예제: 데이터 처리 공정

public interface DataProcessingStrategy {
    void readData();
    void processData();
}
public class CsvStrategy implements DataProcessingStrategy {
    @Override
    public void readData() {
        System.out.println("CSV 데이터를 읽습니다.");
    }

    @Override
    public void processData() {
        System.out.println("CSV 데이터를 처리합니다.");
    }
}
public class JsonStrategy implements DataProcessingStrategy {
    @Override
    public void readData() {
        System.out.println("JSON 데이터를 읽습니다.");
    }

    @Override
    public void processData() {
        System.out.println("JSON 데이터를 처리합니다.");
    }
}
public class DataProcessor {
    private DataProcessingStrategy strategy;

    public DataProcessor(DataProcessingStrategy strategy) {
        this.strategy = strategy;
    }

    public void process() {
        strategy.readData();
        strategy.processData();
        writeData();
    }

    private void writeData() {
        System.out.println("결과 데이터를 파일에 저장했습니다.");
    }
}
public class Main {
    public static void main(String[] args) {
        DataProcessor csvProcessor = new DataProcessor(new CsvStrategy());
        csvProcessor.process();

        DataProcessor jsonProcessor = new DataProcessor(new JsonStrategy());
        jsonProcessor.process();
    }
}
CSV 데이터를 읽습니다.
CSV 데이터를 처리합니다.
결과 데이터를 파일에 저장했습니다.

JSON 데이터를 읽습니다.
JSON 데이터를 처리합니다.
결과 데이터를 파일에 저장했습니다.

🤔템플릿 메소드 패턴과 뭐가 다르지?

항목템플릿 메소드전략 패턴
구조상속 기반 (추상 클래스)조합 기반 (인터페이스/객체 주입)
흐름 제어부모 클래스가 고정된 흐름 제공Context가 전략 객체로부터 행동을 위임받음
유연성클래스 계층 안에서만 행동 확장 가능런타임에 전략 변경 가능 (더 유연함)
사용 방식하위 클래스에서 구현전략 객체 생성 후 주입
---------
알고리즘 변경 방법추상 클래스 상속 + 메소드 오버라이딩전략 객체 새로 만들어서 주입
실행 중 알고리즘 변경❌ 안 됨 (상속은 컴파일 타임 결정)✅ 가능 (전략 객체만 갈아끼우면 됨)
클래스 수 증가있음있음 (근본적으로는 동일)
변경 유연성고정된 흐름 내 일부만 바꿀 수 있음전체 알고리즘을 통째로 바꿀 수 있음

🔍 표현 차이 정리

표현설명더 정확한가?
전략 객체를 새로 만들어서 주입"사용자 입장에서" 실행 중 전략을 바꾸는 방식 강조✅ 사용자 시점에선 자연스러움
인터페이스 구현체를 새로 만들어서 주입정확한 기술적 설명 (전략은 인터페이스/추상 클래스 구현체)✅ 기술적으로 정확

✅ 컴파일 타임 vs 런타임 결정의 진짜 의미

핵심 차이:
코드가 변경되지 않아도, 프로그램이 실행 중일 때 (= 런타임에) 어떤 전략을 쓸지 선택하거나 교체할 수 있는가?

🔹 템플릿 메소드 패턴: 컴파일 타임 고정

DataProcessor processor = new CsvProcessor();  // 이 줄이 고정
processor.process();

어떤 하위 클래스를 쓸지는 코드에 박혀 있음.
만약 JSON 처리를 하고 싶으면? 👉 CsvProcessor 대신 JsonProcessor로 바꿔서 다시 컴파일 해야 함.
즉, 전략을 바꾸려면 "코드 자체"를 바꾸고 다시 컴파일해야 함.
✅ 전략 결정 시점: 컴파일 타임

🔹 전략 패턴: 런타임 선택 가능

Scanner sc = new Scanner(System.in);
System.out.println("CSV(1) or JSON(2)?");
int choice = sc.nextInt();

ReadStrategy read = (choice == 1) ? new CsvReadStrategy() : new JsonReadStrategy();
ProcessStrategy process = (choice == 1) ? new CsvProcessStrategy() : new JsonProcessStrategy();
WriteStrategy write = new FileWriteStrategy();

DataProcessorContext ctx = new DataProcessorContext(read, process, write);
ctx.process();

실행 도중 사용자 입력에 따라 전략 객체를 다르게 주입할 수 있음.
즉, 코드 자체를 수정하지 않아도, 실행 중에 원하는 전략을 골라서 쓸 수 있음.
✅ 전략 결정 시점: 런타임

🔍 똑같이 런타임 분기를 주면 뭐가 다른 건데?

예를 들어 둘 다 이렇게 쓸 수 있음

🔸 템플릿 메소드에서 런타임 분기

DataProcessor processor;

if (userInput.equals("csv")) {
    processor = new CsvDataProcessor();
} else {
    processor = new JsonDataProcessor();
}
processor.process();

🔸 전략 패턴에서도 런타임 분기

ReadStrategy read;
ProcessStrategy process;

if (userInput.equals("csv")) {
    read = new CsvReadStrategy();
    process = new CsvProcessStrategy();
} else {
    read = new JsonReadStrategy();
    process = new JsonProcessStrategy();
}

DataProcessorContext ctx = new DataProcessorContext(read, process, new FileWriteStrategy());
ctx.process();

겉보기엔 비슷해 보여. 둘 다 실행 중에 분기해서 결정했잖아?

✅ 그럼 차이는 뭐냐면...

  1. 전략 교체의 “단위”와 “유연성”
    템플릿 메소드는 하위 클래스 전체를 교체함 → 모든 전략이 한 덩어리로 움직여야 함
    전략 패턴은 개별 전략 객체를 자유롭게 교체함 → read, process, write 각각 따로 바꿀 수 있음

📌 예:
CsvRead + JsonProcess + DBWrite 같은 혼합 조합은 전략 패턴만 가능함
템플릿은 이런 조합을 위해 새로운 하위 클래스를 또 만들어야 함

  1. 동작 흐름 자체 변경
    템플릿 메소드는 process() 안의 순서가 고정
    전략 패턴은 ctx.process() 내부 로직도 원하는 대로 바꿀 수 있음
// 전략 패턴 - 순서 유동적
public void process() {
    read.read();
    validate();
    if (needsTransform) process.process();
    write.write();
}

템플릿 메소드는 이런 흐름 분기가 어렵고, 거의 process()가 고정돼 있어야 함.

  1. "열린 구조"의 정도
    전략 패턴은 실행 중에도 새로운 전략 클래스를 동적으로 로딩하거나, Lambda로 대체하거나, Spring Bean으로 주입받을 수도 있음
    템플릿 메소드는 새로운 동작이 필요하면 결국 새 하위 클래스를 생성해야 함

같은 런타임 분기처럼 보여도…

구분템플릿 메소드 패턴전략 패턴
런타임 전략 선택 가능?가능하지만 하위 클래스 단위로만가능하고 세부 전략까지 유연하게
전략 교체 단위클래스 하나전략 하나씩 (조합 가능)
흐름 제어 수정어렵다 (process 고정)유연하게 제어 가능
조립 자유도낮음매우 높음
실행 중 동적 확장 (ex. 플러그인)어려움가능함
---------
교체 단위하나의 "전체 알고리즘 틀"을 가진 서브클래스"알고리즘 단계" 하나하나의 전략 객체
조립 방식상속으로 한 번에 구현합성(Composition)으로 부분 교체 가능
조합 가능성제한적 (하나의 클래스에 종속됨)유연 (부분만 갈아끼우기 가능)
클래스 수 증가단계별 커스터마이징은 새로운 전체 클래스 필요전략 단위로만 클래스 추가하면 됨

🎯 예로 비교해보자
예를 들어 CSV 읽기 + JSON 처리 + DB 저장 조합이 필요하다면…

🔸 템플릿 메소드 방식:

public class CsvJsonDbProcessor extends DataProcessor {
    protected void readData() { ... }   // CSV 읽기
    protected void processData() { ... } // JSON 처리
    protected void writeData() { ... }   // DB 저장
}

➡ 조합마다 새로운 하위 클래스를 하나씩 만들어야 해.
조합이 많아질수록 클래스도 기하급수적으로 증가함.

🔸 전략 패턴 방식:

DataProcessorContext ctx = new DataProcessorContext(
    new CsvReadStrategy(),
    new JsonProcessStrategy(),
    new DbWriteStrategy()
);

➡ 이미 있는 전략 객체들만 조합하면 돼.
새 클래스 만들 필요도 없고, 조합도 유연하게 가능함.

💡 그래서 정리하면
✅ 맞아, 둘 다 클래스는 필요해.
❗️ 하지만 템플릿 메소드는 하나의 전체 로직 클래스에 모든 전략을 담아야 하고,
✅ 전략 패턴은 전략을 분리해서 재사용/조합할 수 있어.

📌 비유로 풀어보면

  • 템플릿 메소드: 정해진 요리 레시피를 상속받아 약간만 수정할 수 있는 요리책.
    → 다른 조합은 새로운 요리책을 만들어야 함.
  • 전략 패턴: 재료와 도구(전략)를 하나씩 골라서 조합해서 새로운 요리를 만드는 느낌.
    → 기존 전략들을 조합만으로 다양한 레시피를 만들 수 있음.

결론적으로 말하면:
🔸 둘 다 런타임 분기 "가능"하지만,
🔹 전략 패턴은 "부분 교체"와 "조합", "동적 확장"이 가능한 구조이고,
🔸 템플릿 메소드는 흐름 고정 + 클래스 단위 교체라 덜 유연한 구조인 거야.

"클래스는 둘 다 필요하지만, 전략 패턴은 조립 가능한 작은 단위로 나뉘어 있어서 구조적으로 훨씬 유연하다"
→ 그게 구조적 차이, 설계 의도 차이야!

📌 정리

항목템플릿 메소드전략 패턴
전략 교체 방법하위 클래스를 새로 만들어서 상속전략 객체를 갈아끼움 (주입)
전략 바꾸는 시점소스코드 변경 + 컴파일 필요코드 변경 없이 실행 중 교체 가능
런타임 전략 선택❌ 불가 (미리 정해짐)✅ 가능 (입력, 설정, 상황에 따라 전략 선택)

💡 현실 비유
템플릿 메소드 = 🍱 도시락
메뉴 구성은 미리 정해져 있어.
고기 도시락, 생선 도시락처럼 만들기 전에 정해야 해.
도시락을 바꾸고 싶으면, 다시 만들어야 함. (= 컴파일)

전략 패턴 = 🍔 햄버거 세트 커스터마이즈
빵, 패티, 소스, 사이드 조합을 주문하면서 결정 가능.
손님이 고르면 매장에서 조립함. (= 런타임 결정)

✅ “수정 가능하다” vs “사용 시점에서 유연하게 바꿀 수 있다”는 다름

🎯 시나리오: 데이터를 읽고, 처리하고, 저장하는 DataProcessor

🧱 1. 템플릿 메소드 패턴 버전

// 공통 템플릿을 제공하는 추상 클래스
public abstract class DataProcessor {

    // 고정된 알고리즘 흐름
    public final void process() {
        readData();
        processData();
        writeData();
    }

    // 각 단계는 하위 클래스가 구현
    protected abstract void readData();
    protected abstract void processData();

    // 공통 메서드
    protected void writeData() {
        System.out.println("데이터를 저장합니다.");
    }
}

// CSV 처리기
public class CsvProcessor extends DataProcessor {
    protected void readData() {
        System.out.println("CSV 파일을 읽습니다.");
    }

    protected void processData() {
        System.out.println("CSV 데이터를 처리합니다.");
    }
}
DataProcessor processor = new CsvProcessor();
processor.process();  // 순서 고정: read → process → write

⚙️ 2. 전략 패턴 버전

// 전략 인터페이스 정의
public interface ReadStrategy {
    void read();
}

public interface ProcessStrategy {
    void process();
}

public interface WriteStrategy {
    void write();
}
// Context: 알고리즘의 실행 흐름을 직접 구성
public class DataProcessorContext {
    private ReadStrategy readStrategy;
    private ProcessStrategy processStrategy;
    private WriteStrategy writeStrategy;

    public DataProcessorContext(ReadStrategy read, ProcessStrategy process, WriteStrategy write) {
        this.readStrategy = read;
        this.processStrategy = process;
        this.writeStrategy = write;
    }

    public void process() {
        if (readStrategy != null) readStrategy.read();
        if (processStrategy != null) processStrategy.process();
        if (writeStrategy != null) writeStrategy.write();
    }
}
// 전략 구현
public class CsvReadStrategy implements ReadStrategy {
    public void read() {
        System.out.println("CSV 파일을 읽습니다.");
    }
}

public class CsvProcessStrategy implements ProcessStrategy {
    public void process() {
        System.out.println("CSV 데이터를 처리합니다.");
    }
}

public class FileWriteStrategy implements WriteStrategy {
    public void write() {
        System.out.println("파일에 저장합니다.");
    }
}
DataProcessorContext context = new DataProcessorContext(
    new CsvReadStrategy(),
    new CsvProcessStrategy(),
    new FileWriteStrategy()
);
context.process();  // 구성에 따라 자유롭게 실행 순서 제어 가능

💣단점도 있다?

단점 요약설명
클래스 수 증가전략마다 클래스가 하나씩 필요함
전략 선택 책임클라이언트 코드가 전략을 알아야 함
상태 공유 어려움전략 객체끼리 데이터 공유 어려움
역할 혼합 위험Context가 너무 많은 책임 가질 수 있음
런타임 오류잘못된 전략 주입 시 컴파일러가 막아주지 못함

0개의 댓글