Posts 학원 #55일차: Observer 패턴
Post
Cancel

학원 #55일차: Observer 패턴

Observer 디자인 패턴

객체의 상태 변화에 따라 특정 작업을 수행하고 싶을 때 사용하는 패턴이다. 객체의 상태 변화를 관찰하는 observer(listener, subscriber)들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목목을 옵저버에게 통지하도록 하는 디자인 패턴이다. publish-subscribe 패턴이라고 하기도 한다. Observer(관찰자)라고는 하지만, 실제로 관찰할 방법이 없으니, Observer 객체를 가지고 있다가 그 객체의 메서드 호출을 통해 통지를 한다. 따라서 관찰자라기보다는 Listener라는 말이 더 적합하다고 볼 수도 있다.

image

실무에서는 위와 같은 다이어그램은 사용하지 않는다. 리스너 구현체를 사용한다기보다 리스너 인터페이스(호출 규칙)를 사용하는 것처럼 다음과 같이 그림을 그린다.

image

메일링 리스트도 옵저버 디자인 패턴의 실생활의 예이다. 메일링 리스트에 자신의 메일을 등록해 놓으면 새 메일을 등록할 때마다 자신에게 통지가 된다.

문제상황: 기능 추가 시 기존 코드 변경

image

1
2
3
4
5
6
7
public class Test01 {
  Car car = new Car();
  
  car.start();
  car.run();
  car.stop();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Car {
  public void start() {
    System.out.println("시동을 건다.");
  }

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

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

위와 같이 프로젝트를 만들어 배포했다고 하자. 여기서 기능을 추가하기 위해서는 어떻게 해야 할까?

프로젝트 완료한 다음 시간이 지난 후, 자동차의 시동을 걸 때 안전벨트 착용 여부를 검사하는 기능을 추가하기 위해, 기존 Car 클래스의 start() 메서드에 코드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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월 15일 - 자동차 시동을 끌 때 전조등 자동 끄기 기능을 추가
    System.out.println("전조등을 끈다.");
    
    // 예) 5월 5일 - 자동차 시동을 끌 때 썬루프 자동 닫기 기능을 추가
    System.out.println("썬루프를 닫는다.");
  }
}

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

    • 어떤 고객은 해당 기능이 필요 없을 수 있다. 이런 경우 조건문을 추가하여 기능의 동작 여부를 제어해야 한다. 따라서 코드가 복잡해진다. (즉 유지보수가 힘들다.)
    • 이미 디버깅과 테스트가 완료된 기존 코드를 변경하면 새 버그가 발생할 수 있다.
  • 어떻게 해결할까?

    • 기존 코드를 손대지 않거나 최소한으로 손대는 것이 좋다.

    • 기존 코드를 손대지 않고 새 기능을 추가하는 방법 중에 하나가 Observer 패턴으로 설계하는 것이다.

    • 즉 여러 방법 중에 하나다. 상속도 그 방법 중에 하나인데 이번에는 Observer 패턴을 공부할 것이다.

기능 추가: Observer 패턴

image

image

  • publisher의 상태가 바뀔 때마다 호출할 subscriber에 대해 메서드 규칙을 정의한다.
  • 보통 메서드의 이름은 동사로 시작하지만 Subcriber에게 통지할 때 호출하는 메서드명사구로 지어 특정 상태를 의미함을 강조한다.

WindowListener: windowClosing(), windowDeactivated(), winowIconified

1
2
3
4
public interface CarObserver {
  void carStarted();
  void carStopped();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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("시동을 건다.");
    //------------------------------------------------------
    // Observer 디자인 패턴:
    // - publisher의 상태가 바뀌었을 때 subscriber에게 통지한다.
    // - 즉 subscriber(observer/listener)에 대해 규칙에 따라 메서드를 호출한다.
    // 예) 자동차의 시동을 걸면, 등록된 관찰자들에게 알린다.
    for (CarObserver observer : observers) {
      observer.carstarted();
    }
  }
  
  public void run() {
    System.out.println("달린다.");
  }
  
  public void stop() {
    System.out.println("시동을 끈다.");
    
    //------------------------------------------------------
    // Observer 디자인 패턴:
    // - publisher의 상태가 바뀌었을 때 subscriber에게 통지한다.
    // 예) 자동차가 멈췄을 때, 등록된 관찰자들에게 알린다.
    for (CarObserver observer : observers) {
      observer.carStopped();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test01 {
  public static void main(String[] args) {
    Car car = new Car();
    
    // 새 기능이 들어 잇는 객체를 Car(publisher)에 등록한다.
    // - Car 클래스를 손대지 않고 새 기능을 추가하는 방법이다.
    // - 이것이 Observer 패턴으로 구조화시킨 이유이다.
    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();
  }
}
1
2
3
4
5
public class SafeBeltCarObserver implements CarObserver {
  public void carStarted() {
    System.out.println("안전벨트 작용 여부 검사");
  }
}

실행결과

1
2
3
4
5
6
7
8
시동을 건다.
안전벨트 착용 여부 검사
엔진 오일 유무 검사
브레이크 오일 유무 검사
달린다.
시동을 끈다.
전조등을 끈다.
썬루프를 닫는다.

리팩토링: 옵저버에게 통지하는 코드 메서드 추출

등록된 관찰자들에게 알리는 코드를 메서드 추출한다. 비록 짧은 코드일지라도 이렇게 추출하면 코드를 더 이해하기 쉽다. 코드에 주석을 달았다는 것은 코드만으로 이해하기 힘들다, 즉 리팩토링이 필요하다는 뜻이다. 코드만으로 이해할 수 있게 만드는 것이 리팩토링이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Car {
  List<CarObserver> observers = new ArrayList<>();
  
  public void addCarObserver(CarObserver observer) {
    observers.add(observer);
  }
  
  public void removeCarObserver(CarObserver observer) {
    observers.add(observer);
  }
  
  // 리팩토링: 메서드 추출 (extract method)
  // - 특정 기능을 수행하는 코드를 이해하기 쉽도록 외부 메서드로 추출하는 것
  private void notifyObserverOnStart() {
    for (CarObserver observer : observers) {
      observer.carStarted();
    }
  }
  
  private void notifyObserverOnStop() {
    for (CarObserver observer : observers) {
      observer.carStopped();
    }
  }
  
  public void start() {
    System.out.println("시동을 건다.");
    notifyCarObserverOnStart();
  }
  
  public void run() {
    System.out.println("달린다.");
  }
  
  public void stop() {
    System.out.println("시동을 끈다.");
    notifyCarObserverOnStop();
  }
}
1
2
3
4
5
6
public class SafeBeltCarObserver implements CarObserver {
  public void carStarted() {
    System.out.println("안전벨트 작용 여부 검사");
  }
  public void carStopped() {}
}

추상 클래스 문법의 활용

image

인터페이스를 바로 구현할 때의 문제점

1
2
3
4
5
6
public class SafeBeltCarObserver implements CarObserver {
  public void carStarted() {
    System.out.println("안전벨트 작용 여부 검사");
  }
  public void carStopped() {}
}

위와 같이 관심 있는 메서드는 carStarted() 메서드 하나뿐이지만, 인터페이스를 구현할 때는 인터페이스에 정의된 메서드를 전부 구현해야 한다. 이때 추상 클래스 문법을 사용할 수 있다. 추상 클래스가 인터페이스를 구현하도록 하고, 추상 클래스를 상속받으면, 이제 concrete 클래스는 관심 있는 메서드만 오버라이딩할 수 있다.

왜 인터페이스에서 default 메서드를 사용하지 않고 추상 클래스를 따로 만드는 것일까? 왜냐하면 이 상황에서 인터페이스에서 default를 사용하는 것은 바람직하지 않기 때문이다. default 메서드는 정말 어쩔 수 없을 때, 기능을 추가하는데 기존 코드에 영향을 쓰지 않기 위해서 쓴다. 인터페이스는 인터페이스 답게, 추상 클래스는 추상 클래스 답게 사용하자.

왜 일반 클래스가 아니라 추상 클래스로 선언할까? 이 클래스는 CarObserver 구현체를 쉽게 만들 수 있도록 서브 클래스에 인터페이스의 메서드를 구현하여 상속해주는 역할만 하기 때문이다.

서브 클래스가 인터페이스에 정의된 메서드 중에서 원하는 메서드만 오버라이딩할 수 있도록 수퍼 클래스에서 미리 구현한다. 단, 아무런 코드를 넣지 않는다. 인터페이스를 구현한 추상 클래스는 보통 그 클래스 이름을 Abstract구현한인터페이스명으로 하는 것이 관례이다. 이러한 관례는 다른 개발자가 그 용도를 빨리 파악하게 만들어준다.

1
2
3
4
5
6
7
public abstract class AbstractCarObserver implements CarObserver {
  @Override
  public void carStarted() {}

  @Override
  public void carStopped() {}
}

인터페이스를 직접 구현하는 것보다 추상클래스를 상속받아서 구현하는 것이 훨씬 간편하다. 관심 있는 메서드만 오버라이딩하면 되기 때문이다.

1
2
3
4
5
public class SafeBeltCarObserver extends AbstractCarObserver {
  public void carStarted() {
    System.out.println("안전벨트 작용 여부 검사");
  }
}

따라서 우리는 추상 클래스가 서브 클래스가 좀 더 인터페이스를 쉽게 구현할 수 있도록 할 수 있다는 것을 알 수 있다.

pms v.33: Observer 패턴 적용

image

Observer 디자인 패턴

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

단계

  • 인터페이스를 활용하여 옵저버 호출 규칙을 정의한다.
  • 옵저버 구현체를 등록하고 제거하는 메서드와 컬렉션을 추가한다.
  • 특정 상태가 되면 옵저버에게 통지하게 된다.

실습

1단계: App 클래스의 스태틱 멤버를 인스턴스 멤버로 전환한다.

1
2
3
4
5
6
7
8
9
10
public class App {
  public static void main(String[] args) {
    
  }
  
  // 인스턴스 멤버
  public void service() throws Exception {
    List<Board>
  }
}

지금 당장은 사용하지 객체를 많이 만들지 않더라도 확장성을 위해서 인스턴스 멤버로 전환한다.

2단계: 애플리케이션을 시작하거나 종료할 때 실행할 옵저버의 메서드 호출 규칙을 적용한다.

ApplicationContextListener 인터페이스 생성

  • Observer가 갖춰야 할 규칙을 정의한다.
  • 애플리케이션이 시작할 때 자동으로 호출할 메서드의 규칙을 정의한다.

애플리케이션 상태가 변경되었을 때(이벤트가 일어났을 때) 호출할 메서드 규칙을 정의한다. 즉 애플리케이션 상태에 대해 보고를 받을 Observer 규칙을 정의한다. 보통 Observer를 Listener, Subscriber라고 한다.

여러 프로젝트에 사용될 수 있는 것이라면 프로젝트 패키지 아래에 두지 말고, 회사 도메인 아래에 두어야 한다.

1
2
3
4
5
6
7
8
9
package com.eomcs.context;

public interface ApplicationContextListener {
  // publisher(애플리케이션)가 애플리케이션 시작을 알리기 위해 호출하는 메서드
  void contextInitialized();
  
  // publisher(애플리케이션)가 애플리케이션 종료를 알리기 위해 호출하는 메서드
  void contextDestroyed();
}

3단계: 옵저버를 저장할 컬렉션 객체와 옵저버를 추가하고 제거하는 메서드를 추가한다.

  • App.java
    • 옵저버를 보관할 컬렉션 객체를 추가한다.
    • 옵저버를 등록하는 메서드(addApplicationContextListener())를 추가한다.
    • 옵저버를 제거하는 메서드(removeApplicationContextListener())를 추가한다.

실무에서는 옵저버라는 용어보다는 리스너라는 용어를 많이 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
  // 옵저버를 보관할 컬렉션 객체 준비
  List<ApplicationContextListener> listener = new ArrayList<>();
  // 옵저버를 등록하는 메서드
  public void addApplicationContextListener (ApplicationContextListener listener) {
    listeners.add(listener);
  }
  
  public void removeApplicationContextListener (ApplicationContextListener listener) {
    listeners.remove(listener);
  }
}

4단계: 애플리케이션의 service() 실행전/후에 옵저버에게 통지하는 코드를 추가한다.

  • 옵저버를 호출하는 메서드를 정의한다.
  • notifyApplicationContextListenerOnServiceStart()
  • notifyApplicationContextListenerOnServiceClose()

메서드를 호출한다는 것은 통지한다, 알린다는 의미이기도 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class App {
  // 옵저버를 보관할 컬렉션 객체 준비
  List<ApplicationContextListener> listener = new ArrayList<>();
  // 옵저버를 등록하는 메서드
  public void addApplicationContextListener (ApplicationContextListener listener) {
    listeners.add(listener);
  }
  
  // 옵저버를 제거하는 메서드
  public void removeApplicationContextListener (ApplicationContextListener listener) {
    listeners.remove(listener);
  }
  
  // service() 실행 전에 옵저버에게 통지한다.
  public void notifyApplicationContextListenerOnServiceStarted() {
    for (ApplicationContextListener listener : listeners) {
      // 곧 서비스를 시작할 테니 준비하라고,
      // 서비스 시작에 관심있는 각 옵저버에게 통지한다.
      listener.contextInitialized();
    }
  }
  
  // service() 실행 후에 옵저버에게 통지한다.
  private void notifyApplicationContextListenerOnServiceStopped() {
    for (ApplcationContextListener listener : listeners) {
      // 서비스가 종료되었으니 마무리 작업하라고,
      // 마무리 작업에 관심 있는 각 옵저버에게 통지한다.
      listener.contextDestroyed();
    }
  }
}

service() 메서드의 시작/종료 부분에 옵저버를 호출하는 코드를 추가한다.

1
2
3
4
5
6
7
8
9
public void service() throws Exception {
  // 옵저버에게 통지한다.
  notifyApplicationContextListenerOnServiceStarted();
  
  //.......
  
  // 옵저버에게 통지한다.
  notifyApplicationContextListenerOnServiceStopped();
}

5단계: 애플리케이션을 시작하고 종료할 때 간단한 안내 메시지를 출력하는 옵저버를 추가한다.

옵저버 디자인패턴을 적용하고 그 사용법을 테스트한다.

  • AppInitListener 생성
    • ApplicationContextListener를 구현한다.
    • 애플리케이션을 시작할 때 다음과 같이 간단한 안내 메시지를 출력한다.
      • 프로젝트 관리 시스템(PMS)에 오신 것을 환영합니다!
    • 애플리케이션을 종료할 때 다음과 같이 간단한 안내 메시지를 출력한다.
      • 프로젝트 관리 시스템(PMS)를 종료합니다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.eomcs.pms.listener;

import com.eomcs.context.ApplicationContextListener;

public class AppInitListener implements ApplicationContextListener {
  @Override
  public void contextInitialized() {
    System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
  }
  
  @Override
  public void contextDestroyed() {
    System.out.println("프로젝트 관리 시스템(PMS)을 종료합니다!");
  }
}
This post is licensed under CC BY 4.0 by the author.

:book: 리팩토링 #4장: 테스트 작성

학원 #56일차: PMS 프로젝트: Observer 패턴의 활용

Loading comments from Disqus ...