티스토리 뷰

Java

DesignPattern#02. Observer Pattern

LichKing 2019. 9. 22. 18:12

1. 축구경기

국가대표 축구경기가 있다고 생각해보자. 골이 들어갈때마다 네이버와 다음 각 포털의 데이터가 변경되어야한다. 이를 어떻게 구현하면 될까?

 

첫번째 방법은 담당자가 축구를 보다가 골이 들어갈때마다 데이터를 변경해주는 수동 방법이있다. 수동방법을 몰라서 이 포스팅을 하는건 아니니 넘어가자.

 

두번째 방법은 축구협회에서 현재 골득점 상황을 알려주는 API를 제공한다고 가정했을때(제공안해주면 크롤링을 해서라도..), 각 포털사가 지속적으로 API를 호출하면서 데이터 변경 유무를 체크하는 방법이 있을 수 있다. 이 방식으로 구현하면 적어도 담당자가 직접 수작업은 하지않아도 되니 자동화가 가능해진다. 다만 축구협회 API를 호출하는 주기를 어떻게 잡을것인지가 중요하다. 너무 짧게 잡으면 축구협회 API에 부담이 될 수 있고 너무 길게 잡으면 데이터 실시간성이 맞지않아 이용자의 원성을 받을 수도있다. 가령 경기가 0:0 으로 끝나 사실 데이터 변경이 한번도 없었음에도 축구협회는 지속적으로 API 요청을 받아야하게된다.

 

세번째 방법은 역으로 포털사가 데이터 갱신 API를 제공하고 축구협회가 데이터 변동이 생겼을때 포털사의 API를 호출하는 것이다. 가장 합리적인 방법으로 보인다. 경기가 0:0 으로 끝나게되면 실질적인 API 요청은 한번도 이루어지지 않을것이다.

 

2. 코드

public class Kfa {
    private NaverApi naverApi;
    private DaumApi daumApi;
    
    public void goal(int home, int away) {
        naverApi.update(home, away);
        daumApi.update(home, away);
    }
}

public class NaverApi {
    public void update(int home, int away) {
        System.out.println(String.format("naver %d:%d", home, away));
    }
}

public class DaumApi {
    public void update(int home, int away) {
        System.out.println(String.format("daum %d:%d", home, away));
    }
}

간략하게 코드레벨로 보면 골득점 이벤트가 발생하면 Kfa 객체의 goal() 메서드가 호출된다. goal() 메서드는 각 포털사의 update() 메서드를 호출해서 데이터를 갱신한다. 아주 간략하게 잘 구현된것 같다. 그리고 이게 옵저버 패턴의 가장 기본이 되는 형태다. 하지만 지금의 이 형태는 디자인패턴이라고 보기힘든 절차적 코드다. 네이버와 다음 이외에 네이트도 축구협회의 정보를 받아보고싶다면? 축구협회와 다음과의 계약파기에 따라 다음은 축구협회의 정보를 받는것에서 제외되어야 한다면? 현재의 구조로서는 Kfa 클래스를 수정해야한다. 이를 좀더 객체지향적이고 유연한 코드로 리팩토링 해보자.

 

3. Observer Pattern

public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

public interface Observer {
    void update(int home, int away);
}

옵저버 패턴에서는 갱신된 데이터를 알려주는 주체를 Subject 라고 부르고, Subject 가 발행(publish)하는 내용을 구독(subscribe)하는 주체들을 Observer 라고 부른다. 각각 이에 해당하는 인터페이스를 생성했다.

public class Kfa implements Subject {
    private List<Observer> observers;
    private int home, away;

    public Kfa() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void notifyObservers() {
        observers.forEach(observer -> observer.update(home, away));
    }

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void setHome(int home) {
        this.home = home;
    }

    public void setAway(int away) {
        this.away = away;
    }
}

public class NaverApi implements Observer {
    private Kfa kfa;

    public NaverApi(Kfa kfa) {
        this.kfa = kfa;
        this.kfa.registerObserver(this);
    }

    @Override
    public void update(int home, int away) {
        System.out.println(String.format("naver %d:%d", home, away));
    }
}

public class DaumApi implements Observer {
    private Kfa kfa;

    public DaumApi(Kfa kfa) {
        this.kfa = kfa;
        this.kfa.registerObserver(this);
    }

    @Override
    public void update(int home, int away) {
        System.out.println(String.format("daum %d:%d", home, away));
    }
}

기존에 존재하던 Kfa, NaverApi, DaumApi 클래스들이 인터페이스를 구현하도록 변경했다. Subject 에 등록하는건 Observer 들이 직접 등록한다. 또한 지금은 코드상에 없지만 Subject 에서 제외하는것도 Observer 가 직접 하도록한다. 현재의 구조상에는 신규 포털인 네이트가 추가되어도 Subject 구현체는 아무런 수정없이 자가등록하면 Subject 가 발행하는 내용을 구독할 수 있다. 또한 다음이 구독에서 빠진다고 하더라도 Subject 에는 아무런 코드변경도 필요하지않다.

@Test                                 
public void test() {                  
    new NaverApi(this.kfa);           
    new DaumApi(this.kfa);            
                                      
    this.kfa.notifyObservers();       
    this.kfa.setHome(3);              
    this.kfa.notifyObservers();       
}                                     

(간단한 테스트 클래스)

 

완성된 내용을 클래스 다이어그램으로 정리하면 이렇게 된다.

다이어그램을 그리면서 생각해보니 현재 코드에서 아쉬운점이 하나 있는것 같다. 만약 현재 스코어 정보이외에 다른 정보들이 추가된다면 어떨까? 예를들어 골을 넣은 선수 명단을 보내야한다면 현재 구조상으로는 Observer 인터페이스가 변경되어야 할것이다. 코드의 구현이 변하는것과 공개된 인터페이스는 메서드 시그니처가 변하는건 차원이 다른 문제다. 지금처럼 각각 정보들을 파라미터로 넘기는게아니라 파라미터 객체를 따로 추출해서 파라미터 내용을 캡슐화한다면 좀 더 유연한 변화가 가능할것같다. 더욱이 해당 파라미터 객체도 인터페이스로 변경하면 단순히 축구정보뿐만 아니라 좀 더 일반적인 상황에서 사용가능한 인터페이스가 될것같다. (더욱 일반화 시켜 범용 인터페이스로 만드는게 나은지는 그때그때 상황과 판단에 따라 다를것이다.)

 

4. Java 제공 API

위에 대한 내용을 이미 JDK에서 제공하고있다. 그 내용에 대해 잠깐 살펴보자.

public class Kfa extends Observable {
    private List<Observer> observers;
    private int home, away;

    public Kfa() {
        this.observers = new ArrayList<>();
    }

    public void setHome(int home) {
        this.setChanged();
        this.home = home;
    }

    public void setAway(int away) {
        this.setChanged();
        this.away = away;
    }

    public void notifyObservers() {
        Map<String, Integer> map = Map.of("home", home, "away", away);
        this.notifyObservers(map);
    }
}

public class DaumApi implements Observer {
    private Kfa kfa;

    public DaumApi(Kfa kfa) {
        this.kfa = kfa;
        this.kfa.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        Map<String, Integer> map = (Map<String, Integer>) arg;
        System.out.println(String.format("daum %d:%d", map.get("home"), map.get("away")));
    }
}

JDK 에서는 Subject 에 대해서 Observable 이라는 클래스로, Observer 에 대해서는 동일하게 Observer 라는 인터페이스로 제공하고있다. Observable 에서는 기본 메서드들이 전부 구현되어있어 Subject 에 대한 메서드는 크게 구현할게 없다. 다만 인터페이스가 아니라 클래스로 제공되고있기때문에 필히 상속을 써야한다는점이 큰 단점이다. 거기에 상태가 변경되면 setChanged() 라는 메서드를 반드시 호출해줘야하는데 이 메서드가 protected 로 제공되고있기때문에 컴포지션으로 사용할수도없다.

 

근데 사실 이런저런내용에 대해 왈가왈부하고 뭐가 좋네 마네 하기전에.. 이번 포스팅을 작성하면서 알게된 사실인데 Observable 클래스와 Observer 인터페이스는 JDK 9 에서부터 deprecated 됐다. 원래는 너무 오래된 API 라서 제네릭도 적용이 안되어있다는 얘기도 덧붙이려고했었는데 예제코드 작성하다보니 이제 더이상 쓸일이 없는 API가 되어버렸다. (예전에 공부할때 살펴볼때만해도 멀쩡했었는데... 물론 그때는 JDK 9 나오기전이었지만...)

 

Observer 패턴 자체가 Rx 가 나오면서 많이 묻혀서.. 이제는 리액티브로 넘어가야할것같다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함