Design Patter6: Observer Pattern

업데이트:
8 분 소요

비트캠프 서초본원 엄진영 강사님의 수업을 듣고 정리했습니다.


Observer Pattern

1. Observer Pattern?

  • 특정 객체의 상태 변화에 따라 수행해야 하는 작업이 있을 경우, 기존 코드를 손대지 않고 손쉽게 기능을 추가하거나 제거할 수 있는 설계 기법이다.
  • 행(publish)/구독(subscribe) 모델 이라고 부르기도 한다.
  • 발행 측(publisher)에서는 구독 객체(subscriber)의 목록을 유지할 컬렉션을 가지고 있다.
  • 또한 구독 객체를 등록하거나 제거하는 메서드가 있다.
  • 구독 객체를 리스너(listener) 또는 관찰자(observer)라 부르기도 한다.

2. Observer Pattern 적용 전

프로젝트 완료

public class Car {
  public void start() {
    System.out.println("시동을 건다.");
  }

  public void run() {
    System.out.println("달린다.");
  }

  public void stop() {
    System.out.println("시동을 끈다.");
  }
}
public class Test01 {
  public static void main(String[] args) {
    Car car = new Car();
    car.start();
    car.run();
    car.stop();
  }
}

프로젝트 완료 후 기능 추가

  • Observer Pattern을 적용하기 전에는 프로젝트 완료 후 기능을 추가하는 상황이 발생한다면, 기존 코드를 수정하여 새로운 기능을 추가해야 한다.

    (1) 자동차의 시동을 걸 때 안전벨트 착용 여부를 검사하는 기능을 추가한다.

    ⇒ 기존 Car 클래스의 start() 메서드에 코드를 추가

    (2) 자동차 시동 걸 때 엔진 오일 검사 기능을 추가한다.

    ⇒ Car 클래스의 start() 메서드에 해당 기능을 수행하는 코드를 추가한다.

    (3) 자동차 시동 걸 때 브레이크 오일 검사 기능을 추가한다.

    ⇒ Car의 start() 메서드에 해당 코드 추가

public class Car {
  public void start() {
    System.out.println("시동을 건다.");

    // 1월 20일 - 자동차 시동을 걸 때 안전 벨트 착용 여부를 검사하는 기능을 추가
    System.out.println("안전벨트 착용 여부 검사");

    // 2월 30일 - 자동차 시동을 걸 때 엔진 오일 유무를 검사하는 기능을 추가
    System.out.println("엔진 오일 유무 검사");

    // 3월 2일 - 자동차 시동을 걸 때 브레이크 오일 유무를 검사하는 기능을 추가
    System.out.println("브레이크 오일 유무 검사");
  }

  public void run() {
    System.out.println("달린다.");
  }

  public void stop() {
    System.out.println("시동을 끈다.");
  }
}
  • 기능 업그레이드 후 또 다른 기능을 추가하게 된다면, 마찬가지로 기존 코드를 수정하여 새로운 기능을 추가해야 했다.

    (4) 시동 끌 때 자동차 전조등을 자동으로 끄는 기능을 추가한다.

    ⇒ Car의 stop() 메서드에 해당 코드 추가

    (5) 시동 끌 때 썬루프를 자동으로 닫기

    ⇒ Car의 stop() 메서드에 해당 코드 추가

public class Car {
  public void start() {
    System.out.println("시동을 건다.");
    
    // 1월 20일 - 자동차 시동을 걸 때 안전 벨트 착용 여부를 검사하는 기능을 추가
    System.out.println("안전벨트 착용 여부 검사");
    
    // 2월 30일 - 자동차 시동을 걸 때 엔진 오일 유무를 검사하는 기능을 추가 
    System.out.println("엔진 오일 유무 검사");
    
    // 3월 2일 - 자동차 시동을 걸 때 브레이크 오일 유무를 검사하는 기능을 추가
    System.out.println("브레이크 오일 유무 검사");
  }
  
  public void run() {
    System.out.println("달린다.");
  }
  
  public void stop() {
    System.out.println("시동을 끈다.");
    
    // 4월 18일 - 자동차 시동을 끌 때 전조등 자동 끄기 기능을 추가
    System.out.println("전조등을 끈다.");
    
    // 5월 5일 - 자동차 시동을 끌 때 썬루프 자동 닫기 기능을 추가
    System.out.println("썬루프를 닫는다.");
  }
}

기존 코드를 변경할 때 나타날 수 있는 문제점

  • 어떤 고객은 해당 기능이 필요 없을 수도 있다. 이런 경우, 조건문을 추가하여 기능의 동작 여부를 제어해야 한다.
  • 코드가 복잡해진다. 이미 디버깅과 테스트가 완료된 기존 코드를 변경하면 새 버그가 발생할 수 있다.
  • 기존 코드를 수정하게 되면, 이전 버전을 사용하려고 할 때 문제가 발생한다.

기존 코드를 손대지 않고 새 기능을 추가하려면 Observer 패턴으로 설계하라!

결론!

  • 기존의 프로그래밍 방식은 특정 상태에서 수행하는 기능을 추가할 때 기존 클래스에 계속 코드를 추가해야 했다.
  • 기존 코드에 계속 새 코드를 추가하는 방식은 유지보수에 좋지 않다.
  • Observer 패턴을 적용하면 기존 클래스를 손대지 않고 특정 상태에서 수행하는 작업을 쉽게 추가할 수 있다.

3. Observer Pattern 적용 하기

Observer 패턴 적용 과정

1) publisher의 상태가 바뀔때마다 subscriber에 대해 호출할 메서드 규칙 정의

2) subscriber를 publisher에 등록하고 제거하는 메서드와 컬렉션 추가

3) publisher의 상태가 바뀌었을 때 subscriber에게 통지하는 코드 추가

1단계 : 호출 규칙을 정의한다.

  • 자동차 시동을 켜고 끌 때 호출될 규칙을 정의한다.
  • 인터페이스에 시동을 켤 때와 시동을 끌 때 호출할 메서드를 정의한다. 보통 메서드의 이름은 동사로 시작하는데, 옵저버에게 통지할 때 호출하는 메서드는 명사구의 상태 이름으로 정의할 수 있다.
public interface CarObserver {
	// 자동차 시동을 켤 때 호출될 메서드
	//  - 시동 걸 때 뭔가 작업하고 싶다면 이 메서드에 그 코드를 작성하면 된다.
  void carStarted();

  // 자동차 시동을 끌 때 호출될 메서드
	//  - 시동 끌 때 뭔가 작업하고 싶다면 이 메서드에 그 코드를 작성하면 된다.
  void carStopped();
}

2단계 : subscrber를 등록하고 제거하는 메서드와 컬렉션을 추가한다.

  • Car클래스(publisher)에 Car 클래스의 상태에 따라 각각의 메서드를 호출할 CarObserver 객체를 담을 Collection을 만든다.
  • 옵저버 객체를 Collection에 담을 addCarObserver()와 옵저버 객체를 Collection에서 삭제할 removeCarObserver() 메서드를 추가한다.
public class Car {
  //--------------------------------------------------------------
  // Observer 디자인 패턴 적용
  //  - Publisher 쪽에 추가해야 하는 필드와 메서드

  // 관찰자(Observer/Listener/Subscriber)의 객체 주소를 보관한다.
  List<CarObserver> observers = new ArrayList<>();

  // 자동차의 상태 변경을 보고 받을 관찰자를 등록한다.
  public void addCarObserver(CarObserver observer) {
    observers.add(observer);
  }

  // 자동차의 상태 변경을 보고 받는 관찰자를 제거한다.
  public void removeCarObserver(CarObserver observer) {
    observers.remove(observer);
  }
  //--------------------------------------------------------------
	
	public void start() {
    System.out.println("시동을 건다.");
  }

  public void run() {
    System.out.println("달린다.");
  }

  public void stop() {
    System.out.println("시동을 끈다.");
}

3단계 : publisher의 상태를 subscriber에게 통지할 코드를 추가한다.

public class Car {
  List<CarObserver> observers = new ArrayList<>();

  public void addCarObserver(CarObserver observer) {
    observers.add(observer);
  }

  public void removeCarObserver(CarObserver observer) {
    observers.remove(observer);
  }

  public void start() {
    System.out.println("시동을 건다.");
    //------------------------------------------------------------
    // Observer 디자인 패턴:
    //  - publisher의 상태가 바뀌었을 때 subscriber에게 통지한다.
    //  - 즉 subscriber(observer/listener)에 대해 규칙(CarObserver 인터페이스)에 따라 메서드를 호출한다.
    //    예) 자동차의 시동을 걸면, 등록된 관찰자들에게 알린다.
    for (CarObserver observer : observers) {
      observer.carStarted();
    }
    //------------------------------------------------------------
  }

  public void run() {
    System.out.println("달린다.");
  }

  public void stop() {
    System.out.println("시동을 끈다.");
    //------------------------------------------------------------
    // Observer 디자인 패턴:
    //  - publisher의 상태가 바뀌었을 때 subscriber에게 통지한다.
    //  - 즉 subscriber(observer/listener)에 대해 규칙(CarObserver 인터페이스)에 따라 메서드를 호출한다.
    //    예) 자동차의 시동을 끄면, 등록된 관찰자들에게 보고한다.
    for (CarObserver observer : observers) {
      observer.carStopped();
    }
    //------------------------------------------------------------
  }
}

4단계: 새로운 기능을 추가한다.

  • 프로젝트 완료한 다음 시간이 지난 후, 자동차의 시동을 걸 때 안전벨트 착용 여부를 검사하는 기능을 추가한다.

    (1) 자동차의 시동을 걸릴 때 보고를 받을 객체(SafeBeltCarObserver)를 준비한다.

    (2) 시동 걸 때 수행할 기능을 정의한다. 즉 carStarted() 메서드 정의한다.

    (3) Car 객체에 관찰자를 등록한다.

public class SafeBeltCarObserver implements CarObserver {
  @Override
  public void carStarted() {
    System.out.println("안전벨트 착용 여부 검사");
  }

  @Override
  public void carStopped() {}
}
public class Test01 {
  public static void main(String[] args) {

    Car car = new Car();

    // 새 기능이 들어있는 객체를 Car(publisher)에 등록한다.
    //  - Car 클래스를 손대지 않고 새 기능을 추가하는 방법이다.
    //  - 이것이 Observer 패턴으로 구조와시킨 이유이다.
    car.addCarObserver(new SafeBeltCarObserver());
    // Car 객체의 상태가 바뀔 때 실행될 코드를 별도의 클래스로 정의한 다음,
    // Car 객체에 등록한다.

    car.start();
    // Car 객체는 start()가 호출되면
    // 등록된 모든 subscriber(observer/listener)에게 통지(메서드 호출)한다.

    car.run();
    car.stop();
  }
  • 업그레이드를 수행한 다음 시간이 지난 후 자동차 시동 걸 때 엔진 오일 검사 기능을 추가한다.

    (1) 엔진오일 검사하는 옵저버(EngineOilCarObserver)를 정의한다.

    (2) Car 객체에 등록한다.

public class EngineOilCarObserver implements CarObserver {
  @Override
  public void carStarted() {
    System.out.println("엔진 오일 유무 검사");
  }

  @Override
  public void carStopped() {}
}
public class Test01 {
  public static void main(String[] args) {
    Car car = new Car();

    car.addCarObserver(new SafeBeltCarObserver());

    // 엔진 오일을 검사할 옵저버를 등록한다.
    car.addCarObserver(new EngineOilCarObserver());

    car.start();
    car.run();
    car.stop();
  }
}
  • Observer Pattern을 적용하면 다양한 기능을 추가하거나 삭제할 수 있다.

    (1) 새로운 기능을 수행할 클래스를 정의한다.

public class BrakeOilCarObserver implements CarObserver {
  @Override
  public void carStarted() {
    System.out.println("브레이크 오일 유무 검사");
  }

  @Override
  public void carStopped() {}
}
public class LightOffCarObserver implements CarObserver {
  @Override
  public void carStarted() {}

  @Override
  public void carStopped() {
    System.out.println("전조등을 끈다.");
  }
}
public class SunRoofCloseCarObserver implements CarObserver {
  @Override
  public void carStarted() {}

  @Override
  public void carStopped() {
    System.out.println("썬루프를 닫는다.");
  }
}

(2) 새 기능을 수행하는 옵저버를 추가한다.

public class Test01 {
  public static void main(String[] args) {
    Car car = new Car();

    car.addCarObserver(new SafeBeltCarObserver());
    car.addCarObserver(new EngineOilCarObserver());
    car.addCarObserver(new BrakeOilCarObserver());
    car.addCarObserver(new LightOffCarObserver());
    car.addCarObserver(new SunRoofCloseCarObserver());

    car.start();
    car.run();
    car.stop();
  }
}

5단계 : 리팩토링

  • 옵저버에 통지하는 코드를 별도의 메서드로 분리하여 유지보수 하기 좋게 만든다.
public class Car {

  List<CarObserver> observers = new ArrayList<>();

  public void addCarObserver(CarObserver observer) {
    observers.add(observer);
  }

  public void removeCarObserver(CarObserver observer) {
    observers.remove(observer);
  }

  // 리팩토링: 메서드 추출(extract method)
  //  - 특정 기능을 수행하는 코드를 이해하기 쉽도록 외부 메서드로 추출하는 것
  private void notifyObserversOnStarted() {
    for (CarObserver observer : observers) {
      observer.carStarted();
    }
  }

  private void notifyObserversOnStopped() {
    for (CarObserver observer : observers) {
      observer.carStopped();
    }
  }

  public void start() {
    System.out.println("시동을 건다.");

    notifyObserversOnStarted();
  }

  public void run() {
    System.out.println("달린다.");
  }

  public void stop() {
    System.out.println("시동을 끈다.");

    notifyObserversOnStopped();
  }
}

6단계 : 추상 메서드 활용

  • CarObserver 구현체를 만들 때, 인터페이스에 선언된 모든 메서드를 구현해야 한다. 관심없는 메서드라도 구현해야 한다.
  • 예)
    • SafeBeltCarObserver는 시동 걸 때 작업을 수행한다. 그래서 carStarted() 메서드에 코드를 삽입하였다.
    • 그런데 인터페이스를 구현하려면 모든 메서드를 정의해야 하기 때문에 관심이 없는데도 불구하고 carStopped() 메서드도 구현하였다. 물론 코드가 없는 빈 메서드이다.
  • 위와 같은 경우, (여러 개의 메서드 중에서 주로 일부 메서드만 구현하는 경우) 추상 클래스를 사용하여 메서드를 미리 구현해 놓으면 인터페이스 구현체를 정의하기 편하다!

  • 인터페이스
public interface CarObserver 
  void carStarted();
  void carStopped();
}
  • 추상 메서드
    • 인터페이스 구현체가 메서드를 정의하기 쉽도록 이 클래스에서 미리 모든(또는 일부) 메서드를 구현하였다. 이 클래스의 존재 이유는 인터페이스 구현체가 메서드를 정의하기 쉽도록 미리 구현된 메서드를 상속해주는 일을 한다.
    • 즉 이 클래스 자체를 사용하려는 것이 아니다. 추상 메서드가 없지만, 추상 클래스로 선언함으로써 개발자에게 이 클래스의 역할을 알리는 효과가 있다.
// 인터페이스를 구현한 추상 클래스는
// 보통 그 클래스 이름을 'Abstract-'로 시작한다.
public abstract class AbstractCarObserver implements CarObserver {

  @Override
  public void carStarted() {
    // 서브 클래스에게 구현된 메서드를 상속해주기 때문에 수퍼클래스에서 미리 구현한다.
    // 단, 아무런 코드를 넣지 않는다.
  }

  @Override
  public void carStopped() {
    // 서브 클래스에게 구현된 메서드를 상속해주기 때문에 수퍼클래스에서 미리 구현한다.
    // 단, 아무런 코드를 넣지 않는다.
  }
}
  • 각 기능을 수행하는 Observer를 추상 클래스를 상속받도록 수정한다.
    • 이전 버전에서는 인터페이스를 직접 구현했지만, 이번 버전에서는 추상 클래스를 상속 받아 간접적으로 구현한다.
    • 수퍼 클래스(추상 클래스)에서 모든 메서드를 정의했기 때문에 굳이 정의하지 않아도 되는 메서드는 정의하지 않는다.
public class SafeBeltCarObserver extends AbstractCarObserver {
  @Override
  public void carStarted() {
    System.out.println("안전벨트 착용 여부 검사");
  }

	// carStopped()는 정의하지 않는다.
}
public class EngineOilCarObserver extends AbstractCarObserver {
  @Override
  public void carStarted() {
    System.out.println("엔진 오일 유무 검사");
  }
}
public class BrakeOilCarObserver extends AbstractCarObserver {
  @Override
  public void carStarted() {
    System.out.println("브레이크 오일 유무 검사");
  }
}
public class LightOffCarObserver extends AbstractCarObserver {
  @Override
  public void carStopped() {
    System.out.println("전조등을 끈다.");
  }
}
public class SunRoofCloseCarObserver extends AbstractCarObserver {
  @Override
  public void carStopped() {
    System.out.println("썬루프를 닫는다.");
  }
}