|
| 1 | +# 옵저버(Observer) 패턴 |
| 2 | + |
| 3 | +## 옵저버 패턴이란.. |
| 4 | +한 객체의 상태 변화를 정해지지 않은 여러 다른 객체에 통지하고 싶을 때 사용되는 패턴 |
| 5 | + |
| 6 | +## 옵저버 패턴의 구조 |
| 7 | +옵저버 패턴은 크게 주제(subject) 객체, 옵저버(observer) 객체로 나뉜다. <br><br> |
| 8 | + |
| 9 | +주제 객체는 아래와 같은 두 가지 책임을 갖는다.<br> |
| 10 | +<ul> |
| 11 | +<li> 옵저버 목록을 관리하고, 옵저버를 등록하고 제거할 수 있는 메서드를 제공한다. </li> |
| 12 | +<li> 상태의 변경이 발생하면 등록된 옵저버에 변경 낸역을 알린다. </li> |
| 13 | +</ul> |
| 14 | + |
| 15 | +> 예를 들자면 아래와 같은 구조를 띄고있다! |
| 16 | +
|
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +StatusSubject 클래스는 아래와 같이 옵저버 목록을 List와 같은 타입으로 보관할 수 있다.<br> |
| 21 | + |
| 22 | +### StatusSubject (추상)클래스 |
| 23 | +~~~ |
| 24 | +public abstract class StatusSubject { |
| 25 | + private List<StatusObserver> observers = new ArrayList<StatusObserver>(); |
| 26 | + |
| 27 | + public void add(StatusObserver observer) { |
| 28 | + observers.add(observer); |
| 29 | + } |
| 30 | + |
| 31 | + public void remove(StatusObserver observer) { |
| 32 | + observers.remove(observer); |
| 33 | + } |
| 34 | + |
| 35 | + public void notifyStatus(Status status) { |
| 36 | + for (StatusObserver observer : observers) |
| 37 | + observer.onAbnormalStatus(status); |
| 38 | + } |
| 39 | +} |
| 40 | +~~~ |
| 41 | +<br> |
| 42 | + |
| 43 | +notifyStatus() 메서드는 observers List에 등록된 각 StatusObserver 객체의 onAbnormalStatus() 메서드를 호출하는데,<br> |
| 44 | +이렇게 옵저버 객체의 메서드를 호출하는 방식으로 상태에 변화가 생겼음을 옵저버 객체에게 알린다. <br> |
| 45 | + |
| 46 | +Status의 상태 변경을 알려야 하는 StatusChecker 클래스는 아래와 같이 StatusSubject 클래스를 상속받아 구현한다. |
| 47 | + |
| 48 | +### StatusChecker 클래스 |
| 49 | +~~~ |
| 50 | +public class StatusChecker extends StatusSubject { |
| 51 | + |
| 52 | + public void check() { |
| 53 | + Status status = loadStatus(); |
| 54 | + |
| 55 | + if (status.isNotNormal()) |
| 56 | + super.notifyStatus(status); |
| 57 | + } |
| 58 | + |
| 59 | + private Status loadStatus() { |
| 60 | + .... |
| 61 | + } |
| 62 | +} |
| 63 | +~~~ |
| 64 | +<br> |
| 65 | +StatusChecker 클래스는 비정상 상태가 감지되면 상위 클래스의 notifyStatus() 메서드를 호출해서 등록된 옵저버 객체들에 상태 값을 전달한다. <br> |
| 66 | +옵저버 객체를 구현한 클래스는 주제 객체가 호출하는 메서드에서 필요한 기능을 구현하면된다. 앞의 예제의 경우, StatusSubject 타입 객체에 등록되는 옵저버 인터페이스인 <br> |
| 67 | +StatusObserver는 주제 객체로부터 상태 변화를 전달 받을 수 있는 메서드 onAbnormalStatus() 메서드를 정의하고 있다. <br> |
| 68 | +옵저버 구현 클래스인 StatusEmailSender 클래스는 StatusObserver 인터페이스를 상속받아 상태 변화를 통지 받을 때 수행해야 할 기능을 구현하게 된다. <br> |
| 69 | + |
| 70 | +### StatusObserver |
| 71 | +~~~ |
| 72 | +public interface StatusObserver { |
| 73 | + void onAbnormalStatus(Status status); |
| 74 | +} |
| 75 | +~~~ |
| 76 | + |
| 77 | +### StatusEmailSender |
| 78 | +~~~ |
| 79 | +public class StatusEmailSender implements StatusObserver { |
| 80 | + |
| 81 | + @Override |
| 82 | + public void onAbnormalStatus(Status status) { |
| 83 | + sendEmail(status); |
| 84 | + } |
| 85 | + |
| 86 | + private void sendEmail(Status status) { |
| 87 | + ... // 이메일 전송 코드 |
| 88 | + } |
| 89 | +} |
| 90 | +~~~ |
| 91 | +<br> |
| 92 | +주제 객체의 상태에 변화가 생길 때 그 내용을 통지받도록 하려면, 옵저버 객체를 주제 객체에 등록해 주어야 한다. <br><br> |
| 93 | + |
| 94 | +예를 들어, 시스템의 상태가 비정상이 될 때 StatusChecker 객체가 StatusEmailSender 객체에 통지하게 하려면 다음 코드처럼 StatusEmailSender 객체를 <br> |
| 95 | +StatusChecker 객체에 옵저버로 등록해 주어야 한다. <br><br> |
| 96 | + |
| 97 | +~~~ |
| 98 | +StatusChecker checker = new StatusChecker(); |
| 99 | +checker.add(new StatusEmailSender()); // 옵저버로 등록 |
| 100 | +~~~ |
| 101 | +<br> |
| 102 | + |
| 103 | +위와같이 옵저버로 등록되면, 시스템이 비정상 상태가 될 때마다 StatusChecker 객체가 StatusEmailSender 객체의 onAbnormalStatus() 메서드를 <br> |
| 104 | +호출해서 상태 정보를 통지해준다. 따라서 StatusEmailSender 객체는 시스템이 비정상 상태가 될 때 담당자에게 이메일로 통보해 줄 수 있게 된다. <br><br> |
| 105 | + |
| 106 | +옵저버 패턴을 적용할 때의 장점은 주제 클래스 변경 없이 상태 변경을 통지 받을 옵저버를 추가할 수 있다는 점이다. <br> |
| 107 | +예를 들어, 장애가 발생할 때 SMS를 이용해서 문자를 전송한다면, 해당 기능을 구현한 옵저버 객체를 StatusChecker 객체에 등록해 주기만 하면 된다. <br> |
| 108 | + |
| 109 | +~~~ |
| 110 | +StatusChecker checker = ...; |
| 111 | +
|
| 112 | +//새로운 타입의 옵저버가 추가되어도 StatusChecker 코드는 바뀌지 않는다. |
| 113 | +StatusObserver faultObserver = new FaultStatusSMSSender(); |
| 114 | +checker.add(faultObserver); |
| 115 | +checker.add(new StatusEmailSender()); |
| 116 | +~~~ |
| 117 | + |
| 118 | +## 옵저버 객체에게 상태 전달 방법 |
| 119 | +옵저버 객체가 기능을 수행하기 위해 주제 객체의 상태가 필요할 수 있다. <br> |
| 120 | +예를 들어, FaultStatusSMSSender 클래스는 장애 상태인 경우에만 SMS를 전송하고, 응답 속도가 느려진 상태처럼 장애 이외의 비정상 상태인 경우에는 <br> |
| 121 | +메시지를 전송하지 않도록 구현할 수 있을 것이다. 이 경우 FaultStatusSMSSender 클래스는 상태 값을 확인해야 한다. 지금까지 작성한 예에서는 아래 코드에서 보듯이 <br> |
| 122 | +주제 객체에서 옵저버 객체에 상태 값을 전달했다. 위 코드에서 FaultStatusSMSSender 클래스는 onAbnormalStatus() 메서드를 통해서 전달받은 status 객체만으로<br> |
| 123 | +원하는 기능을 구현하는데 부족함이 없다. 하지만, 경우에 따라서 옵저버 객체의 메서드를 호출할 때 전달한 객체만으로는 옵저버의 기능을 구현할 수 없을 수도 있다. <br><br> |
| 124 | + |
| 125 | +이런 경우에는 옵저버 객체에서 콘크리트 주제 객체에 직접 접근하는 방법을 사용하기도 한다. 아래 코드는 옵저버 객체에서 특정 타입의 주제 객체를 사용하는 코드의 예를 보여준다. |
| 126 | + |
| 127 | +~~~ |
| 128 | +public class SpecialStatusObserver implements StatusObserver { |
| 129 | + private StatusChecker statusChecker; |
| 130 | + private Siren siren; |
| 131 | + |
| 132 | + public SpecialStatusObserver(StatusChecker statusChecker) { |
| 133 | + this.statusChecker = statusChecker; |
| 134 | + } |
| 135 | + |
| 136 | + public void onAbnormalStatus(Status status) { |
| 137 | + // 특정 타입의 주제 객체에 접근 |
| 138 | + if (status.isFault() && statusChecker.isContinuousFault()) |
| 139 | + siren.begin(); |
| 140 | + } |
| 141 | +} |
| 142 | +~~~ |
| 143 | +<br> |
| 144 | +SpecialStatusObserver 클래스의 onAbnormalStatus() 메서드는 status 파라미터와 statusChecker 필드를 이용해서 사이렌의 실행 조건을 판단하고 있다. <br><br> |
| 145 | + |
| 146 | +이 코드를 보면 SpecialStatusObserver 클래스에서 StatusChecker 클래스로의 의존이 발생하게 되는데, 이렇게 콘크리트 옵저버 클래스(SpecialStatusObserver)는 <br> |
| 147 | +필요에 따라 특정한 콘크리트 주제 클래스(StatusChecker)에 의존하게 된다. <br> |
| 148 | + |
| 149 | + |
| 150 | + |
| 151 | +## 옵저버에서 주제 객체 구분 |
| 152 | +옵저버 패턴이 가장 많이 사용되는 영역을 꼽으라면 GUI 프로그래밍 영역일 것이다. 버튼이 눌릴 때 로그인 기능을 호출한다고 할 때, 버튼이 주제 객체가 되고 로그인 <br> |
| 153 | +모듈을 호출하는 객체가 옵저버가 된다. <br><br> |
| 154 | + |
| 155 | +예를 들어, 안드로이드에서는 다음과 같이 OnClickListener 타입의 객체를 Button 객체에 등록하는데, 이때 OnClickListener 인터페이스가 옵저버 인터페이스가 된다. <br> |
| 156 | + |
| 157 | +~~~ |
| 158 | +public class MyActivity extends Activity implements View.OnClickListener { |
| 159 | + public void onCreate(Bundle savedInstanceState) { |
| 160 | + super.onCreate(savedInstanceState); |
| 161 | + setContentView(R.layout.main); |
| 162 | + ... |
| 163 | + Button loginButton = getViewById(R.id.main_loginbtn); |
| 164 | + loginButton.setOnClickListener(this); |
| 165 | + } |
| 166 | + |
| 167 | + @Override |
| 168 | + public void onClick(View v) { // OnclickListener의 메서드 |
| 169 | + login(id, password); |
| 170 | + } |
| 171 | + |
| 172 | + ... |
| 173 | +~~~ |
| 174 | +<br> |
| 175 | +한 객체의 옵저버 객체를 여러 주제 객체에 등록할 수도 있을 것이다. GUI 프로그래밍을 하면 이런 상황이 흔하게 발생한다. <br> |
| 176 | + |
| 177 | +예를 들어, 아래 코드처럼 로그인 버튼과 로그아웃 버튼에 동일한 OnclickListener 객체를 등록할 수 있다. <br> |
| 178 | + |
| 179 | +~~~ |
| 180 | +public class MyActivity extends Activity implements View.OnclickListener { |
| 181 | + public void onCreate(Bundle savedInstanceState) { |
| 182 | + super.onCreate(savedInstanceState); |
| 183 | + setContentView(R.layout.main); |
| 184 | + ... |
| 185 | + // 두 개의 버튼에 동일한 OnclickListener 객체 등록 |
| 186 | + Button loginButton = (Button) findViewById(R.id.main_loginbtn); |
| 187 | + loginButton.setOnClickListener(this); |
| 188 | + Button logoutButton = (Button) findViewById(R.id.main_logoutbtn); |
| 189 | + logoutButton.setOnClickListener(this); |
| 190 | + } |
| 191 | + |
| 192 | + @Override |
| 193 | + public void onClick(View v) { // OnClickListener의 메서드 |
| 194 | + // 주제 객체를 구분할 수 있는 방법 필요 |
| 195 | + if (v.getId() == R.id.main_loginbtn) { |
| 196 | + login(id, password); |
| 197 | + } else if (v.getId() == R.id.main_logoutbtn) { |
| 198 | + logout(_; |
| 199 | + } |
| 200 | + } |
| 201 | + ... |
| 202 | +~~~ |
| 203 | +<br> |
| 204 | +한 옵저버 객체를 여러 주제 객체에 등록하면, 옵저버 객체에서 각 주제 객체를 구분할 수 있는 방법이 필요하다. 위 코드에서 옵저버 객체의 메서드인 onClick() 메서드에서<br> |
| 205 | +주제 객체인 Button 객체를 구분하기 위해 ID값을 사용하였다. ID값 외에 아래 코드처럼 객체 레퍼런스를 사용할 수도 있을 것이다. <br> |
| 206 | + |
| 207 | +~~~ |
| 208 | + @Override |
| 209 | + public void onClick(View v) { // OnClickListener의 메서드 |
| 210 | + if (v == loginButton) { |
| 211 | + login(id, password); |
| 212 | + } else if (v == logoutButton) { |
| 213 | + logout(); |
| 214 | + } |
| 215 | +~~~ |
| 216 | +<br> |
| 217 | +앞서 StatusChecker 예제나 안드로이드의 예제는 모두 주체 객체를 위한 추상 타입을 제공하고 있다. 예를 들어, StatusChecker는 상위 타입인 StatusSubject <br> |
| 218 | +추상 클래스가 존재하고, 안드로이드의 Button 클래스는 상위 타입은 View가 존재한다. StatusSubject 클래스와 View 클래스는 모두 옵저버 객체를 관리하기 위한 기능을 <br> |
| 219 | +제공한다는 공통점이 있다. <br> |
| 220 | + |
| 221 | +~~~ |
| 222 | +// StatusChecker 클래스 |
| 223 | +public void add(StatusObserver observer) {... } |
| 224 | +
|
| 225 | +// View 클래스: |
| 226 | +public void setOnClickListener(OnClickListener o) {... } |
| 227 | +~~~ |
| 228 | +<br> |
| 229 | +한 주제에 대한 다양한 구현 클래스가 존재한다면, 위 코드처럼 옵저버 객체 관리 및 통지 기능을 제공하는 추상 클래스를 제공함으로써 불필요하게 동일한 코드가 <br> |
| 230 | +여러 주제 클래스에서 중복되는 것을 방지할 수 있을 것이다. 하지만, 해당 주제 클래스가 한 개뿐이라면 옵저버 관리를 위한 추상 클래스를 따로 만들 필요는 없을 것이다. <br><br> |
| 231 | + |
| 232 | +## 옵저버 패턴 구현의 고려 사항 |
| 233 | +옵저버 패턴을 구현할 때에는 다음 내용을 고려해야 한다.<br> |
| 234 | + |
| 235 | +<ul> |
| 236 | +<li>주제 객체의 통지 기능 실행 주체</li> |
| 237 | +<li>옵저버 인터페이스의 분리</li> |
| 238 | +<li>통지 시점에서의 주제 객체 상태</li> |
| 239 | +<li>옵저버 객체의 실행 제약 조건</li> |
| 240 | +</ul> |
| 241 | + |
| 242 | +이 외에 생각해 볼만한 고려 사항들이 있다. 예를 들면, 옵저버 객체에서 주제 객체의 상태를 다시 변경하면 어떻게 구현할 것인가에 대한 문제나 <br> |
| 243 | +옵저버 자체를 비동기로 실행하는 문제 등을 생각해 볼 수 있다. 이런 문제는 주어진 상황에 따라 대답이 달라질 수 있으므로, 실제 옵저버 패턴을 적용할 때 고민해봐야 한다. |
0 commit comments