CodeGym /행동 /JAVA 25 SELF /Observer 패턴 (관찰자)

Observer 패턴 (관찰자)

JAVA 25 SELF
레벨 50 , 레슨 3
사용 가능

1. "관찰자" 패턴 알아보기

"관찰자"(Observer) 패턴은 가장 잘 알려진 기초적인 디자인 패턴 중 하나입니다. 하나의 객체(피관찰자, 또는 subject)가 자신의 변경 사항을 그 변경에 구독한 다른 객체들(관찰자, observers)에게 알리는 상황을 설명합니다.

좀 더 쉽게 말하면: 텔레그램 채널(피관찰자 객체)이 있고, "구독자"(관찰자)가 있습니다. 새 게시물이 올라올 때마다 채널은 모든 구독자에게 알리고, 구독자는 이를 읽거나 무시하거나 구독을 해지하는 등 무엇을 할지 결정합니다.

프로그래밍에서 이 패턴은 객체들을 서로 직접적으로 강하게 결합하지 않고, 관심 있는 객체들에게 이벤트나 상태 변경을 자동으로 통지할 수 있게 해 줍니다. 이는 유연하고 확장 가능하며 유지 관리가 쉬운 시스템을 만드는 데 중요합니다.

"관찰자" 패턴은 어디에서 쓰일까요?

  • 그래픽 UI(Swing, AWT, JavaFX) — 이벤트 리스너.
  • 리액티브 라이브러리(RxJava, Project Reactor).
  • 비즈니스 로직: 모델 상태 변경에 대한 반응.
  • 게임 엔진(충돌, 승리, 패배 등의 이벤트).
  • "무엇이 일어났는가"와 "그것을 어떻게 처리할까"를 분리해야 하는 모든 곳.

Java의 이벤트/리스너와 패턴의 연관

사실상 Java의 이벤트 모델은 "관찰자"에 기반합니다. 다음과 같이 작성할 때 button.addActionListener(listener); 이 패턴을 구현하는 것입니다:

  • 피관찰자 — 버튼(또는 다른 컴포넌트).
  • 관찰자actionPerformed() 메서드를 구현한 리스너.
  • 이벤트 — 사용자가 클릭, 마우스 오버 등을 함.
  • 알림 — 컴포넌트가 actionPerformed()를 호출.

이 모든 것이 Observer의 전형적인 구현입니다!

2. "관찰자" 패턴의 고전적 구현

Swing과 AWT 없이, 직접 클래스에 패턴을 구현해 보며 "마법"이 없음을 확인해 봅시다.

패턴의 핵심 요소

  • Observable (Subject) — 피관찰자 객체. 관찰자 목록을 보관하고 변경 사항을 알립니다.
  • Observer — 관찰자 인터페이스로, 보통 update() 같은 메서드를 가집니다.

예제: 온도계와 에어컨

관찰자 인터페이스

public interface TemperatureObserver {
    void temperatureChanged(int newTemperature);
}

클래스 "Thermometer"(피관찰자)

import java.util.*;

public class Thermometer {
    private int temperature;
    private final List<TemperatureObserver> observers = new ArrayList<>();

    public void addObserver(TemperatureObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(TemperatureObserver observer) {
        observers.remove(observer);
    }

    public void setTemperature(int newTemperature) {
        if (this.temperature != newTemperature) {
            this.temperature = newTemperature;
            notifyObservers();
        }
    }

    private void notifyObservers() {
        for (TemperatureObserver observer : observers) {
            observer.temperatureChanged(temperature);
        }
    }
}

관찰자 예시 — "AirConditioner"

public class AirConditioner implements TemperatureObserver {
    @Override
    public void temperatureChanged(int newTemperature) {
        if (newTemperature > 25) {
            System.out.println("에어컨이 켜졌습니다! 덥습니다: " + newTemperature + "°C");
        } else {
            System.out.println("에어컨이 꺼졌습니다. 온도: " + newTemperature + "°C");
        }
    }
}

사용 예

public class Main {
    public static void main(String[] args) {
        Thermometer thermometer = new Thermometer();
        AirConditioner conditioner = new AirConditioner();

        thermometer.addObserver(conditioner);

        thermometer.setTemperature(22); // 에어컨이 꺼졌습니다. 온도: 22°C
        thermometer.setTemperature(28); // 에어컨이 켜졌습니다! 덥습니다: 28°C
    }
}

이게 전부입니다! 관찰자를 백 개 더 추가해도, 온도가 바뀌면 모두 알림을 받습니다.

패턴의 시각적 다이어그램

flowchart LR
    T["온도계 (Observable)"] -- 알림 --> AC["에어컨 (Observer)"]
    T -- 알림 --> L["로거 (Observer)"]
    T -- 알림 --> Alarm["알람 (Observer)"]

현대적 세부사항: 폐기된 Observable과 새로운 접근

표준 Java 라이브러리에는 java.util.Observablejava.util.Observer가 존재했지만, Java 9부터 폐기(Deprecated)되었습니다. 이유는 유연성이 부족하기 때문입니다(예를 들어 Observable은 클래스이므로 다른 클래스를 상속받기 어렵습니다).

현대적인 접근은 리스너 인터페이스와 구독/해지 로직을 직접 설계하는 것입니다(위 예제처럼). 이 방식이 더 유연하고 안전하며, 실제 요구 사항에 더 잘 맞습니다.

3. 예제: 구독자가 있는 미니 애플리케이션

"클릭 카운터"를 만들고 값 변경에 구독할 수 있게 해 봅시다.

리스너 인터페이스

public interface CounterListener {
    void counterChanged(int newValue);
}

카운터 클래스

import java.util.*;

public class Counter {
    private int value = 0;
    private final List<CounterListener> listeners = new ArrayList<>();

    public void addCounterListener(CounterListener l) {
        listeners.add(l);
    }

    public void removeCounterListener(CounterListener l) {
        listeners.remove(l);
    }

    public void increment() {
        value++;
        notifyListeners();
    }

    private void notifyListeners() {
        for (CounterListener l : listeners) {
            l.counterChanged(value);
        }
    }

    public int getValue() {
        return value;
    }
}

리스너: 메시지 출력

public class ConsoleCounterListener implements CounterListener {
    @Override
    public void counterChanged(int newValue) {
        System.out.println("카운터가 변경되었습니다: " + newValue);
    }
}

사용 예

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.addCounterListener(new ConsoleCounterListener());

        counter.increment(); // 카운터가 변경되었습니다: 1
        counter.increment(); // 카운터가 변경되었습니다: 2
    }
}

4. 유용한 팁

현대적 대안과 확장

실제 프로젝트에서는 익명 클래스나 람다 표현식으로 구독하는 경우가 많습니다: counter.addCounterListener(newValue -> System.out.println("새 값: " + newValue));

(이렇게 하려면 인터페이스가 함수형이어야 합니다 — 추상 메서드가 하나만 있어야 합니다.)

또한 리액티브 라이브러리(RxJava, Project Reactor)가 널리 쓰이며, 여기서는 이벤트 스트림, 필터링, 비동기 처리 등을 지원하는 형태로 "관찰자"가 구현됩니다. 본질을 이해하기에는 위에서 다룬 고전적 구조로 충분합니다.

"관찰자" 패턴의 실무 적용

  • 데이터 모델. 모델(할 일 목록, 상품, 사용자 등)의 변경이 뷰에 알림을 보내 갱신합니다.
  • 로깅. 로거 구독자가 시스템 전반의 이벤트에 반응합니다.
  • 알림. 상태 변경 시 email, 푸시 알림, Telegram 메시지 전송.
  • 게임. 체력 변화, 적 출현, 레벨 완료 등.
  • 멀티스레딩. 한 스레드는 이벤트를 발행하고, 다른 스레드들이 반응합니다.

5. Observer 패턴 구현 시 흔한 실수

오류 №1: 리스너 제거를 잊음. 리스너가 더 이상 필요 없는데 제거하지 않으면 계속 알림을 받습니다. 오래 실행되는 애플리케이션에서는 메모리 누수로 이어질 수 있습니다.

오류 №2: 처리기에서 오래 걸리는/블로킹 작업. 처리기가 무거운 작업(IO, DB)을 수행하면 애플리케이션이 "멈춘" 것처럼 보일 수 있습니다. 특히 알림이 UI 스레드에서 발생한다면 더욱 그렇습니다. 무거운 작업은 백그라운드 스레드로 옮기세요.

오류 №3: 리스너에서 발생한 예외. 한 리스너에서 예외가 발생하면 나머지에게 알림이 중단될 수 있습니다. 리스너 호출을 try-catch로 감싸고 오류를 로깅하세요.

오류 №4: 동일한 리스너의 중복 등록. 같은 리스너가 여러 번 추가되면 이벤트를 그 횟수만큼 중복으로 받습니다. 등록을 관리하고 중복 추가를 방지하세요.

오류 №5: 피관찰자와 관찰자 간의 강한 결합. 피관찰자가 관찰자의 구체 구현에 대해 알게 되면 느슨한 결합이 깨집니다. 인터페이스만 사용하세요(예: TemperatureObserver, CounterListener).

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION