✏️ Observer Pattern (옵저버 패턴) 이란?
옵서버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. - 위키백과
상태가 변하는 특정 객체를 관찰자(옵저버)들이 관찰(구독)하고, 특정 객체에서 상태의 변화가 나타날 때 자신을 구독하고 있는 옵저버들에게 상태가 변화 됐음을 알려주는(발행) 발행/구독 패턴이다. 객체의 상태 변화를 감지하기 위해 사용될 수 있는 polling 방식을 지양하거나, 상대 클래스나 객체에 의존하지 않고 상태 변화를 통보하기 위해 유용한 패턴이다. 대표적으로 자바스크립트에서 onClick과 같은 이벤트 핸들러들을 예로 들을 수 있다.
🙋♂️ 특정 유저의 정보 조회하기
어느 웹 어플리케이션에서 유저 목록을 확인할 수 있는 테이블이 있다. 이 테이블에서 특정 유저를 클릭하면 해당하는 유저 ID의 변화를 감지하여 자동으로 정보를 가져오는 기능을 구현한다.
![UML]()
UserTable
클래스에서 특정 유저를 선택하는 selectUserId
메서드가 실행되면 userId
값을 저장하고, userInfoView.update()
메서드를 통해 값이 변경됨을 통보함으로써 변화를 감지하도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class UserTable {
private String userId;
private UserInfoView userInfoView;
public void setUserInfoView(UserInfoView userInfoView) {
this.userInfoView = userInfoView;
}
public String getUserId() {
return userId;
}
public void selectUserId(String userId) {
this.userId = userId;
userInfoView.update();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class UserInfoView {
private UserTable userTable;
public UserInfoView(UserTable userTable) {
this.userTable = userTable;
}
public void display(String userId) {
System.out.println("================ " + userId + " User Info ===============");
System.out.println("User ID: ...");
System.out.println("User Name: ...");
System.out.println("===============================================");
System.out.println("");
}
public void update() {
String userId = userTable.getUserId();
display(userId);
}
}
|
1
2
3
4
5
6
7
| public static void main(String[] args) {
UserTable userTable = new UserTable();
UserInfoView userInfoView = new UserInfoView(userTable);
userTable.setUserInfoView(userInfoView);
userTable.selectUserId("U001");
}
|
1
2
3
4
| ================ U001 User Info ===============
User ID: ...
User Name: ...
===============================================
|
의도했던 대로 유저를 선택하면 변화된 userId
값을 감지하고 해당 유저의 기본 정보를 출력하게 되었다.
💣 문제점
하지만 만약 우리가 유저 목록 테이블에서 선택한 유저 ID가 변화할 때, 해당 유저의 기본 정보뿐만 아니라, 결제 기록, 활동 로그 등 다양한 데이터를 가져오게 하고 싶다면 UserTable
클래스를 직접 수정해야 할 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class UserTable {
private String userId;
private UserInfoView userInfoView;
private UserLogView userLogView;
public void setUserInfoView(UserInfoView userInfoView) {
this.userInfoView = userInfoView;
}
public void setUserLogView(UserLogView userLogView) {
this.userLogView = userLogView;
}
public String getUserId() {
return userId;
}
public void selectUserId(String userId) {
this.userId = userId;
userInfoView.update();
userLogView.update();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class UserLogView {
private UserTable userTable;
public UserLogView(UserTable userTable) {
this.userTable = userTable;
}
public void display(String userId) {
System.out.println("================ " + userId + " User Log ===============");
System.out.println("2022-08-21 12:45:50: ...");
System.out.println("2022-08-19 09:21:00: ...");
System.out.println("2022-08-17 06:01:21: ...");
System.out.println("2022-08-16 14:58:44: ...");
System.out.println("2022-08-15 16:22:07: ...");
System.out.println("2022-08-14 20:11:54: ...");
System.out.println("===============================================");
System.out.println("");
}
public void update() {
String userId = userTable.getUserId();
display(userId);
}
}
|
1
2
3
4
5
6
7
8
9
10
| public static void main(String[] args) {
UserTable userTable = new UserTable();
UserInfoView userInfoView = new UserInfoView(userTable);
UserLogView userLogView = new UserLogView(userTable);
userTable.setUserInfoView(userInfoView);
userTable.setUserLogView(userLogView);
userTable.selectUserId("U001");
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| ================ U001 User Info ===============
User ID: ...
User Name: ...
===============================================
================ U001 User Log ===============
2022-08-21 12:45:50: ...
2022-08-19 09:21:00: ...
2022-08-17 06:01:21: ...
2022-08-16 14:58:44: ...
2022-08-15 16:22:07: ...
2022-08-14 20:11:54: ...
===============================================
|
새로운 유저 정보 View를 추가하여 적용하기 위해 UserTable
클래스를 직접적으로 수정하게 됐다. 이렇게 작성하게 된다면 해당 유저의 다른 정보 조회의 기능이 추가 될 때마다 UserTable
클래스를 반복적으로 수정해야할 것이다. 이는 곧 개방 폐쇄 원칙(Open Close Principle)에 위배하게 된다.
💡 해결법
이렇게 정보 조회의 기능이 추가되거나 수정이 되어도, UserTable
클래스를 변경하지 않고 그대로 재사용 할 수 있어야 한다. 옵저버 패턴을 적용하여 이 문제를 해결해보자.
![Observer Pattern UML]()
앞서 말했듯 옵저버 패턴은 상태가 변하는 특정 객체를 옵저버들이 구독하고, 특정 객체에서 상태의 변화가 나타날 때 자신을 구독하고 있는 옵저버들에게 상태가 변화 됐음을 통지하는 패턴이라 설명했다. Subject
클래스를 통해 변화하는 객체를 감시하는 옵저버들을 구독시키거나, 해제한다. 그리고 옵저버가 될 객체는 observer
인터페이스를 구현하도록 하고, 선택한 유저 ID가 변화할 때 즉, UserTable
클래스의 selectUserId
메소드가 실행되면, Subject
클래스의 notifyObserver
메소드를 실행하여, 구독하고 있는 옵저버들에게 변화를 통지한다. 통지받은 옵저버들은 Observer
인터페이스를 통해 update
메소드를 실행하게 된다.
1
2
3
| public interface Observer {
public void update();
}
|
통지 대상을 인터페이스로 추상화하고, 변화를 통지 받았을 때 처리하는 update
메서드를 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public abstract class Subject {
private List<Observer> observers = new ArrayList<Observer>();
public void subscribe(Observer observer) {
observers.add(observer);
};
public void unsubscribe(Observer observer) {
observers.remove(observer);
};
public void notifyObservers() {
for (Observer o:observers) {
o.update();
}
};
}
|
통지 대상인 옵저버를 구독하고, 해제하는 기능을 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class UserTable extends Subject {
private String userId;
public String getUserId() {
return userId;
}
public void selectUserId(String userId) {
this.userId = userId;
notifyObservers();
}
}
|
userId
가 변화하면 각 옵저버들에게 변화를 알린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class UserInfoView implements Observer {
private UserTable userTable;
public UserInfoView(UserTable userTable) {
this.userTable = userTable;
}
public void display(String userId) {
System.out.println("================ " + userId + " User Info ===============");
System.out.println("User ID: ...");
System.out.println("User Name: ...");
System.out.println("===============================================");
System.out.println("");
}
public void update() {
String userId = userTable.getUserId();
display(userId);
}
}
|
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
| public class UserLogView implements Observer {
private UserTable userTable;
public UserLogView(UserTable userTable) {
this.userTable = userTable;
}
public void display(String userId) {
System.out.println("================ " + userId + " User Log ===============");
System.out.println("2022-08-21 12:45:50: ...");
System.out.println("2022-08-19 09:21:00: ...");
System.out.println("2022-08-17 06:01:21: ...");
System.out.println("2022-08-16 14:58:44: ...");
System.out.println("2022-08-15 16:22:07: ...");
System.out.println("2022-08-14 20:11:54: ...");
System.out.println("===============================================");
System.out.println("");
}
public void update() {
String userId = userTable.getUserId();
display(userId);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public static void main(String[] args) {
UserTable userTable = new UserTable();
UserInfoView userInfoView = new UserInfoView(userTable);
UserLogView userLogView = new UserLogView(userTable);
userTable.subscribe(userInfoView);
userTable.subscribe(userLogView);
userTable.selectUserId("U001");
userTable.unsubscribe(userLogView);
userTable.selectUserId("U002");
}
|
상속받은 Observer
인터페이스의 update
메소드를 구현함으로써 옵저버가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| ================ U001 User Info ===============
User ID: ...
User Name: ...
===============================================
================ U001 User Log ===============
2022-08-21 12:45:50: ...
2022-08-19 09:21:00: ...
2022-08-17 06:01:21: ...
2022-08-16 14:58:44: ...
2022-08-15 16:22:07: ...
2022-08-14 20:11:54: ...
===============================================
================ U002 User Info ===============
User ID: ...
User Name: ...
===============================================
|
이제 UserTable
클래스는 Subject
클래스를 상속받아 변화하는 객체를 구독하고 해지할 수 있도록 Subject
클래스를 통해 관리함으로써, 관심 객체를 직접 참조할 필요가 없게되었고, UserTable
클래스를 직접 수정하지 않고도 옵저버를 추가, 제거가 가능해져 객체가 느슨한 결합이 되었다고 볼 수 있을 것이다.