Observer는 스타크래프트 게임을 해본 사람이라면 익숙한 단어일 것이다. Observer는 사용자에게 시야를 제공할 뿐만 아니라 보이지 않는 유닛이나 지뢰 등을 비추어 보여주는 능력을 가지고 있다. 사전적 의미는 관찰자, 관측자 이며 단어의 의미에 부합하는 역할을 수행하는 유닛이다.
디자인 패턴 중 하나인 Observer Pattern의 Observer도 동일한 의미로 생각하면 된다. 한 개 이상의 Observer 객체가 특정 객체의 메서드 호출이나 상태 변경 등을 관찰하고 있다가 해당 객체에 변경 사항이 발생하면 변경을 감지하여 각 Observer 객체의 내부 로직을 수행하게 된다.
Observer 패턴은 스프링과 연계하여 사용하면 순수한 자바 기반의 코드보다 더욱 결합도, 의존도를 낮출 수 있을 것으로 보인다. 하지만 이 글은 기본 개념을 학습하는 데 의의를 두기 때문에 스프링과의 연계 없이 자바 애플리케이션만으로 예제를 작성할 것이다.
Class Diagram
예제는 리그오브레전드(LOL) 게임을 기반으로 작성할 것이다. LOL에는 총 4개의 라인이 있고, 한 팀에 5명의 유저가 배속된다. 라인은 다음의 네 가지 이다.
- Top : 최상단 공격로를 담당하는 유저
- Mid : 중단 공격로를 담당하는 유저
- Bottom : 하단 공격로를 담당하는 유저들 (원거리 딜러와 서포터가 한팀으로 구성됨)
- Jungle : 정글을 돌며 각 라인을 지원하는 유저
게임에는 팀원들과의 소통을 위해 도움, 퇴각, 위험 신호 등이 존재한다. 이를 흔히 '핑'이라고 하는데, 팀원 전체에게 핑을 전달하여 현재 상황에 대해 공유하고 소통을 할 수 있다.
Publisher에서 '핑'을 발생시키게 되면, 각 라인의 옵저버 클래스들이 이를 감지하여 내부 메서드를 실행하는 방식으로 예제를 구성한다.
구현
public interface Publisher {
void help();
void back();
void add(Observer observer);
void ignore(Observer observer);
void noti();
}
public class Orders implements Publisher {
private final List<Observer> observers;
public Orders() {
this.observers = new ArrayList<>();
}
@Override
public void help() {
System.out.println("[Publisher] 도움핑 발생...");
noti();
}
@Override
public void back() {
System.out.println("[Publisher] 퇴각 신호 발생...");
noti();
}
@Override
public void add(Observer observer) {
this.observers.add(observer);
}
@Override
public void ignore(Observer observer) {
this.observers.remove(observer);
}
@Override
public void noti() {
this.observers.forEach(Observer::accept);
}
}
이벤트를 발생시키는 Publisher 인터페이스는 게임과 코드에서 각각 다음의 의미를 가진다.
인게임: 게임 내에서 핑을 발생시켜 전체 팀원에게 신호를 전달한다.
코드: Publisher를 구현한 클래스에서 핑을 발생시키는 메서드가 호출되었을 때 Observer 객체가 이를 관찰 및 감지한다.
public interface Observer {
void accept();
}
public class Top implements Observer {
public Top(Publisher publisher) {
publisher.add(this);
}
@Override
public void accept() {
System.out.println("[Observer-Top] Accept...");
}
}
public class Mid implements Observer {
public Mid(Publisher publisher) {
publisher.add(this);
}
@Override
public void accept() {
System.out.println("[Observer-Mid] Accept...");
}
}
public class Bottom implements Observer {
public Bottom(Publisher publisher) {
publisher.add(this);
}
@Override
public void accept() {
System.out.println("[Observer-Bottom] Accept...");
}
}
public class Jungle implements Observer {
public Jungle(Publisher publisher) {
publisher.add(this);
}
@Override
public void accept() {
System.out.println("[Observer-Jungle] Accept...");
}
}
다음으로 이벤트를 관찰하여 내부 로직을 수행하는 Observer 클래스이다. Publisher와 마찬가지로 인게임 내에서와 코드에서의 의미를 구분지으면 다음과 같다.
인게임: 구성원이 핑을 발생시키면 나머지 팀원들은 이를 확인한 후 상황을 파악하고, 다음 행동을 결정한다.
코드: Publisher 객체에서 핑을 발생시키면 (메서드 호출) Observer가 이를 관찰한 후 자신의 내부 로직을 수행한다.
public class ProcessClass {
public static void main(String[] args) {
// Publisher
Publisher publisher = new Orders();
// Observers
Top top = new Top(publisher);
Mid mid = new Mid(publisher);
Bottom bottom = new Bottom(publisher);
Jungle jungle = new Jungle(publisher);
publisher.help();
System.out.println("=================================");
System.out.println("=================================");
publisher.back();
System.out.println("=================================");
System.out.println("=================================");
publisher.ignore(top);
publisher.back();
}
}
실행결과
[Publisher] 도움핑 발생...
[Observer-Top] Accept...
[Observer-Mid] Accept...
[Observer-Bottom] Accept...
[Observer-Jungle] Accept...
=================================
=================================
[Publisher] 퇴각 신호 발생...
[Observer-Top] Accept...
[Observer-Mid] Accept...
[Observer-Bottom] Accept...
[Observer-Jungle] Accept...
=================================
=================================
[Publisher] 퇴각 신호 발생...
[Observer-Mid] Accept...
[Observer-Bottom] Accept...
[Observer-Jungle] Accept...
main 메서드의 실행 흐름을 간략하게 요약하면 다음과 같다.
1. 핑(Event)을 발생시키는 주체인 Publisher 객체를 생성한다.
2. 그 후 핑을 관찰하고 상황을 판단하여 다음 행동을 이어갈 옵저버 객체를 각각 생성해준다. 이 때 옵저버 객체의 생성자에 파라미터로 앞서 생성한 Publisher 객체를 넘겨주어 Publisher 객체가 각 옵저버 객체를 컬랙션 형태로 포함할 수 있도록 한다.
3. Publisher 객체에서 help 신호가 발생한다. (메서드 호출) help 메서드의 기능이 수행된 후 noti() 메서드 호출을 통해 Publisher에 등록된 옵저버 객체의 컬랙션 전체가 반복으로 accept() 메서드를 수행한다. 즉 핑이라는 이벤트가 발생하면 이벤트 기능이 수행된 후 옵저버 객체의 기능이 각각 트리거처럼 곧바로 실행되는 것이다.
4. Publisher 객체에서 back 신호가 발생한다. 동작은 3과 동일하다.
5. Publisher에 연결된 Top 옵저버를 제거한 후 back 신호를 발생시킨다. 당연하게도, Mid / Bottom / Jungle은 이를 관찰하여 메서드가 정상 수행되었으나 Top은 신호를 인지하지 못하였다.
학습하며 느낀점
옵저버 패턴의 정의를 읽어보니 동작 방식이 스프링 내부 이벤트 처리나, Jpa의 flush를 통한 변경감지 및 Sql 자동 생성 기능과 유사한 것 같다. 스프링의 내부 이벤트는 사용자가 이벤트를 발행하게 되면 리스너에서 이벤트를 잡은 후 처리한다. Jpa의 경우에는 트랜잭션이 정상 종료, 즉 커밋되는 시점에 flush가 발생하면서 변경된 엔티티를 감지한 후 Sql을 자동으로 생성 및 실행해준다. 그 외에 데이터베이스의 트리거 등도 유사한 방식으로 동작한다고 할 수 있을 것 같다.
전체 소스 Github Link
https://github.com/kwonseonwoo/design-pattern
https://shan0325.tistory.com/33
https://woo0doo.tistory.com/28
'development > design pattern' 카테고리의 다른 글
[Java] Design-Pattern Proxy (2) | 2024.10.10 |
---|---|
[Java] Design-Pattern Singleton (0) | 2024.09.19 |