Observer
옵저버 패턴
- UML
- Design Principle
- Loose coupling
- Implementation
- Java Observer
- Kotlin Observer
- Spring Observer
- Links
- References
UML
From: Wikipedia
From: whiteship
Design Principle
Observer Design Pattern: 다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴. pub-sub(발행-구독) 패턴을 구현할 수 있다. polling (주기적으로 요청해서 데이터를 가져오는 방식) 방식이 적합하지 않을 때 유용하다.
- 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
- 옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다. one 이 주제(subject) 이며, many 는 옵저버(observer) 이다.
- 어떤 이벤트가 발생했을 때 한 객체(주제(subject) 라 불리는)가 다른 객체 리스트(옵저버(observer) 라 불리는)에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용한다. GUI 애플리케이션에서 옵저버 패턴이 자주 등장한다. 버튼 GUI 컴포넌트에 옵저버를 설정할 수 있다. 그리고 사용자가 버튼을 클릭하면 옵저버에 알림이 전달되고 정해진 동작이 수행된다.
- 옵저버 패턴은 어떤 객체에 이벤트가 발생했을 때, 이 객체와 관련된 객체들(옵저버들)에게 통지하도록 하는 디자인 패턴을 말한다. 즉, 객체의 상태가 변경되었을 때, 특정 객체에 의존하지 않으면서 상태의 변경을 관련된 객체들에게 통지하는 것이 가능해진다. 이 패턴은 Pub/Sub(발행/구독) 모델으로 불리기도 한다.
advantages
- 상태를 변경하는 객체(publisher)와 변경을 감지하는 객체(subscriber)의 관계를 느슨하게 유지할 수 있음
- Subject 의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있음
- 런타임에 옵저버를 추가하거나 제거할 수 있음
disadvantages
- 복잡도가 증가함
- 다수의 Observer 객체를 등록 이후 해지하지 않는다면 Memory leak 이 발생할 수도 있음
- Ex.
private Map<String, List<Subscriber>> subscribers = new HashMap<>();
코드에서 subscribers 를 다른 곳에서 참조하지 않는다면 Map 에 WeakReference 를 적용할 수 있다. 이 경우 해지 메서드를 호출하지 않아도 GC 에 의한 회수 대상이 된다. 하지만 베스트 프랙티스는 명시적으로 해지하는 코드를 작성 하는 것이 가장 좋다.
- Ex.
Loose coupling
두 객체가 느슨하게 결합되어 있다는 것은, 그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미한다. 옵저버 패턴은 느슨한 결합을 제공한다.
느슨한 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다. 객체 사이의 상호의존성을 최소화 할 수 있다.
- 주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현 한다는 것 뿐이다.
- 옵저버의 구상 클래스가 무엇인지, 옵저버가 무엇을 하는지 등에 대해서 알 필요가 없다.
- 옵저버는 언제든 새로 추가할 수 있다.
- 실행 중 옵저버를 변경할 수도 있고, 제거할 수도 있다.
- 새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없다.
- 새로운 클래스에서 Observer 인터페이스를 구현하고 옵저버로 등록하면 된다.
- 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지는 않는다.
Implementation
First Example
옵저버 패턴으로 트위터 같은 커스터마이즈된 알림 시스템을 설계하고 구현할 수 있다. 다양한 신문 매체(뉴욕타임스, 가디언 등)가 뉴스 트윗을 구독하고 있으며 큭정 키워드를 포함하는 트윗이 등록되면 알림을 받고 싶어한다. 옵저버 인터페이스는 새로운 트윗이 있을 때 주제(Feed)가 호출할 수 있도록 notify 라고 하는 하나의 메서드를 제공한다.
- 옵저버 구현
class NYTimes implements Observer {
public void notify(String tweet) {
if(tweet != null & tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
}
}
class Guardian implements Observer {
public void notify(String tweet) {
if(tweet != null & tweet.contains("queen")) {
System.out.println("Yet more news from London! " + tweet);
}
}
}
- 주제 인터페이스
interface Subject {
void registerObserver(Observer o);
void notifyObservers(String tweet);
void removeObserver(Observer o);
}
주제는 registerObserver 메서드로 새로운 옵저버를 등록한 다음에 notifyObservers 메서드로 트윗의 옵저버에 이를 알린다.
- 주제 구현
class Feed implements Subject {
private final List<Observer> observers = new ArrayList<>();
// 옵저버 등록
public void registerObserver(Observer o) {
this.observers.add(o);
}
// 알림
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
// 옵저버 제거
public void removeObserver(Observer o) {
int i = this.observers.indexOf(o);
if(i >= 0) {
observers.remove(i);
}
}
}
- 주제와 옵저버를 연결하는 데모 애플리케이션
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
- 람다로 리팩토링 하기
f.registerObserver(String(tweet) -> {
if(tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
});
f.registerObserver(String(tweet) -> {
if(tweet != null && tweet.contains("money")) {
System.out.println("Yet more news from London! " + tweet);
}
});
Second Example
Chatting Service 처럼 polling 방식이 적합하지 않은 곳에서 pub/sub 패턴을 활용할 수 있다.
- Subscriber
public interface Subscriber {
void handleMessage(String message);
}
- Concrete Subscriber
@Getter
public class User implements Subscriber {
private String name;
public User(String name) {
this.name = name;
}
@Override
public void handleMessage(String message) {
System.out.println(message);
}
}
- ChatServer: Observer 패턴에 해당하는 Subject
public class ChatServer {
private Map<String, List<Subscriber>> subscribers = new HashMap<>();
// 등록
public void register(String subject, Subscriber subscriber) {
if (subscribers.containsKey(subject)) {
subscribers.get(subject).add(subscriber);
} else {
List<Subscriber> list = new ArrayList<>();
list.add(subscriber);
subscribers.put(subject, list);
}
}
// 해지
public void release(String subject, Subscriber subscriber) {
if (subscribers.containsKey(subject)) {
subscribers.get(subject).remove(subscriber);
}
}
// 알림
public void notify(User user, String subject, String message) {
if (subscribers.containsKey(subject)) {
String userMessage = user.getName() + ": " + message;
subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
}
}
}
- 사용 코드
public class Client {
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
User user1 = new User("BAEK-1");
User user2 = new User("BAEK-2");
chatServer.register("오징어게임", user1);
chatServer.register("오징어게임", user2);
chatServer.register("디자인패턴", user1);
chatServer.notify(user1, "오징어게임", "squid game");
chatServer.notify(user2, "디자인패턴", "observer");
}
}
Java Observer
java.util.Observable 클래스와 java.util.Observer 인터페이스가 있다. 이 두개는 직접 구현하는것 보다 훨씬 많은 기능을 지원한다. 푸시 방식
으로 갱신할 수도 있고, 풀 방식
으로 갱신할 수도 있다.
- Observable 과 Observer (자바 9부터 deprecated)
- 자바 9 이후 부터는 PropertyChangeListener, PropertyChangeEvent, Flow API, SAX (Simple API for XML) 라이브러리
Flow
- SubmissionPublisher
/**
* Creates a new SubmissionPublisher using the {@link
* ForkJoinPool#commonPool()} for async delivery to subscribers
* (unless it does not support a parallelism level of at least two,
* in which case, a new Thread is created to run each task), with
* maximum buffer capacity of {@link Flow#defaultBufferSize}, and no
* handler for Subscriber exceptions in method {@link
* Flow.Subscriber#onNext(Object) onNext}.
*/
public SubmissionPublisher() {
this(ASYNC_POOL, Flow.defaultBufferSize(), null);
}
- 사용 코드
public class FlowInJava {
public static void main(String[] args) throws InterruptedException {
Flow.Publisher<String> publisher = new SubmissionPublisher<>();
Flow.Subscriber<String> subscriber = new Flow.Subscriber<String>() {
private Flow.Subscription subscription;
@Override
public void onSubscribe(Flow.Subscription subscription) {
System.out.println("sub!");
this.subscription = subscription;
this.subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println("onNext called");
System.out.println(Thread.currentThread().getName());
System.out.println(item);
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
System.out.println("completed");
}
};
publisher.subscribe(subscriber);
((SubmissionPublisher)publisher).submit("hello java");
System.out.println("이게 먼저 출력될 수도 있습니다.");
}
}
- Reactive Stream API 의 주요 목적 중 하나는 BackPressure 를 관리하는 것이다. 구독을 하는 쪽에서 BackPressure 기능을 활용할 수 있다.
Kotlin Observer
프로퍼티의 변경 사항을 로그로 출력하고 싶은 경우나, 변경 내용을 통지하고 싶은 경우 stdlib 의 observable delegate 를 사용할 수 있다.
var items: List<Items> by Delegates.observable(listOf()) {
_, _, _ -> notifyDataSetChanged()
}
var key: String? by Delegates.observable(null) {
_, old, new -> Log.e("key changed form $old to $new")
}
Spring Observer
ApplicationContext(IoC Container, EventPublisher) 와 ApplicationEvent 에 Observer 패턴이 적용되어 있다.
@EventListener
- Event Object
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
- EventListener
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class MyEventListener {
@EventListener(MyEvent.class)
public void onApplicationEvent(MyEvent event) {
System.out.println(event.getMessage());
}
}
- Runner
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
public class MyRunner implements ApplicationRunner {
private ApplicationEventPublisher publisher;
public MyRunner(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public void run(ApplicationArguments args) throws Exception {
publisher.publishEvent(new MyEvent("hello spring event"));
}
}
- Application
@SpringBootApplication
public class ObserverInSpring {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ObserverInSpring.class);
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
}
Links
References
- GOF Design Pattern / gof 저 / 프로텍 미디어
- Head First Design Pattern / 에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠 저 / O'REILLY
- Java 객체지향 디자인 패턴 / 정인상, 채흥석 저 / 한빛미디어