옵저버들의 목록을 객체에 등록하여 상태 변화가 있을때마다 메소드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하고, 옵저버들은 알림을 받아 조치를 취하는 디자인 패턴
특징
- 콜백
- 콜을 먼저 하고 백은 나중에 함 ⇒ 언제 끝날지 모르니까 콜해놓고 끝나면 백 (즉각적으로 백하는게 아님)
- 일대다(1:N) 관계
- 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 알림이 가고, 자동으로 정보가 갱신됨
- 인터페이스를 통해 연결하여 느슨한 결합성 유지
- Publisher 인터페이스
- Observer 인터페이스
참고
- 수동적인 서버(polling 방식)가 아닌 능동적인 서버(push 방식) 이용
- polling은 수동적이어서 요청이 있어야만 함
- push는 능동적이어서 클라이언트 상태 관리 필요 없음 ⇒ 변수로 안둬도 되는데 상태를 알고는 있어야 함
폴링 방식(polling)
- 옵저버가 주기적으로 주체(특정 장치나 시스템)의 상태 조회 및 변경 사항 확인하고 필요 데이터 요청
- 구현이 간단함 (장점)
- 주기적으로 확인해서 예측 가능한 동작 할 수 있음 (장점)
- 불필요한 요청 발생으로 자원 낭비할 수 있음 (단점)
- 실시간성이 낮아 즉각적인 반응이 필요한 경우 타이밍 잘 못 맞춤 (단점)
푸쉬 방식(push)
- 주체가 자신의 상태가 변경될 때마다 옵저버들에게 직접 알림과 필요한 데이터 보냄
- 데이터 변경 즉시 반영, 실시간 반응 가능 ⇒ 타이밍 잘 맞춤 (장점)
- 주체의 상태가 자주 변경된다면 옵저버들에게 데이터를 지속적으로 push하므로 주체에 부하 생길 수 있음 (단점)
예제 1
- PubListener 인터페이스를 Pub 클래스에서 구현(implements)
- SubListener를 담는 구독자 명단(List) 생성
- 구독자 명단에 추가 (오버라이드)
- 구독자 명단에서 삭제 (오버라이드)
- 상품이 입고되면, 구독자들에게 알림을 보냄 (오버라이드)
- SubListener 인터페이스를 Sub1, Sub2 클래스에서 구현(implements)
- sub1, sub2가 각각 받은 알림 (오버라이드)
- App 클래스의 main에서 실행
- 객체 생성 (Pub, Sub1, Sub2를 new)
- 구독하기 ⇒ Pub의 add메소드에 sub1(구독자), sub2(구독자)를 적용 ⇒ 구독자 명단에 추가됨
- 상품 입고(가정)
- 상품이 입고됐다는 알림 보냄(notifyChange 호출)
package ch07.pub;
import ch07.sub.SubListener;
public interface PubListener {
void add(SubListener subListener); // 컬렉션 추가
void remove(SubListener subListener); // 컬렉션 삭제
void notifyChange(String msg); // 추가되든 삭제되든 알림
}
package ch07.pub;
import ch07.sub.SubListener;
import java.util.ArrayList;
import java.util.List;
// 인터페이스 구현(implements PubListener)
// => 오버라이드, 동적바인딩
public class Pub implements PubListener {
// 1. 구독자 명단
List<SubListener> subList = new ArrayList<>();
// 2. 이 메소드의 책임 : 구독자 명단에 추가
@Override
public void add(SubListener sub) {
subList.add(sub);
}
// 2. 이 메소드의 책임 : 구독자 명단에 삭제
@Override
public void remove(SubListener sub) {
subList.remove(sub);
}
// 2. 이 메소드의 책임 : 상품이 입고되면, 구독자들에게 알림을 보냄
@Override
public void notifyChange(String msg) {
// 5초 잠자기
for (int i = 1; i < 6; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(".");
}
for (SubListener sub : subList) {
sub.update(msg);
}
}
}
package ch07.sub;
public interface SubListener {
void update(String msg);
}
package ch07.sub;
// 인터페이스 구현(implements SubListener)
// => 오버라이드, 동적바인딩
public class Sub1 implements SubListener {
@Override
public void update(String msg) {
System.out.println("sub1이 받은 알림 : " + msg);
// 무엇을 해야할지는 이곳에서 구현
}
}
package ch07.sub;
public class Sub2 implements SubListener {
@Override
public void update(String msg) {
System.out.println("sub2가 받은 알림 : " + msg);
// 무엇을 해야할지는 이곳에서 구현
}
}
package ch07;
import ch07.pub.Pub;
import ch07.pub.PubListener;
import ch07.sub.Sub1;
import ch07.sub.Sub2;
import ch07.sub.SubListener;
// 옵저버 패턴 -> 콜백
// 콜을 먼저 하고 백은 나중에(언제 끝날지 모르니까 / 콜해놓고 끝나면 백)
public class App {
public static void main(String[] args) {
// 1. 객체 생성 init
// 타입이 왜 부모형(이 경우 인터페이스)인가?
// 각 클래스에 implement(구현) 해놨으니까
PubListener pub = new Pub();
SubListener sub1 = new Sub1();
SubListener sub2 = new Sub2();
// 2. 구독하기
pub.add(sub1);
pub.add(sub2);
// 3. 상품 들어옴(가정)
// 4. notifyChange 호출
pub.notifyChange("상품 들어옴");
}
}예제2 - Thread(스레드) 응용
- Pub 클래스
- 상품 선언과 상품 입고 메소드
- 구독자 명단
- 구독자 명단에 추가 메소드
- 상품 입고시 구독자에게 알림 보냄 메소드
- SubListener 인터페이스를 Sub1, Sub2 클래스에서 구현(implements)
- Sub1, Sub2 클래스에서 오버라이드 (메소드의 책임(기능) 작성)
- App 클래스의 main에서 실행 (main 스레드)
- 객체 생성
- 구독하기
- main 스레드가 새로운 스레드 달라고 요청
- 마트에 상품 입고하는 스레드
- 상품 입고되면 마트가 구독자에게 알림보내는 스레드
⇒ 스레드 객체 생성(new)하고 start()로 os로부터 스레드 빌림
⇒ os가 run(실행)해주고 다 하면 생명주기 종료
⇒ 스레드에서 run 메소드 (() → {}) 실행
package ch08.pub;
import ch08.sub.SubListener;
import java.util.ArrayList;
import java.util.List;
public class Pub {
// 상품
public String value = null;
// 상품 들어옴 메소드
public void income() {
// 어떤 상품인지 초기화
value = "바나나";
}
// 1. 구독자 명단
public List<SubListener> subList = new ArrayList<>();
// 2. 이 메소드의 책임 : 구독자 명단에 추가
public void add(SubListener sub) {
subList.add(sub);
}
// 2. 이 메소드의 책임 : 물건이 들어오면, 구독자들에게 알림을 보냄
public void notifyChange(String msg) {
// 구독자들에게 알림주기
// for-each문으로 subList 순회
for (SubListener sub : subList) {
sub.update(msg);
}
}
}
package ch08.sub;
public interface SubListener {
void update(String msg);
}
package ch08.sub;
// 인터페이스 구현(implements SubListener)
// => 오버라이드, 동적바인딩
public class Sub1 implements SubListener {
@Override
public void update(String msg) {
System.out.println("sub1이 받은 알림 : " + msg);
// 무엇을 해야할지는 이곳에서 구현
}
}
package ch08.sub;
public class Sub2 implements SubListener {
@Override
public void update(String msg) {
System.out.println("sub2가 받은 알림 : " + msg);
// 무엇을 해야할지는 이곳에서 구현
}
}
package ch08;
import ch08.pub.Pub;
import ch08.sub.Sub1;
import ch08.sub.Sub2;
import ch08.sub.SubListener;
// 옵저버 패턴 -> 콜백
// 콜을 먼저 하고 백은 나중에
// => 언제 끝날지 모르니까 콜하자마자 백하는게 아님
public class App {
public static void main(String[] args) {
// 1. 객체 생성 init
// 타입이 왜 부모형(이 경우 인터페이스)인가?
// 각 클래스에 implement 해놨으니까
Pub pub = new Pub();
SubListener sub1 = new Sub1();
SubListener sub2 = new Sub2();
// 구독하기
pub.add(sub1);
pub.add(sub2);
// 2. 마트에 상품 입고하는 스레드
// 스레드 생성(구독자/서버, new로 객체 생성)하고
// start()로 os로부터 스레드를 빌림
// => os가 run(실행)을 해주고 다 하면 생명주기 종료
// 이 경우 현재 스레드는 3개 (main 스레드가 이미 존재하기 때문)
// => main 스레드가 새로운 스레드를 달라고 요청함
// 스레드 파라미터 안의 ()->{} : run 메소드
new Thread(()->{
try {
Thread.sleep(10000); // 10초
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pub.income(); // 상품 들어옴
}).start();
// 3. 마트 스레드
new Thread(()->{
while(true) {
if (pub.value == null) {
// 상품 입고 안 됐을 경우 구독자한테 줄 알림
System.out.println("상품 미입고");
} else {
// 상품 입고됐을 경우 구독자한테 줄 알림
pub.notifyChange(pub.value + " 입고됨");
break; // 상품 입고되면 break
}
try {
Thread.sleep(1000); // 1초
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
// 이 경우, 이 시점에서 main 스레드는 종료(생명주기)되고
// main 스레드가 요청해놓은 스레드 2개는 살아있음
}
}Share article