옵저버는 '감시자'라는 뜻을 가지고 있습니다. '감시'라는 건 어떤 상황이 일어나는지 계속 지켜보는 것이죠.
옵저버 패턴이란, 한 객체의 상태 변화에 따라 다른 객체의 상태도 연동되도록 일대다 객체 의존 관계를 구성 하는 패턴을 말합니다. 다시 말해, A 클래스와 B 클래스가 있다고 한다면, B가 수정되었을 때, 옵저버를 통해 A도 B가 수정된 사실을 알 수 있게 해주는 것입니다.
예를 들면, 감자 가격이 1% 올랐다고 가정해 봅시다.
그러면 감자를 재료로 하는 물건들도 가격이 따라 올라가겠죠.
각각 감자튀김 가격이 5%, 감자칩 가격이 3% 올랐습니다.
감자튀김이랑 감자칩이 감자의 가격이 오른 것을 어떻게 알았을까요?
가격을 찾아볼 수 있는 것도 아닐텐데 말이죠.
여기서 옵저버가 등장합니다.
감자와, 감자튀김/감자칩을 이어주는 역할을 옵저버가 맡음으로써 서로의 수정 사실을 알 수 있게 됩니다.
예시가 이해가 되셨나요? 그렇다면 이 예시를 코드로 바꾸어 다시 한번 설명해 보겠습니다.
// 옵저버 interface Products { void priceUpdate(); }
먼저, 옵저버 인터페이스를 만들었습니다.
이 옵저버를 각 물건들에 상속시켜서 물건들에게 가격 변동을 알리는 역할을 합니다.
즉, 이 옵저버는 물건들의 명단이 추상화된 것 입니다.
// 감자를 재료로 하는 물건들 관리 abstract class PotatoProducts { //감자를 재료로 하는 물건 리스트 private ArrayList<Products> products = new ArrayList<>(); // 물건 리스트에 추가 public void attach(Products product) { products.add(product); } // 물건 리스트에서 제거 public void detach(Products product) { products.remove(product); } // 물건들에게 가격 변동 알림 public void notifyPriceUpdate() { for (Products product : products) { product.priceUpdate(); } } }
감자를 재료로 하는 물건들을 관리하는 클래스를 만들었습니다.
이 클래스 안에 물건들을 직접 변수로 넣어 관리하면 객체 간의 독립성이 떨어지고 SOLID에 위배되기 때문에, 리스트로 만들어 간접적으로 관리해줍니다.
class Potato extends PotatoProducts { private int increase = 0; public void setIncrease(int increase) { this.increase = increase; notifyPriceUpdate(); } public void getIncrease() { return this.increase; } }
감자 클래스입니다. PotatoProducts를 통해 가격 변동을 물건들에게 전달해야 하기 때문에 감자에 상속시켜 줍니다.
숫자를 설정하면, notifyPriceUpdate 메소드가 실행되어 물건들에게 가격 변동 알림을 전달합니다.
class FrenchFries implements Products{ // 감자튀김 가격 변동 폭 private int FFIncrease = 5; // potato의 가격 변동 폭에 접근하기 위해 선언(getIncrease) private Potato potato; FrenchFries(Potato potato){ this.potato = potato; } @Override public void priceUpdate() { int nowIncrease = FFIncrease * potato.getIncrease(); System.out.println("감자튀김 가격이 " + nowIncrease + "% 올랐습니다!"); } }
potato.getIncrease()를 호출하기 위해 감자 객체를 만들어주고,
setIncrease()의 실행 후 호출된 notifyPriceUpdate()를 통해 감자튀김 객체에 접근해서, priceUpdate()를 실행했습니다.
class PotatoChip implements Products{ // 감자칩 가격 변동 폭 private int PCIncrease = 3; private Potato potato; PotatoChip(Potato potato) { this.potato = potato; } @Override public void priceUpdate() { int nowIncrease = PCIncrease * potato.getIncrease(); System.out.println("감자칩 가격이 " + nowIncrease + "% 올랐습니다!"); } }
감자튀김 클래스와 동일합니다.
public static void main(String[] args) { Potato potato = new Potato(); FrenchFries frenchFries = new FrenchFries(potato); PotatoChip potatoChip = new PotatoChip(potato); // 물건 관리 리스트에 추가 potato.attach(frenchFries); potato.attach(potatoChip); // i가 변화할 때 마다 감자의 가격 변동 폭 설정 for (int i = 1; i <= 5; i++) { potato.setIncrease(i); // 감자튀김 가격이 5 * i% 올랐습니다! System.out.println(); // 감자칩 가격이 3 * i% 올랐습니다! } }
실행 순서를 요약하면,
Potato 인스턴스화 → 물건들 인스턴스화 후 리스트에 추가(attach()) →
감자 가격 설정(potato.setIncrease()) → 각 물건에게 가격 변동 알림(notifyPriceUpdate()) → 오버라이딩한 priceUpdate() 실행순으로 진행되겠습니다.
결과적으로, 관리 대상 물건들을 유연하게 관리할 수 있고, 객체들 간의 의존성을 떨어뜨려 객체들끼리 직접 상호작용하지 않고, 옵저버를 통해 간접적으로 교류할 수 있었습니다.
이러한 것을 "느슨한 결합" 이라고 합니다.
느슨한 결합이란, 두 객체가 상호작용 하긴 하지만 그 둘이 서로에 대해 잘 모른다는 것을 의미합니다.
위의 예시에서는 감자 - 감자튀김/감자칩이 있습니다.
감자는 감자튀김/감자칩의 내부 변수(Increase)를 알 수 없고, 감튀/감자칩 또한 그렇습니다.
하지만 감튀/감자칩은 감자의 내부 변수가 필요하기에 getIncrease()라는 메소드를 통해 값을 전달받았죠.만약 내부 변수를 직접 접근(Potato.Increase)했다면, 감자의 가격에 변동이 생겼을 때, 감자 내부의 코드를 수정해야 하기 때문에 개방-폐쇄 원칙에 어긋나게 됩니다.
- 실시간으로 한 객체의 변경사항을 다른 객체에 전파할 수 있다.
- 느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다.
- 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다.
- 데이터 배분에 문제가 생기면 자칫 큰 문제로 이어질 수 있다.
- Thread safe 하지 않아 구독을 신청/취소하는 동안 원하는 결괏값을 얻기 힘들수도 있다.
- observer를 제때 제거해주지 않으면 메모리 누수가 일어날 수 있다.
필요없는 옵저버를 제때 detach하지 않을 경우 상태가 변경될 때 마다 필요 없는 함수가 계속 호출된다.
- 비동기 방식이기 때문에 이벤트 구독을 원하는 순서대로 받지 못할 수 있다.
옵저버의 추가/삭제가 빈번해질 경우 순서가 꼬일 수 있다.
https://gmlwjd9405.github.io/2018/07/08/observer-pattern.html