옵저버 패턴
옵져버 패턴은 신문이나 잡지처럼 데이터(목적)을 데이터가 필요한 쪽(구독자)에게 발행해주는 형식의 디자인을 말한다.
이름은 이와 맞지 않게 Observer(관찰자) 라고 붙여 있는데, 데이터를 필요로 하는 쪽 즉, 구독자가 데이터가 변화할때나 혹은 주기적으로 데이터의 상태를 전달 받음으로서 ‘관찰’하는 형식이 되기 때문이다.
따라서 이 패턴의 출발 과제는 데이터를 조회 하는 client객체와 데이터를 제공하는 객체간의 관계를 어떻게 디자인해야 하는지에 대한 고민이었다는 것을 알 수 있다.
버스 정류장의 곧 도착하는 버스알림 전광판을 예를 들어 보자.
각 버스 정류장에서는 곧 도착할 버스 리스트가 변경 될 때 마다 전광판에 표시를 해줘야 한다. 전국의 버스 위치의 정보를 관리하는’ 버스 위치 시스템’이 있는데, 각 버스 정류장에서는 곧 도착할 버스 리스트를 이 ‘버스 위치 시스템’를 통해서 곧 도착할 버스들의 리스트 정보를 얻어 올 수 있다.
버스 리스트가 언제 갱신 될지 모르므로 각 정류장에서 주기적으로 데이터를 조회한다고 하면, 데이터가 변경되지 않은 상황에서도 불필요하게 데이터를 조회하는 비용이 발생하게 되어 매우 비효율 적일 것이다.
거꾸로 ‘버스 위치 시스템’에서 버스 위치 정보가 변화할때마다 각 정류장으로 데이터를 전송해 준다면, 불필요한 비용을 막을 수 있을 것이다.
요즘 관심이 높아지고 있는 비동기 프로그래밍이나 spring-cloud진영의 유레카 같은 기능도 옵져버 패턴을 기반으로 하고 있다.
아까 옵져버 패턴은 신문이나 잡지와 같은 ‘구독’ 의 형식을 취하고 있다고 했다.
‘신문 구독’의 특징을 생각해 보자.
- 신문사가 사업을 시작하고 신문을 찍어낸다. (publish)
- 독자(describer)가 특정 신문사에 구독 신청을 하면 매번 새로운 신문을 배달 받는다.
- 신문을 더이상 받아보고 싶지 않으면 구독 해지 신청을 한다.
- 신문사가 계속 영업을 하는 이상 꾸준히 구독 및 해지를 하게 된다.
이와 같은 특성을 객체간의 관계에 적용이 되면 아래와 같다.
옵저버 패턴의 정의 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 가지게 된다.
옵져버패턴을 적용한 객체관계 :
[옵져버 패턴 - 클래스 다이어그램]
디자인 원칙
- 헐리우드 원칙 : 서로 상호작용을 하는 개체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
옵져버 패턴을 이용하면 정보를 제공하는 객체(subject)와 정보를 조회하는 객체(observer)간의 결합을 느슨하게 해준다.
- subject는 옵저버에 대해서 특정 인터페이스(Observer인터페이스)를 구현한다는 것만 알뿐 개별 옵저버 객체에 대해서는 알 필요가 없다.
- 옵저버는 언제든지 새로 추가 할 수 있다. (삭제되고 추가 되어도 subject의 코드가 변화할 될 필요가 없다)
- 새로운 형식의 옵저버를 추가하려고 할 때도 subject를 전혀 변경할 필요가 없다.
- subject와 옵저버가 바뀌더라도 서로에게 영향을 미치지 않으며 서로 독립적으로 재사용할 수 있다.
코드로 만들어 보자
설정 : 기상청 미세먼지 측정 시스템에서는 미세먼지 농도 지수가 위험수치 이상이 되면 앱푸시, SMS 전송으로 경고 메세지를 보낸다. 전체 소스 참고
- subject(발행자) interface와 observer(구독자) interface를 만듬
public interface Publisher { void registerDescriber(); void removeDescriber(); void notifyDescribers(); } public interface Describer { void alert(); //미세먼지 농도가 위험수준이라고 알리는 함수 }
- publisher에 구독자를 등록 및 해지하려면 해당 객체를 넘겨받아야 함으로 등록, 해지 함수에 파라미터 추가
public interface Publisher { void registerDescriber(Describer db); void removeDescriber(Describer db); void notifyDescribers(); }
- pulisher와 Describer를 구현하는 각 객체를 만듬
//미세먼지를 위험성을 알리는 객체 public class FineDustData implements Publisher{ @Override public void registerDescriber(Describer db) { } @Override public void removeDescriber(Describer db) { } @Override public void notifyDescribers() { } } //앱푸시를 보내는 객체 public class AppPush implements Describer { @Override public void alert(int level) { } } //sms전송을 위한 객체 public class SMS implements Describer { @Override public void alert(int level) { } }
- FineDustData에 구독객체 리스트와 현재 미세먼지 농도수치, 위험수준의 농도수치를 저장할 변수를 선언 하였다.
그리고 객체를 생성할때 각 변수의 값이 초기화 되도록 처리.
public class FineDustData implements Publisher{ List<Describer> describers; int currentLevel; int dangersLevel; public FineDustData(int dangersLevel) { this.dangersLevel = dangersLevel; this.describers = new ArrayList<>(); } ..(생략).. }
- 구독자를 등록하고 해지하는 함수 구현
public class FineDustData implements Publisher { ..(생략).. @Override public void registerDescriber(Describer dc) { describers.add(dc); } @Override public void removeDescriber(Describer dc) { describers.remove(dc); } }
- 미세먼지가 위험수치에 다다를때마다 구독자에게 알리는 함수 구현
public class FineDustData implements Publisher { ...(생략).. @Override public void notifyDescribers() { //구독자 리스트를 돌면서 알림 메소드를 호출한다. for (Describer dc : describers) { dc.alert(); } } //현재 미세먼지 수치를 변경하는 함수 public void changeDustLevel(int level) { this.currentLevel = level; //위험수치에 다다르면 구독자에체 알리는 함수를 호출한다. if(currentLevel >= dangersLevel) { notifyDescribers(); } } }
- 각 구독객체인 AppPush와 SMS는 미세먼지 농도가 위험 수치라는 것을 알린다!
public class AppPush implements Describer { @Override public void alert() { System.out.println("앱푸시! - 미세먼지가 위험수준에 도달하였습니다."); } } public class SMS implements Describer { @Override public void alert() { System.out.println("SMS발송! - 미세먼지가 위험수준에 도달하였습니다.\n 외출하실때 마스크 착용하세요!!"); } }
- 각 Describer이 Publisher에게 구독을 요청을 하기 위해 초기화 함수에서 publisher를 넘겨 받을 수 있도록 함.
추가로 구독 해지 함수를 Describer insertface에 추가하고 각각의 구현 객체에 추가해줌.
public interface Describer { void alert(); void stopDescribe(); } public class AppPush implements Describer { Publisher pb; public AppPush(Publisher pb) { pb.registerDescriber(this); } @Override public void alert() { System.out.println("앱푸시! - 미세먼지가 위험수준에 도달하였습니다."); } @Override public void stopDescribe(){ pb.removeDescriber(this); } } public class SMS implements Describer { Publisher pb; public SMS(Publisher pb) { this.pb = pb; pb.registerDescriber(this); } @Override public void alert() { System.out.println("SMS발송! - 미세먼지가 위험수준에 도달하였습니다.\n 외출하실때 마스크 착용하세요!!"); } @Override public void stopDescribe() { pb.removeDescriber(this); } }
- 테스트
public class TestFineDustObserver { @Test public void test() { //publisher정의 FineDustData dustData = new FineDustData(10); //앱푸시 구독자 AppPush appPush = new AppPush(dustData); //SMS 구독자 SMS sms = new SMS(dustData); //미세먼지 농도가 올라간다. dustData.changeDustLevel(5); //..점점 올라가더니 위험수치에 다다랐다. dustData.changeDustLevel(10); } }
처리결과
앱푸시! - 미세먼지가 위험수준에 도달하였습니다.
SMS발송! - 미세먼지가 위험수준에 도달하였습니다.
외출하실때 마스크 착용하세요!!