[Design Patterm] 3. 행위(Behavioral) 패턴(1) - Observer, Template Method

김민경·2025년 6월 22일

Design Pattern

목록 보기
5/9
post-thumbnail

행위(Behavioral) 패턴

객체나 클래스 사이의 책임 분산과 커뮤니케이션 방식에 초첨을 맞춘 디자인 패턴

  1. 객체들 간의 유연한 통신을 도와줌
  2. 알고리즘, 제어 흐름, 요청 처리를 동적으로 변경 가능하게 함
  3. 클래스 사이의 결합도를 낮추고, 유지보수성과 확장성을 높임

즉, 객체들이 어떻게 상호작용하고, 책임을 어떻게 나누는지를 정의하는 패턴


옵저버 (Observer) 패턴

한 객체의 상태가 바뀌면, 그 객체에 의존하는 다른 객체들에게 자동으로 알림을 보내는 패턴

Publiser-Subscriber(발행자-구독자) 모델이라고도 불림

  • Subject(발행자) : 상태를 가지고 있으며, 상태가 바뀌면 옵저버들에게 알림
  • Observer(구독자) : Subject의 상태 변화에 따라 자신의 상태 업데이트

객체 간 느슨한 결합(loose coupling)을 유지하면서도 유연한 확장이 가능
Subject는 자신이 어떤 Observer를 가지고 있는지 알지만, 그들의 구체적인 동작은 알지 못함.


예제

발행, 구독 구조의 대표적인 '뉴스 구독' 으로 예를 들어보자

우선 뉴스를 발행하는 Subject뉴스를 구독하는 Observer의 인터페이스 구성

public interface Observer {
    void update(String news);
}

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

뉴스를 발행함과 동시에 구독한 Observer를 관리하는 NewsAgency 구현체
뉴스를 발행할 때(setNews())마다 구독자(Observer)들에게 알림을 보내는 함수 notifyObservers()

public class NewsAgency implements Subject{
    private List<Observer> observers = new ArrayList<>();
    private String news;

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(news);
        }
    }

    public void setNews(String news){
        this.news = news;
        notifyObservers();
    }
}

뉴스를 구독하는 NewsReader 구현체
update()를 통해 Subject에서 업데이트 된 내역을 전달

public class NewsReader implements Observer{
    private String name;

    public NewsReader(String name){
        this.name = name;
    }
    @Override
    public void update(String news) {
        System.out.println(name + " received news : " + news);
    }
}

구독자 reader1, reader2를 생성 후 NewsAgency를 구독
뉴스를 발행할 때(setNews())마다 구독자(Observer)들에게 알림이 가는 모습으로 reader2를 삭제한 이후에는 reader2에게 알림이 가지 않는다.

public class NewsMain {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();
        Observer reader1 = new NewsReader("guest1");
        Observer reader2 = new NewsReader("guest2");

        agency.registerObserver(reader1);
        agency.registerObserver(reader2);

        agency.setNews("Big News !");
        agency.removeObserver(reader2);
        System.out.println("==================");
        System.out.println("Unsubscribe reader2");
        System.out.println("==================");
        agency.setNews("Second Big News!");
    }
}

/*
guest1 received news : Big News !
guest2 received news : Big News !
==================
Unsubscribe reader2
==================
guest1 received news : Second Big News!
 */

특징

  1. 객체 간의 느슨한 결합 및 확장성
    Subject는 Observer의 내부 구현을 몰라도 되며, Observer를 쉽게 추가하거나 제거할 수 있음
  1. 자동 알림
    상태 변경 시 즉각 반영 가능
  1. Observer 이슈
    Observer 수가 많으면 성능적으로 이슈가 발생할 수 있으며, 호출 순서를 보장하는 데 어려움이 있음

결론

변화를 감지하고 자동으로 반응해야 하는 구조에 매우 적합한 패턴이지만
의존성 관리와 성능에 주의해야 하며, 옵저버가 많아질수록 복잡해질 수 있음


템플릿 메소드 (Template Method) 패턴

알고리즘의 뼈대(템플릿)를 상위 클래스에서 정의하고, 그 세부 동작은 하위 클래스에서 구현하는 디자인 패턴

전체적인 처리 흐름은 고정하고, 그 중 일부 단계는 서브클래스에서 오버라이드하여 다르게 동작

  • 상위 클래스는 템플릿 메서드라는 고정된 로직의 틀을 제공
  • 하위 클래스는 세부 단계의 메서드만 재정의(Override)하여 다양하게 행동 가능

코드 중복을 줄이고, 알고리즘 구조는 유지하면서도 각기 다른 동작을 수행할 수 있게 함


예제

아메리카노와 라떼를 만드는 과정에서 동일한 과정은 상위 메서드에 구현하고, 다른 과정은 하위 메서드에서 구현

  1. 물을 끓이고 (boilWater())
  2. 원두를 양조하고 (brew())
  3. 컵에 따르고 (pourInCup())
  4. 어떤 첨가물을 추가적으로 넣는 (addCondiments())

과정은 고정된 상태

하지만 원두를 양조하고, 첨가물을 추가에 대한 세부적인 구현은 어떤 커피를 만드냐에 따라 달라질 수 있음 (추상화)

//Abstract Class : Template Method 제공
abstract class CoffeeTemplate {

    //Template Method : 고정된 알고리즘 구조
    public final void makeCoffee(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    private void pourInCup() {
        System.out.println("Pour In Cup");
    }

    private void boilWater() {
        System.out.println("Boil Water");
    }

    // Abstract Method : 하위 클래스가 구현
    protected abstract void addCondiments();

    protected abstract void brew();
}

아메리카노와 라떼에 대하여 추상화된 메서드를 구현

public class Americano extends CoffeeTemplate{

    @Override
    protected void brew() {
        System.out.println("Americano : brew Coffee Beans");
    }

    @Override
    protected void addCondiments() {
        System.out.println("No Condiments");
    }
}

public class Latte extends CoffeeTemplate{

    @Override
    protected void brew() {
        System.out.println("Latte : Get Espresso");
    }

    @Override
    protected void addCondiments() {
        System.out.println("Add Milk");
    }
}

CoffeeTemplate의 인터페이스에서 makeCoffee() 라는 동일한 메서드를 실행하지만, 생성된 인스턴스에 따라 진행되는 과정이 다른 것을 확인

public class CoffeeMain {
    public static void main(String[] args) {
        CoffeeTemplate americano = new Americano();
        americano.makeCoffee();
        System.out.println();

        CoffeeTemplate latte = new Latte();
        latte.makeCoffee();
    }
}

/*
Boil Water
Americano : brew Coffee Beans
Pour In Cup
No Condiments

Boil Water
Latte : Get Espresso
Pour In Cup
Add Milk
 */

특징

  1. 알고리즘 구조 고정 및 코드 재사용성 증가
    전체 흐름을 통제할 수 있고, 공통 로직을 상위 클래스에 정의함으로써 재사용성
  1. 유연성
    서브 클래스에서 특정 단계만 수정 가능

  2. 상속에 의존하며 LSP(리스코프 치환 원칙) 위반 위험

    클래스 간 강한 결합 가능성이 있으며, 모든 서브 클래스가 알고리즘 단계를 알아야 함
    -> 따라서 상위 클래스가 일정한 실행 흐름을 강제할 때, 하위 클래스가 그 기대를 벗어나는 행동을 하면 LSP를 위반하고 패턴의 의미를 무너뜨리게 됨

결론

공통된 알고리즘 틀을 제공하면서도, 특정 단계는 유연하게 확장하고자 할 때 적합한 패턴
!! 프레임워크나 라이브러리처럼 개발자가 일부 동작만 커스터마이징하면 되는 구조에 적합
상속의 한계를 고려해, 필요에 따라 전략 패턴 등 다른 방법과 함께 고려할 필요도 있음.

그럼 전략 패턴이란?

profile
뭐든 기록할 수 있도록

0개의 댓글