
알고리즘(전략)을 각각 클래스로 정의하고, 실행 중에 교체할 수 있도록 하는 디자인 패턴
동일한 문제를 해결하는 여러 방법(전략)이 있을 때, 그 방법들을 각각 전략 객체로 캡슐화하여 필요할 때마다 선택적으로 사용
if-else나 switch로 분기하는 구조를 피하고,
알고리즘의 변경을 객체 변경으로 처리하기 때문에 유지보수성과 확장성이 높음
결제 시 카카오페이 혹은 네이버페이로 결제하는 방식(전략)들이 존재
결제에 대한 인터페이스(Strategy Interface)를 설계하고, 해당 결제에 대하여 다양한 전략으로 구현하는 구현체 생성
public interface PaymentStrategy {
void pay(int amount);
}
public class KakaoPayPayment implements PaymentStrategy{
@Override
public void pay(int amount) {
System.out.println(amount + " With KakaoPay");
}
}
public class NaverPayPayment implements PaymentStrategy{
@Override
public void pay(int amount) {
System.out.println(amount + " With NaverPay");
}
}
public class PaymentContext {
private PaymentStrategy strategy;
public void setPayment(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void payNow(int amount){
if (strategy==null){
System.out.println("No PaymentStrategy");
return;
}
strategy.pay(amount);
}
}
public class PaymentMain {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
context.setPayment(new KakaoPayPayment());
context.payNow(10000);
context.setPayment(new NaverPayPayment());
context.payNow(5000);
}
}
/*
10000 With KakaoPay
5000 With NaverPay
*/
Template Method 패턴은 상속 기반에 상위 클래스가 전체 흐름을 제어하는 구조이기 때문에, 정적인 흐름을 가져갈 수 밖에 없음
CoffeeTemplate americano = new Americano();
americano.makeCoffee();
CoffeeTemplate latte = new Latte();
latte.makeCoffee();
기존에 정해진 방식으로만 정적 실행

Strategy 패턴은 객체 구성의 위임 기반이기 때문에, 제어된 흐름은 가져가되 상위-하위 클래스가 아닌 동적으로 선택된 Strategy 객체가 알고리즘을 실행함.
전략마다 클래스를 제작하여
BrewStrategy를 상속받은 AmericanoBrew, LatteBrew
CondimentStrategy를 상속받은 NoCondiment, MilkCondiment 구현
interface BrewStrategy {
void brew();
}
class AmericanoBrew implements BrewStrategy {
public void brew() {
System.out.println("아메리카노용 커피 원두를 우려냅니다.");
}
}
class LatteBrew implements BrewStrategy {
public void brew() {
System.out.println("라떼용 에스프레소를 추출합니다.");
}
}
interface CondimentStrategy {
void addCondiments();
}
class NoCondiment implements CondimentStrategy {
public void addCondiments() {
System.out.println("첨가물 없이 제공합니다.");
}
}
class MilkCondiment implements CondimentStrategy {
public void addCondiments() {
System.out.println("우유를 추가합니다.");
}
}
CoffeeMaker라는 객체(Context)를 생성하여 고정된 커피 제작 과정은 가져가되 Brew, Condiment Strategy 객체를 참조하여 실행
class CoffeeMaker {
private BrewStrategy brewStrategy;
private CondimentStrategy condimentStrategy;
public CoffeeMaker(BrewStrategy brewStrategy,
CondimentStrategy condimentStrategy) {
this.brewStrategy = brewStrategy;
this.condimentStrategy = condimentStrategy;
}
public void makeCoffee() {
boilWater();
brewStrategy.brew();
pourInCup();
condimentStrategy.addCondiments();
}
private void boilWater() {
System.out.println("Boil Water");
}
private void pourInCup() {
System.out.println("Pour In Cup");
}
}
BrewStrategy, CondimentStrategy를 동적으로 선택하여 커피 제작 가능
CoffeeMaker americano =
new CoffeeMaker(new AmericanoBrew(), new NoCondiment());
americano.makeCoffee();
CoffeeMaker latte = new CoffeeMaker(new LatteBrew(), new MilkCondiment());
latte.makeCoffee();
CoffeeMaker anyCoffee = new CoffeeMaker(new LatteBrew(), new NoCondiment());
anyCoffee.makeCoffee(); //우유가 들어가지 않은 라떼 제작 가능

Strategy 예제를 살펴보면서 객체를 생성하는 부분을 Factory Method 패턴을 사용해도 되겠다.. 는 판단
class PaymentStrategyFactory {
public static PaymentStrategy create(String type) {
switch (type) {
case "Kakao":
return new KakaoPayPayment();
case "Naver":
return new NaverPayPayment();
default:
throw new IllegalArgumentException("No Payment");
}
}
}
```java
public class PaymentMain {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
context.setPayment(PaymentStrategyFactory.create("Kakao"));
context.payNow(10000);
context.setPayment(PaymentStrategyFactory.create("Naver"));
context.payNow(5000);
}
}
Strategy 패턴은 “어떤 방식으로 동작할지”를 유연하게 변경,
Factory Method는 “무엇을 생성할지”를 유연하게 변경
이는 생성과 실행의 책임을 분리하면서도 더 유연한 구조로 설계 가능
여러 전략을 캡슐화하여 동적으로 교체 가능하게 만드는 패턴
변화가 잦은 알고리즘을 관리하고, 조건문 의존을 줄이며 유지보수를 용이하게 함
Template Method 패턴과 달리 상속이 아닌 "구성(composition)" 기반이라는 점에서 더욱 유연함
컬렉션(집합 객체)의 내부 구조를 노출하지 않고, 그 안의 요소들을 순차적으로 접근할 수 있게 해주는 디자인 패턴
ArrayList, Set, Map 같은 컬렉션의 내부 구현에 관계없이, 하나씩 꺼내면서 처리할 수 있도록 표준화된 인터페이스 제공
컬렉션의 다양한 자료구조 (Array, Tree, LinkedList 등)에 공통된 반복 방식을 적용
표준 Java의 Iterator 라이브러리 메서드와 부연 설명
public interface Iterator<E> {
/*
다음 요소가 존재하는지 확인
true -> next() 호출 가능
false -> next() 호출 시 NoSuchElementException 예외 발생
반복 종료 조건 판단용
반복자 내부 상태(커서 등)를 참조해서 동작
next() 호출 전에는 항상 hasNext()를 먼저 호출하는 것이 안전
*/
boolean hasNext();
/*
현재 커서 위치에서 다음 요소를 반환, 커서 한 칸 이동
*/
E next();
/*
마지막으로 next()로 반환된 요소를 컬렉션에서 제거
단, 지원하지 않는경우 UnsupportedOperationException 예외 발생
반드시 next() 이후에만 호출 가능
한 번의 next()에 대해 remove()는 한 번만 가능
remove()를 구현하지 않은 컬렉션에서는 예외 발생 (ArrayList는 가능, unmodifiableList는 불가)
Collection.remove() 대신 Iterator 기반 삭제를 통해 ConcurrentModificationException 방지
안전한 삭제 수단
*/
default void remove() {
throw new UnsupportedOperationException("remove");
}
/*
현재 위치부터 끝까지 모든 요소를 순회하며, 주어진 action을 적용
즉, 남은 요소에 대해 자동 반복 실행하는 고차 함수
Java8 이후 추가
함수형 스타일로 순회 가능(Consumer 활용)
*/
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
Iterator를 적용시킬 Book과 BookShelf
public class Book {
private String title;
public Book(String title) {this.title = title;}
public String getTitle() {return title;}
}
public class BookShelf implements Iterable<Book>{
private List<Book> books = new ArrayList<>();
public void addBook(Book book){
books.add(book);
}
//표준 Iterator 반환
@Override
public Iterator<Book> iterator() {
return books.iterator();
}
}
public class BookMain {
public static void main(String[] args) {
BookShelf shelf = new BookShelf();
shelf.addBook(new Book("Clean Code"));
shelf.addBook(new Book("Effective Java"));
for (Book book : shelf) {
System.out.println("Book name : " + book.getTitle());
}
}
}
/*
Book name : Clean Code
Book name : Effective Java
*/
Iterable<Book>을 상속하는 이유Iterable<T>는 자바에서 for-each 문으로 순회할 수 있는 객체임을 나타내는 인터페이스이며, 내부적으로는 Iterator<T>를 반환
따라서 아래의 구조를 사용할 수 있음
for (Book book : shelf)
만약 상속하지 않는다면 사용 불가❌, Iterator를 직접 꺼내서 순회해야 함
// Iterator 직접 꺼내서 순회 가능
Iterator<Book> it = books.iterator();
while (it.hasNext()) {
System.out.println(it.next().getTitle());
}
단순히 반복만 하려면 굳이 Iterable을 구현할 필요는 없지만
컬렉션처럼 쓰이고 싶다면 구현하는 것이 사실상 표준이라고 한다 ..
내부 구조를 몰라도 순회가 가능하며 일관된 방식으로 접근 가능 (for-each 루프)
컬렉션이 변경되어도 순회 로직은 고정
역방향 순회는 기본 Iterator로 불가
순회 중 구조 변경 시 예외 발생 가능
Iterator가 어떻게 제어되는가, 즉 누가 순회의 흐름을 제어하는가
사용자가 반복 과정을 직접 제어할 수 있는 반복자
hasNext(), next() 호출)Iterator, while, forIterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("stop")) break; // 흐름 제어 가능
}
반복 흐름을 컬렉션이나 함수에 전달 위임
forEach, Stream.forEach()list.forEach(item -> {
if (!item.equals("B"))
System.out.println(item); // 내부에서 반복됨
});
흐름은 넘길 수 있지만 break, return 등은 불완전하게 작동
(람다 안에서 return하면 람다 종료이지 전체 반복 종료가 아님)
컬렉션 내부 구조를 숨기고, 요소들을 하나씩 꺼내기 위한 반복자를 제공하며
복잡한 자료구조를 단순하게 다루도록 해주는 핵심 디자인 패턴
대부분의 프로그래밍 언어가 컬렉션 라이브러리에 기본 적용하고 있음