"알고리즘을 캡슐화하여 교체 가능하게 만들자"
// 문제 1: if-else 지옥
public class PaymentService {
public void processPayment(String method, double amount) {
if (method.equals("CREDIT_CARD")) {
// 신용카드 결제 로직 (50줄)
System.out.println("신용카드 결제...");
validateCard();
connectToCardGateway();
processCardPayment(amount);
} else if (method.equals("PAYPAL")) {
// PayPal 결제 로직 (50줄)
System.out.println("PayPal 결제...");
validatePayPalAccount();
connectToPayPal();
processPayPalPayment(amount);
} else if (method.equals("BANK_TRANSFER")) {
// 계좌이체 로직 (50줄)
System.out.println("계좌이체...");
validateBankAccount();
connectToBank();
processBankTransfer(amount);
}
// 새 결제 수단 추가 시 이 메서드 수정! (OCP 위반)
}
}
// 문제 2: 알고리즘 변경이 어려움
public class DataCompressor {
private String compressionType = "ZIP";
public byte[] compress(byte[] data) {
if (compressionType.equals("ZIP")) {
return zipCompress(data);
} else if (compressionType.equals("GZIP")) {
return gzipCompress(data);
} else if (compressionType.equals("RAR")) {
return rarCompress(data);
}
return data;
}
// 압축 알고리즘 변경하려면 클래스 수정!
}
// 문제 3: 테스트가 어려움
public class NavigationService {
public Route findRoute(Location from, Location to, String mode) {
if (mode.equals("CAR")) {
// 자동차 경로 계산 (복잡한 로직)
return calculateCarRoute(from, to);
} else if (mode.equals("WALK")) {
// 도보 경로 계산 (복잡한 로직)
return calculateWalkRoute(from, to);
} else if (mode.equals("BIKE")) {
// 자전거 경로 계산 (복잡한 로직)
return calculateBikeRoute(from, to);
}
return null;
}
// 각 알고리즘을 개별적으로 테스트하기 어려움!
}
// 문제 4: 중복 코드
public class SortingService {
public void sort(int[] arr, String algorithm) {
if (algorithm.equals("BUBBLE")) {
// 버블 정렬
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
} else if (algorithm.equals("QUICK")) {
// 퀵 정렬 (복잡한 로직)
quickSort(arr, 0, arr.length - 1);
}
// 알고리즘마다 조건문 안에 전체 구현!
}
}- OCP 위반: 새 알고리즘 추가 시 기존 코드 수정
- 낮은 응집도: 여러 알고리즘이 한 클래스에
- 테스트 어려움: 알고리즘을 독립적으로 테스트 불가
- 재사용 불가: 알고리즘을 다른 곳에서 사용 어려움
알고리즘군을 정의하고 각각을 캡슐화하여 교환 가능하게 만드는 패턴. 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
- 알고리즘 캡슐화: 각 알고리즘을 독립 클래스로
- 교체 가능: 런타임에 알고리즘 변경
- OCP 준수: 새 알고리즘 추가 시 기존 코드 불변
- 재사용성: 알고리즘을 여러 곳에서 사용
// Before: if-else로 알고리즘 선택
if (type.equals("A")) {
// 알고리즘 A
} else if (type.equals("B")) {
// 알고리즘 B
}
// After: Strategy로 알고리즘 교체
Strategy strategy = new StrategyA();
strategy.execute();
strategy = new StrategyB(); // 런타임 교체!
strategy.execute();┌─────────────────┐
│ Context │ ← 전략 사용자
├─────────────────┤
│ - strategy │───┐
│ + setStrategy() │ │ has-a
│ + execute() │ │
└─────────────────┘ │
│
▼
┌───────────────┐
│ Strategy │ ← 전략 인터페이스
├───────────────┤
│ + algorithm() │
└───────────────┘
△
│ implements
┌────────────┼────────────┐
│ │ │
┌────────────┐ ┌────────────┐ ┌────────────┐
│StrategyA │ │StrategyB │ │StrategyC │
├────────────┤ ├────────────┤ ├────────────┤
│algorithm() │ │algorithm() │ │algorithm() │
└────────────┘ └────────────┘ └────────────┘
| 요소 | 역할 | 예시 |
|---|---|---|
| Strategy | 알고리즘 인터페이스 | PaymentStrategy |
| ConcreteStrategy | 구체적 알고리즘 | CreditCardStrategy |
| Context | 전략 사용 | PaymentService |
/**
* Strategy: 결제 전략 인터페이스
*/
public interface PaymentStrategy {
void pay(double amount);
String getPaymentMethod();
}
/**
* ConcreteStrategy 1: 신용카드 결제
*/
public class CreditCardStrategy implements PaymentStrategy {
private String cardNumber;
private String cvv;
private String expiryDate;
public CreditCardStrategy(String cardNumber, String cvv, String expiryDate) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
@Override
public void pay(double amount) {
System.out.println("💳 신용카드 결제 처리 중...");
System.out.println(" 카드번호: " + maskCardNumber(cardNumber));
System.out.println(" 금액: $" + amount);
System.out.println(" ✅ 결제 완료!");
}
@Override
public String getPaymentMethod() {
return "Credit Card";
}
private String maskCardNumber(String cardNumber) {
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}
}
/**
* ConcreteStrategy 2: PayPal 결제
*/
public class PayPalStrategy implements PaymentStrategy {
private String email;
private String password;
public PayPalStrategy(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public void pay(double amount) {
System.out.println("💰 PayPal 결제 처리 중...");
System.out.println(" 계정: " + email);
System.out.println(" 금액: $" + amount);
System.out.println(" ✅ 결제 완료!");
}
@Override
public String getPaymentMethod() {
return "PayPal";
}
}
/**
* ConcreteStrategy 3: 계좌이체
*/
public class BankTransferStrategy implements PaymentStrategy {
private String bankName;
private String accountNumber;
public BankTransferStrategy(String bankName, String accountNumber) {
this.bankName = bankName;
this.accountNumber = accountNumber;
}
@Override
public void pay(double amount) {
System.out.println("🏦 계좌이체 처리 중...");
System.out.println(" 은행: " + bankName);
System.out.println(" 계좌: " + accountNumber);
System.out.println(" 금액: $" + amount);
System.out.println(" ✅ 이체 완료!");
}
@Override
public String getPaymentMethod() {
return "Bank Transfer";
}
}
/**
* Context: 쇼핑카트
*/
public class ShoppingCart {
private List<Item> items;
private PaymentStrategy paymentStrategy;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
items.add(item);
System.out.println("🛒 장바구니에 추가: " + item);
}
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
System.out.println("💡 결제 방법 선택: " + strategy.getPaymentMethod());
}
public void checkout() {
double total = calculateTotal();
System.out.println("\n=== 결제 진행 ===");
System.out.println("총 금액: $" + total);
if (paymentStrategy == null) {
System.out.println("❌ 결제 방법을 선택해주세요!");
return;
}
paymentStrategy.pay(total);
}
private double calculateTotal() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}
}
/**
* 상품 클래스
*/
class Item {
private String name;
private double price;
public Item(String name, double price) {
this.name = name;
this.price = price;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return name + " ($" + price + ")";
}
}
/**
* 사용 예제
*/
public class StrategyExample {
public static void main(String[] args) {
// 쇼핑카트 생성
ShoppingCart cart = new ShoppingCart();
// 상품 추가
System.out.println("=== 쇼핑 시작 ===");
cart.addItem(new Item("노트북", 1200.00));
cart.addItem(new Item("마우스", 25.00));
cart.addItem(new Item("키보드", 75.00));
// 신용카드로 결제
System.out.println("\n--- 신용카드 결제 ---");
cart.setPaymentStrategy(new CreditCardStrategy(
"1234567890123456", "123", "12/25"
));
cart.checkout();
// PayPal로 결제 (전략 교체!)
System.out.println("\n--- PayPal로 변경 ---");
cart.setPaymentStrategy(new PayPalStrategy(
"user@example.com", "password"
));
cart.checkout();
// 계좌이체로 결제
System.out.println("\n--- 계좌이체로 변경 ---");
cart.setPaymentStrategy(new BankTransferStrategy(
"KB국민은행", "123-456-789"
));
cart.checkout();
}
}실행 결과:
=== 쇼핑 시작 ===
🛒 장바구니에 추가: 노트북 ($1200.0)
🛒 장바구니에 추가: 마우스 ($25.0)
🛒 장바구니에 추가: 키보드 ($75.0)
--- 신용카드 결제 ---
💡 결제 방법 선택: Credit Card
=== 결제 진행 ===
총 금액: $1300.0
💳 신용카드 결제 처리 중...
카드번호: ****-****-****-3456
금액: $1300.0
✅ 결제 완료!
--- PayPal로 변경 ---
💡 결제 방법 선택: PayPal
=== 결제 진행 ===
총 금액: $1300.0
💰 PayPal 결제 처리 중...
계정: user@example.com
금액: $1300.0
✅ 결제 완료!
--- 계좌이체로 변경 ---
💡 결제 방법 선택: Bank Transfer
=== 결제 진행 ===
총 금액: $1300.0
🏦 계좌이체 처리 중...
은행: KB국민은행
계좌: 123-456-789
금액: $1300.0
✅ 이체 완료!
/**
* Strategy: 경로 탐색 전략
*/
public interface RouteStrategy {
void buildRoute(String from, String to);
int estimateTime(String from, String to);
}
/**
* ConcreteStrategy: 자동차 경로
*/
public class CarRouteStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("🚗 자동차 경로 계산 중...");
System.out.println(" 출발: " + from);
System.out.println(" 도착: " + to);
System.out.println(" 경로: 고속도로 이용");
System.out.println(" 거리: 45 km");
}
@Override
public int estimateTime(String from, String to) {
return 30; // 30분
}
}
/**
* ConcreteStrategy: 도보 경로
*/
public class WalkRouteStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("🚶 도보 경로 계산 중...");
System.out.println(" 출발: " + from);
System.out.println(" 도착: " + to);
System.out.println(" 경로: 인도 및 횡단보도 이용");
System.out.println(" 거리: 3 km");
}
@Override
public int estimateTime(String from, String to) {
return 40; // 40분
}
}
/**
* ConcreteStrategy: 대중교통 경로
*/
public class PublicTransportStrategy implements RouteStrategy {
@Override
public void buildRoute(String from, String to) {
System.out.println("🚇 대중교통 경로 계산 중...");
System.out.println(" 출발: " + from);
System.out.println(" 도착: " + to);
System.out.println(" 경로: 지하철 2호선 → 버스 720번");
System.out.println(" 정류장: 5개");
}
@Override
public int estimateTime(String from, String to) {
return 50; // 50분
}
}
/**
* Context: 내비게이터
*/
public class Navigator {
private RouteStrategy strategy;
public void setStrategy(RouteStrategy strategy) {
this.strategy = strategy;
}
public void navigate(String from, String to) {
if (strategy == null) {
System.out.println("❌ 이동 수단을 선택해주세요!");
return;
}
System.out.println("\n=== 경로 안내 ===");
strategy.buildRoute(from, to);
int time = strategy.estimateTime(from, to);
System.out.println(" 예상 시간: " + time + "분");
}
}
/**
* 사용 예제
*/
public class NavigationExample {
public static void main(String[] args) {
Navigator navigator = new Navigator();
String from = "강남역";
String to = "홍대입구역";
// 자동차
System.out.println("### 자동차 경로 ###");
navigator.setStrategy(new CarRouteStrategy());
navigator.navigate(from, to);
// 도보
System.out.println("\n### 도보 경로 ###");
navigator.setStrategy(new WalkRouteStrategy());
navigator.navigate(from, to);
// 대중교통
System.out.println("\n### 대중교통 경로 ###");
navigator.setStrategy(new PublicTransportStrategy());
navigator.navigate(from, to);
}
}/**
* Strategy: 압축 전략
*/
public interface CompressionStrategy {
byte[] compress(byte[] data);
byte[] decompress(byte[] data);
String getAlgorithmName();
}
/**
* ConcreteStrategy: ZIP 압축
*/
public class ZipCompressionStrategy implements CompressionStrategy {
@Override
public byte[] compress(byte[] data) {
System.out.println("🗜️ ZIP 압축 중...");
System.out.println(" 원본 크기: " + data.length + " bytes");
// 실제로는 ZIP 압축 수행
byte[] compressed = new byte[data.length / 2];
System.out.println(" 압축 후: " + compressed.length + " bytes");
System.out.println(" 압축률: 50%");
return compressed;
}
@Override
public byte[] decompress(byte[] data) {
System.out.println("📦 ZIP 압축 해제 중...");
return new byte[data.length * 2];
}
@Override
public String getAlgorithmName() {
return "ZIP";
}
}
/**
* ConcreteStrategy: GZIP 압축
*/
public class GzipCompressionStrategy implements CompressionStrategy {
@Override
public byte[] compress(byte[] data) {
System.out.println("🗜️ GZIP 압축 중...");
System.out.println(" 원본 크기: " + data.length + " bytes");
// GZIP은 더 좋은 압축률
byte[] compressed = new byte[data.length / 3];
System.out.println(" 압축 후: " + compressed.length + " bytes");
System.out.println(" 압축률: 67%");
return compressed;
}
@Override
public byte[] decompress(byte[] data) {
System.out.println("📦 GZIP 압축 해제 중...");
return new byte[data.length * 3];
}
@Override
public String getAlgorithmName() {
return "GZIP";
}
}
/**
* Context: 파일 압축기
*/
public class FileCompressor {
private CompressionStrategy strategy;
public FileCompressor(CompressionStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(CompressionStrategy strategy) {
this.strategy = strategy;
System.out.println("💡 압축 알고리즘 변경: " + strategy.getAlgorithmName());
}
public void compressFile(String fileName, byte[] data) {
System.out.println("\n=== 파일 압축: " + fileName + " ===");
byte[] compressed = strategy.compress(data);
System.out.println("✅ 압축 완료!");
}
public void decompressFile(String fileName, byte[] data) {
System.out.println("\n=== 파일 압축 해제: " + fileName + " ===");
byte[] decompressed = strategy.decompress(data);
System.out.println("✅ 압축 해제 완료!");
}
}
/**
* 사용 예제
*/
public class CompressionExample {
public static void main(String[] args) {
byte[] data = new byte[1000]; // 1000 bytes 데이터
// ZIP 압축
FileCompressor compressor = new FileCompressor(new ZipCompressionStrategy());
compressor.compressFile("document.txt", data);
// GZIP으로 변경
compressor.setStrategy(new GzipCompressionStrategy());
compressor.compressFile("document.txt", data);
}
}/**
* Strategy: 정렬 전략
*/
public interface SortStrategy {
void sort(int[] array);
String getAlgorithmName();
}
/**
* ConcreteStrategy: 버블 정렬
*/
public class BubbleSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("🫧 버블 정렬 실행 중...");
long start = System.nanoTime();
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
long end = System.nanoTime();
System.out.println(" 실행 시간: " + (end - start) / 1000 + "μs");
}
@Override
public String getAlgorithmName() {
return "Bubble Sort";
}
}
/**
* ConcreteStrategy: 퀵 정렬
*/
public class QuickSortStrategy implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("⚡ 퀵 정렬 실행 중...");
long start = System.nanoTime();
quickSort(array, 0, array.length - 1);
long end = System.nanoTime();
System.out.println(" 실행 시간: " + (end - start) / 1000 + "μs");
}
private void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
@Override
public String getAlgorithmName() {
return "Quick Sort";
}
}
/**
* Context: 정렬기
*/
public class Sorter {
private SortStrategy strategy;
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
System.out.println("\n=== 정렬 실행: " + strategy.getAlgorithmName() + " ===");
System.out.println("배열 크기: " + array.length);
strategy.sort(array);
}
}
/**
* 사용 예제
*/
public class SortExample {
public static void main(String[] args) {
Sorter sorter = new Sorter();
// 작은 배열: 버블 정렬
int[] smallArray = {5, 2, 8, 1, 9};
sorter.setStrategy(new BubbleSortStrategy());
sorter.sort(smallArray);
// 큰 배열: 퀵 정렬
int[] largeArray = new int[1000];
for (int i = 0; i < 1000; i++) {
largeArray[i] = (int) (Math.random() * 1000);
}
sorter.setStrategy(new QuickSortStrategy());
sorter.sort(largeArray);
}
}| 장점 | 설명 | 예시 |
|---|---|---|
| OCP 준수 | 새 전략 추가 시 기존 코드 불변 | 새 결제 수단 |
| 알고리즘 교체 | 런타임에 동적 변경 | 압축 알고리즘 |
| 테스트 용이 | 각 전략 독립 테스트 | 단위 테스트 |
| 재사용성 | 전략을 여러 곳에서 사용 | 정렬 알고리즘 |
| 단점 | 설명 | 해결책 |
|---|---|---|
| 클래스 증가 | 전략마다 클래스 생성 | 익명 클래스/람다 |
| 전략 선택 | 클라이언트가 전략 알아야 함 | Factory 패턴 |
// 잘못된 예: 전략이 상태를 가짐
public class BadStrategy implements Strategy {
private int count = 0; // 상태!
public void execute() {
count++; // 위험!
}
}해결:
// 전략은 무상태(Stateless)로
public class GoodStrategy implements Strategy {
public void execute(Context context) {
// context에서 필요한 데이터 받기
}
}✅ Strategy 인터페이스 정의
✅ ConcreteStrategy 구현
✅ Context 클래스 작성
✅ 전략 교체 메서드 제공
✅ 클라이언트는 전략 주입
| 상황 | 추천도 | 이유 |
|---|---|---|
| 알고리즘 교체 필요 | ⭐⭐⭐ | 런타임 변경 |
| if-else 많음 | ⭐⭐⭐ | 조건문 제거 |
| 알고리즘 독립 테스트 | ⭐⭐⭐ | 테스트 용이 |
| 여러 변형 알고리즘 | ⭐⭐⭐ | 캡슐화 |
- 알고리즘을 캡슐화
- 상속보다 조합
- 런타임에 교체 가능
- if-else 제거
// Java 8+에서는 람다로 간단히
Context context = new Context();
context.setStrategy(data -> {
// 전략 로직
});
// 또는 메서드 참조
context.setStrategy(this::processData);