백엔드 개발자
[디자인 패턴] 행동 패턴 본문
디자인 패턴은 효율적이고 유지보수하기 쉬운 코드 구조를 만들기 위한 설계 원칙.
그중에서도 행동(Behavioral) 패턴은 객체 간의 상호작용 및 책임 분배를 최적화하는 패턴들로, 객체 간 결합도를 줄이고 유연성을 높이는 데 중요한 역할을 한다.
1. 옵저버 패턴 (Observer Pattern)
→ 주체(Subject)의 상태 변화가 있을 때, 이를 감지하고 자동으로 알림을 받는 패턴
✅ 개념
- 한 객체(Subject)의 상태가 변하면 등록된 옵저버(Observer)들에게 자동으로 알림을 보냄
- 이벤트 기반 시스템에서 많이 활용됨
- MVC(Model-View-Controller) 패턴에서 View가 Model의 변경 사항을 감지할 때 사용됨
✅ 구현 방법 (Java 코드)
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
class Subject {
private List<Observer> observers = new ArrayList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// 실행 예제
public class ObserverPatternExample {
public static void main(String[] args) {
Subject subject = new Subject();
Observer observer1 = new ConcreteObserver("Observer 1");
Observer observer2 = new ConcreteObserver("Observer 2");
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers("Hello, Observers!");
}
}
✅ 특징
- Subject는 Observer 목록을 관리하고, 상태 변경 시 자동으로 알림을 보냄
- 느슨한 결합(Loose Coupling)을 제공하여 유지보수가 용이함
✅ 실제 사용 예시
- 이벤트 리스너 (GUI 프로그램에서 버튼 클릭 이벤트 처리)
- 메시징 시스템 (Kafka, RabbitMQ)
- Stock Price Tracker (주식 시장에서 가격 변경 감지)
2. 전략 패턴 (Strategy Pattern)
→ 실행 중에 알고리즘을 바꿀 수 있도록 인터페이스를 정의하는 패턴
✅ 개념
- 여러 개의 알고리즘을 동적으로 변경할 수 있도록 인터페이스를 분리
- if-else 분기문을 없애고 코드 유지보수를 용이하게 함
✅ 구현 방법 (Java 코드)
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal.");
}
}
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// 실행 예제
public class StrategyPatternExample {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(200);
}
}
✅ 특징
- 코드 재사용성이 높아지고, 새로운 알고리즘 추가가 용이
- 클라이언트 코드가 알고리즘의 구현을 몰라도 됨
✅ 실제 사용 예시
- 결제 시스템 (신용카드, PayPal, 네이버페이 등)
- 데이터 압축 알고리즘 선택 (ZIP, RAR, GZIP)
- AI 게임 캐릭터 행동 변경
3. 상태 패턴 (State Pattern)
→ 객체의 상태에 따라 행동을 다르게 수행하는 패턴
✅ 개념
- 객체가 다양한 상태를 가질 때, 상태에 따라 동작을 변경하는 패턴
- if-else 없이 상태별 클래스를 생성하여 유지보수성을 높임
✅ 구현 방법 (Java 코드)
interface State {
void handle();
}
class StartState implements State {
public void handle() {
System.out.println("Game is in Start State.");
}
}
class PlayState implements State {
public void handle() {
System.out.println("Game is in Play State.");
}
}
class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void execute() {
state.handle();
}
}
// 실행 예제
public class StatePatternExample {
public static void main(String[] args) {
Context game = new Context();
game.setState(new StartState());
game.execute();
game.setState(new PlayState());
game.execute();
}
}
✅ 특징
- 객체의 상태 전이에 따른 행동을 캡슐화
- if-else 문을 줄이고 코드의 확장성을 높임
✅ 실제 사용 예시
- 게임에서 캐릭터의 상태 관리 (Idle, Attack, Dead)
- 문서 편집기에서 파일 상태 (읽기 전용, 편집 가능)
- 스레드의 실행 상태 (Runnable, Blocked, Terminated)
4. 반복자 패턴 (Iterator Pattern)
✅ 개념
반복자(Iterator) 패턴은 컬렉션(배열, 리스트 등) 요소들을 순차적으로 접근할 수 있도록 하는 디자인 패턴이다.
내부 구현을 드러내지 않고도 순회(Iteration)를 할 수 있는 방법을 제공한다.
💡 왜 반복자 패턴을 사용할까?
- 컬렉션의 내부 구조를 알 필요 없이 순회 가능
- 하나의 반복자로 다양한 컬렉션을 다룰 수 있음 (유연성)
- 일관된 인터페이스로 여러 자료구조를 순회할 수 있음
- 객체 간 결합도를 낮추고(Loosely Coupled), 유지보수를 쉽게 만듦
✅ 반복자 패턴 구조
- Iterator 인터페이스: 컬렉션의 요소들을 순회하는 메서드를 정의
- ConcreteIterator (구체적인 반복자): 실제 순회 기능을 구현하는 클래스
- Aggregate (컬렉션 인터페이스): 컬렉션을 관리하고, 반복자를 생성하는 메서드를 제공
- ConcreteAggregate (구체적인 컬렉션): 컬렉션 데이터를 저장하고, 반복자를 반환하는 클래스
✅ 반복자 패턴 구현 (Java)
📌 1. Iterator 인터페이스 정의
interface Iterator<T> {
boolean hasNext(); // 다음 요소가 있는지 확인
T next(); // 다음 요소 반환
}
📌 2. ConcreteIterator 구현 (구체적인 반복자)
class BookIterator implements Iterator<Book> {
private List<Book> books;
private int index = 0;
public BookIterator(List<Book> books) {
this.books = books;
}
@Override
public boolean hasNext() {
return index < books.size();
}
@Override
public Book next() {
return books.get(index++);
}
}
📌 3. Aggregate 인터페이스 정의 (컬렉션 인터페이스)
interface IterableCollection<T> {
Iterator<T> createIterator(); // 반복자 생성 메서드
}
📌 4. ConcreteAggregate 구현 (구체적인 컬렉션)
import java.util.ArrayList;
import java.util.List;
class BookCollection implements IterableCollection<Book> {
private List<Book> books = new ArrayList<>();
public void addBook(Book book) {
books.add(book);
}
@Override
public Iterator<Book> createIterator() {
return new BookIterator(books);
}
}
📌 5. Book 객체 정의
class Book {
private String title;
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
📌 6. 실행 코드 (Iterator 패턴 적용)
public class IteratorPatternExample {
public static void main(String[] args) {
BookCollection bookCollection = new BookCollection();
bookCollection.addBook(new Book("Design Patterns"));
bookCollection.addBook(new Book("Effective Java"));
bookCollection.addBook(new Book("Clean Code"));
Iterator<Book> iterator = bookCollection.createIterator();
while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println("Book Title: " + book.getTitle());
}
}
}
✅ 실행 결과
Book Title: Design Patterns
Book Title: Effective Java
Book Title: Clean Code
✅ 특징
- 컬렉션의 내부 구조를 감추고 반복 가능 → 캡슐화(Encapsulation) 향상
- 여러 종류의 컬렉션에 대해 같은 방식으로 순회 가능 → 유연성 증가
- 일관된 인터페이스 제공 → 코드의 유지보수성 향상
✅ 실제 사용 예시
1️⃣ Java 컬렉션 프레임워크 (Java Collection Framework)
- Iterator<E> 인터페이스 사용:
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
위의 방식은 반복자 패턴을 활용한 컬렉션 순회 방식이다.
2️⃣ 데이터베이스 결과셋 처리 (JDBC)
- JDBC의 ResultSet 객체는 반복자 패턴을 사용:
ResultSet rs = statement.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println("User: " + rs.getString("name"));
}
3️⃣ 게임 개발 - NPC 순회
- 게임의 AI에서 NPC 리스트를 순회하는 경우
- ex) NPC를 탐색하고 특정 행동을 수행할 때 반복자 패턴 사용
5. 템플릿 메서드 패턴 (Template Method Pattern)
✅ 개념
템플릿 메서드 패턴(Template Method Pattern)은 상위 클래스에서 알고리즘의 기본 구조(템플릿)를 정의하고, 하위 클래스에서 세부적인 구현을 결정하는 디자인 패턴이다.
즉, 공통된 알고리즘의 뼈대를 제공하면서도 일부 단계는 서브클래스에서 구현하도록 강제하는 패턴이다.
✅ 왜 템플릿 메서드 패턴을 사용할까?
- 알고리즘의 흐름을 통제하면서, 일부 단계만 변경할 수 있도록 유연성을 제공
- 코드 중복을 줄이고, 유지보수를 쉽게 만듦
- 객체지향 설계 원칙 중 "Hollywood Principle"을 따름
→ "Don't call us, we'll call you" (하위 클래스에서 직접 호출하는 것이 아니라, 상위 클래스가 호출)
✅ 템플릿 메서드 패턴 구조
역할설명
AbstractClass (추상 클래스) | 알고리즘의 기본 구조(템플릿)를 정의하고, 일부 단계를 추상 메서드로 선언 |
---|---|
ConcreteClass (구체적인 하위 클래스) | 상위 클래스에서 정의한 알고리즘의 특정 단계를 구현 |
✅ 템플릿 메서드 패턴 구현 (Java 예제)
📌 1. AbstractClass (추상 클래스) 정의
abstract class Game {
// 템플릿 메서드 (알고리즘의 틀을 정의)
public final void play() {
initialize();
startPlay();
endPlay();
}
// 공통 로직 (변경되지 않는 부분)
private void initialize() {
System.out.println("게임을 초기화합니다.");
}
// 하위 클래스에서 구현해야 하는 메서드
protected abstract void startPlay();
protected abstract void endPlay();
}
✅ play() 메서드가 템플릿 메서드이며, initialize()는 공통 로직, startPlay()와 endPlay()는 하위 클래스가 구현해야 하는 메서드.
📌 2. ConcreteClass (구체적인 하위 클래스) 구현
class Football extends Game {
@Override
protected void startPlay() {
System.out.println("축구 경기를 시작합니다!");
}
@Override
protected void endPlay() {
System.out.println("축구 경기가 종료되었습니다.");
}
}
class Basketball extends Game {
@Override
protected void startPlay() {
System.out.println("농구 경기를 시작합니다!");
}
@Override
protected void endPlay() {
System.out.println("농구 경기가 종료되었습니다.");
}
}
✅ Football과 Basketball 클래스는 Game 클래스를 상속받아 startPlay()와 endPlay()를 구현함.
📌 3. 실행 코드
public class TemplateMethodExample {
public static void main(String[] args) {
Game football = new Football();
football.play();
System.out.println();
Game basketball = new Basketball();
basketball.play();
}
}
✅ 실행 결과
게임을 초기화합니다.
축구 경기를 시작합니다!
축구 경기가 종료되었습니다.
게임을 초기화합니다.
농구 경기를 시작합니다!
농구 경기가 종료되었습니다.
✅ play() 메서드는 initialize() → startPlay() → endPlay() 순서로 실행되며,
각 게임마다 startPlay()와 endPlay()가 다르게 동작함.
✅ 특징
✔ 코드 재사용성 증가 → initialize() 같은 공통 로직을 상속받아 여러 클래스에서 사용 가능
✔ 알고리즘의 흐름을 유지하면서 세부 구현은 하위 클래스에서 결정
✔ 클라이언트가 알고리즘의 세부사항을 몰라도 됨
✅ 실제 사용 예시
1️⃣ Java의 java.io.InputStream (템플릿 메서드 패턴 사용)
- read() 메서드가 템플릿 메서드이며, 하위 클래스에서 read(byte[] b, int off, int len) 등을 구현
InputStream inputStream = new FileInputStream("file.txt");
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
2️⃣ Spring Framework에서 템플릿 메서드 패턴 적용 예시
- JdbcTemplate을 사용하면, DB 연결, SQL 실행, 리소스 해제 등의 공통 과정이 캡슐화됨
- 개발자는 SQL 실행 부분만 구현하면 됨.
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
List<User> users = jdbcTemplate.query("SELECT * FROM users",
(rs, rowNum) -> new User(rs.getInt("id"), rs.getString("name"))
);
✅ query() 메서드는 템플릿 메서드 역할을 하고,
✅ 개발자는 람다식으로 RowMapper를 구현하여 필요한 로직만 제공하면 됨.
3️⃣ 게임 개발 - AI 행동 패턴
- AI 적(enemy)이 같은 알고리즘을 따르지만, 개별 행동(공격 방식 등)이 다를 때 사용
abstract class EnemyAI {
public final void attack() {
move();
specialMove();
retreat();
}
protected abstract void move();
protected abstract void specialMove();
private void retreat() {
System.out.println("적이 후퇴합니다.");
}
}
class Goblin extends EnemyAI {
protected void move() {
System.out.println("고블린이 빠르게 이동합니다.");
}
protected void specialMove() {
System.out.println("고블린이 돌을 던집니다!");
}
}
class Dragon extends EnemyAI {
protected void move() {
System.out.println("드래곤이 하늘을 날아다닙니다.");
}
protected void specialMove() {
System.out.println("드래곤이 불을 내뿜습니다!");
}
}
✅ AI 적들이 같은 attack() 알고리즘을 따르지만, 행동(move()와 specialMove())이 다르게 구현됨.