템플릿 메소드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있는 디자인 패턴
변하지 않는 부분을 'Context'에
변하는 부분을 'Strategy'라는 인터페이스에
Context는 변하지 않는 템플릿 역할
전략을 사용하는 클래스. 실제로 어떤 전략을 사용할지는 외부에서 주입받음
Strategy는 변하는 알고리즘 역할
알고리즘의 공통 인터페이스(혹은 추상 클래스)
여러 개의 유사한 알고리즘이 존재할 때
실행 중 전략(행위)을 바꿔야 할 때
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가 전략 객체로부터 행동을 위임받음 |
유연성 | 클래스 계층 안에서만 행동 확장 가능 | 런타임에 전략 변경 가능 (더 유연함) |
사용 방식 | 하위 클래스에서 구현 | 전략 객체 생성 후 주입 |
--- | --- | --- |
알고리즘 변경 방법 | 추상 클래스 상속 + 메소드 오버라이딩 | 전략 객체 새로 만들어서 주입 |
실행 중 알고리즘 변경 | ❌ 안 됨 (상속은 컴파일 타임 결정) | ✅ 가능 (전략 객체만 갈아끼우면 됨) |
클래스 수 증가 | 있음 | 있음 (근본적으로는 동일) |
변경 유연성 | 고정된 흐름 내 일부만 바꿀 수 있음 | 전체 알고리즘을 통째로 바꿀 수 있음 |
🔍 표현 차이 정리
표현 | 설명 | 더 정확한가? |
---|---|---|
전략 객체를 새로 만들어서 주입 | "사용자 입장에서" 실행 중 전략을 바꾸는 방식 강조 | ✅ 사용자 시점에선 자연스러움 |
인터페이스 구현체를 새로 만들어서 주입 | 정확한 기술적 설명 (전략은 인터페이스/추상 클래스 구현체) | ✅ 기술적으로 정확 |
핵심 차이:
코드가 변경되지 않아도, 프로그램이 실행 중일 때 (= 런타임에) 어떤 전략을 쓸지 선택하거나 교체할 수 있는가?
🔹 템플릿 메소드 패턴: 컴파일 타임 고정
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();
겉보기엔 비슷해 보여. 둘 다 실행 중에 분기해서 결정했잖아?
✅ 그럼 차이는 뭐냐면...
📌 예:
CsvRead + JsonProcess + DBWrite 같은 혼합 조합은 전략 패턴만 가능함
템플릿은 이런 조합을 위해 새로운 하위 클래스를 또 만들어야 함
// 전략 패턴 - 순서 유동적
public void process() {
read.read();
validate();
if (needsTransform) process.process();
write.write();
}
템플릿 메소드는 이런 흐름 분기가 어렵고, 거의 process()가 고정돼 있어야 함.
같은 런타임 분기처럼 보여도…
구분 | 템플릿 메소드 패턴 | 전략 패턴 |
---|---|---|
런타임 전략 선택 가능? | 가능하지만 하위 클래스 단위로만 | 가능하고 세부 전략까지 유연하게 |
전략 교체 단위 | 클래스 하나 | 전략 하나씩 (조합 가능) |
흐름 제어 수정 | 어렵다 (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 “사용 시점에서 유연하게 바꿀 수 있다”는 다름
🧱 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가 너무 많은 책임 가질 수 있음 |
런타임 오류 | 잘못된 전략 주입 시 컴파일러가 막아주지 못함 |