-
[Software Engineering] Design Pattern: Behavioral PatternsCS/Software Engineering 2026. 4. 10. 15:12
Creational이 객체 생성을, Structural이 객체 조합을 다뤘다면, Behavioral Pattern은 객체들 사이의 역할 분배와 협력 방식을 다룬다. 어떤 객체가 어떤 일을 담당하고, 서로 어떻게 소통할지를 정의하는 것이다.
이 글에서는 세 가지 Behavioral Pattern을 살펴본다: Template Method, Iterator, Observer.
1. Template Method Pattern
문제
PDF, CSV, XML 세 가지 형식의 파일을 분석하는 시스템을 만든다고 가정한다. 각 형식마다 별도의 클래스를 만들면 코드가 이렇게 된다.

열고, 읽고, 파싱하고, 분석하고, 리포트하고, 닫는 흐름 자체는 동일하다. 형식마다 다른 건 읽기(read)와 파싱(parse) 단계뿐이다.
그런데 세 클래스가 같은 흐름을 각자 중복으로 갖고 있다. 나중에 흐름에 변경이 생기면 세 곳을 모두 수정해야 한다.
해결
공통된 흐름(골격)은 부모 클래스가 갖고, 달라지는 부분만 서브클래스가 override하게 만든다. 이때 흐름을 정의하는 메서드를 Template Method라고 한다.

abstract class DataMiner { public final void process(String file) { // Template Method — final로 흐름 고정 open(file); String raw = readData(); // 서브클래스가 구현 Object data = parseData(raw); // 서브클래스가 구현 analyse(data); report(); close(); } void open(String file) { /* 공통 구현 */ } void analyse(Object data) { /* 공통 구현 */ } void report() { /* 공통 구현 */ } void close() { /* 공통 구현 */ } abstract String readData(); abstract Object parseData(String raw); } class PDFDataMiner extends DataMiner { String readData() { return readPDF(); } Object parseData(String raw) { return parsePDF(raw); } } class CSVDataMiner extends DataMiner { String readData() { return readCSV(); } Object parseData(String raw) { return parseCSV(raw); } }음료 준비 예시로도 같은 구조를 볼 수 있다. 커피든 차든 "물 끓이기 → 우리기 → 컵에 붓기 → 첨가물 추가"라는 흐름은 같고, 우리는 방법(brew)과 첨가물(addCondiments)만 다르다.
abstract class BeveragePreparation { public final void prepare() { // 흐름 고정 boilWater(); brew(); // 서브클래스 구현 pourIntoCup(); addCondiments(); // 서브클래스 구현 } void boilWater() { System.out.println("Boiling water"); } void pourIntoCup() { System.out.println("Pouring into cup"); } abstract void brew(); abstract void addCondiments(); } class CoffeePreparation extends BeveragePreparation { void brew() { System.out.println("Brewing coffee grounds"); } void addCondiments() { System.out.println("Adding sugar and milk"); } } class TeaPreparation extends BeveragePreparation { void brew() { System.out.println("Steeping tea bag"); } void addCondiments() { System.out.println("Adding lemon"); } }주의할 점
Template Method에서 중요한 건 final 키워드다. 흐름을 정의하는 prepare() 메서드는 서브클래스가 override하면 안 되기 때문에 final로 막는다. 만약 서브클래스가 흐름 자체를 바꿀 수 있다면 패턴의 의미가 사라진다.
서브클래스가 override하는 메서드는 두 종류다. 반드시 구현해야 하는 abstract 메서드와, 기본 구현이 있지만 필요에 따라 override할 수 있는 hook 메서드다. 서브클래스는 필요한 것만 override하면 된다.
2. Iterator Pattern
문제
컬렉션의 요소를 순회하는 코드를 작성할 때, 내부 자료구조가 배열인지 연결 리스트인지 트리인지에 따라 순회 방법이 달라지면 문제다.
// 배열 기반 컬렉션 for (int i = 0; i < items.length; i++) { process(items[i]); } // 연결 리스트 기반 컬렉션 Node curr = head; while (curr != null) { process(curr.data); curr = curr.next; }클라이언트가 내부 구조를 알아야만 순회할 수 있다. 나중에 자료구조가 배열에서 트리로 바뀌면 순회 코드도 전부 수정해야 한다. 클라이언트가 컬렉션의 구현 세부사항에 직접 결합된 것이다.
해결
순회 책임을 별도의 Iterator 객체에게 넘기는 것이다. 클라이언트는 hasNext()와 next() 두 메서드만 알면 되고, 내부가 배열이든 트리든 상관없이 동일한 방식으로 순회한다.

interface Menu { Iterator<MenuItem> createIterator(); } class AmericanDinerMenu implements Menu { private MenuItem[] items = new MenuItem[10]; // 배열로 관리 private int size = 0; public AmericanDinerMenu() { items[0] = new MenuItem("Pancake"); items[1] = new MenuItem("Waffles"); size = 2; } public Iterator<MenuItem> createIterator() { return new DinerIterator(items, size); } } class DinerIterator implements Iterator<MenuItem> { private MenuItem[] items; private int position = 0; private int size; public DinerIterator(MenuItem[] items, int size) { this.items = items; this.size = size; } public boolean hasNext() { return position < size && items[position] != null; } public MenuItem next() { return items[position++]; } }클라이언트 코드는 이렇게 된다
void printMenu(Menu menu) { Iterator<MenuItem> it = menu.createIterator(); while (it.hasNext()) { System.out.println(it.next().getName()); } } Menu diner = new AmericanDinerMenu(); printMenu(diner);printMenu는 메뉴 내부가 배열인지 전혀 모른다. 나중에 AmericanDinerMenu의 내부 자료구조가 List로 바뀌어도 printMenu 코드는 손댈 필요가 없다.
이미 쓰고 있다
Java에서 ArrayList, HashSet, LinkedList가 모두 Iterable<E>를 구현하는 것이 바로 Iterator Pattern이다. for-each 문이 동작하는 원리도 이 패턴이고, 내부가 배열 기반이든 해시 기반이든 똑같이 for (String s : collection)으로 순회할 수 있는 이유이기도 하다.
Composite + Iterator 조합
Iterator는 Composite Pattern과 함께 쓰면 더 강력해진다. 트리 구조로 된 파일 시스템을 순회하는 예시를 보면 이해가 빠르다.
class File(FileSystemComponent): def __iter__(self): yield self.name # 파일은 자기 자신만 반환 class Folder(FileSystemComponent): def __iter__(self): yield f"[{self.name}]" for child in self.children: yield from child # 자식의 이터레이터로 위임 # 클라이언트 for item in root: # 트리 구조인지 모르고 그냥 순회 print(item)
3. Observer Pattern
문제
특정 이벤트가 발생했을 때 반응해야 하는 상황을 생각해 보자. 가장 단순한 접근은 폴링(polling)이다. 주기적으로 상태를 직접 확인하는 방법이다.
while (true) { if (station.getTemperature() != lastTemp) { display.update(station.getTemperature()); lastTemp = station.getTemperature(); } Thread.sleep(1000); // 1초마다 확인 }문제는 두 가지다.- 변화가 없을 때도 계속 확인하니 자원 낭비다.
- 타이머 메커니즘이 필요하고 응답 지연이 생긴다. 화면이 10개라면 각자 폴링해야 하고, 더 심각해진다.
해결

이벤트의 소스(발행자, Publisher)가 관심 있는 객체들(구독자, Subscriber)의 목록을 들고 있다가, 변화가 생겼을 때 직접 알리는 것이다. 관심 있는 쪽이 먼저 등록(subscribe)하고, Publisher는 이벤트 발생 시 등록된 모두에게 통지한다.
날씨 스테이션 예시로 구현을 보면 이렇다.
interface Observer { void update(float temperature); } interface Subject { void registerObserver(Observer o); void removeObserver(Observer o); void notifyObservers(); } class WeatherStation implements Subject { private List<Observer> observers = new ArrayList<>(); private float temperature; public void registerObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { observers.remove(o); } public void notifyObservers() { for (Observer o : observers) { o.update(temperature); } } public void setTemperature(float newTemp) { this.temperature = newTemp; notifyObservers(); // 변화 발생 시 즉시 통지 } } class PhoneDisplay implements Observer { public void update(float temperature) { System.out.println("Phone Display: Temp is " + temperature + "°C"); } } class WindowDisplay implements Observer { public void update(float temperature) { System.out.println("Window Display: Temp is " + temperature + "°C"); } }클라이언트 코드와 실행 결과는 이렇다.
WeatherStation station = new WeatherStation(); Observer phone = new PhoneDisplay(); Observer window = new WindowDisplay(); station.registerObserver(phone); station.registerObserver(window); station.setTemperature(23.5f); // phone, window 둘 다 알림 station.setTemperature(25.0f); // 둘 다 알림 station.removeObserver(phone); // phone 구독 해제 station.setTemperature(22.0f); // window만 알림Phone Display: Temp is 23.5°C Window Display: Temp is 23.5°C Phone Display: Temp is 25.0°C Window Display: Temp is 25.0°C Window Display: Temp is 22.0°CObserver Pattern의 핵심 장점
Publisher는 Observer 목록을 Observer 인터페이스로만 들고 있다. PhoneDisplay인지 WindowDisplay인지 전혀 모른다. 새로운 디스플레이를 추가하고 싶으면 Observer를 구현하고 등록하기만 하면 된다. Publisher 코드는 단 한 줄도 수정할 필요가 없다.
이 패턴은 이미 여러 곳에서 익숙하게 쓰인다. JavaScript의 addEventListener, Spring의 ApplicationEvent, 그리고 React의 상태 변화가 컴포넌트 리렌더링을 유발하는 방식도 넓은 의미에서 Observer 패턴이다.
정리
Template Method Iterator Observer 핵심 문제 같은 알고리즘 흐름이 여러 곳에 중복 순회 방법이 자료구조에 결합 이벤트 감지를 위한 폴링의 비효율 해결 방식 흐름은 부모가, 세부 단계는 서브클래스가 순회 책임을 Iterator 객체로 분리 Publisher가 변화 시 Subscriber에게 직접 통지 핵심 구조 상속 + abstract 메서드 컬렉션과 순회 로직 분리 등록/해제가 가능한 느슨한 결합 세 패턴이 공통적으로 추구하는 것도 한 가지다. 변하는 것과 변하지 않는 것을 분리하는 것이다. Template Method는 알고리즘의 흐름(불변)과 세부 단계(가변)를 분리하고, Iterator는 자료구조(가변)와 순회 방식(불변)을 분리하며, Observer는 Publisher의 로직(불변)과 구독자의 수와 종류(가변)를 분리한다.
Design Pattern의 본질과 비판
세 글에 걸쳐 Creational, Structural, Behavioral Pattern을 모두 살펴봤다. 마지막으로 디자인 패턴에 대한 비판도 짚어두는 게 좋겠다.
GoF 패턴에 대한 비판 중 가장 날카로운 것은 두 가지다.
- 패턴이 설계를 생각하는 도구가 아니라 복사·붙여넣기 대상이 됐다는 것이다. 문제를 깊이 이해하고 구조를 설계하는 대신, 패턴 이름을 붙이고 보일러플레이트를 찍어내는 방식으로 전락했다는 지적이다.
- 일부 패턴은 언어가 충분히 강력하지 않아서 생겨난 우회책에 불과하다. Python이나 Kotlin처럼 일급 함수(first-class function)를 지원하는 언어에서는 Strategy 패턴을 굳이 클래스 계층으로 구현하지 않고 함수 하나로 표현할 수 있다.
패턴은 목적이 아니라 도구다. 좋은 설계가 결과적으로 어떤 패턴과 닮아 있을 수 있지만, 패턴을 따랐다고 해서 자동으로 좋은 설계가 되는 건 아니다.
망치를 들면 모든 게 못으로 보인다는 말처럼, 패턴을 알게 되면 모든 문제에 패턴을 끼워 맞추고 싶어지는 함정이 있다.
중요한 건 패턴의 이름이 아니라, 그 패턴이 어떤 문제를 왜 그 방식으로 해결하는지를 이해하는 것이다.
출처: 경북대학교 손정주 교수님, "소프트웨어공학" 강의 자료
'CS > Software Engineering' 카테고리의 다른 글
[Software Engineering] Requirement Engineering (2) 2026.04.12 [Software Engineering] 버전 관리 시스템(VCS): Git 핵심 (1) 2026.04.11 [Software Engineering] Design Pattern: Structural Patterns (0) 2026.04.09 [Software Engineering] Design Pattern: Creational Patterns (1) 2026.04.08 [Software Engineering] SW Architecture (0) 2026.04.08