티스토리 뷰
‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.
웹 사이트의 상태를 확인해서 응답 속도가 느리거나 연결이 안 되면 모니터링 담당자에게 이메일로 통지해주는 시스템을 만든다고 가정해보자.
상태를 확인하는 StatusChecker 클래스를 다음과 같이 구현할 수 있다.
public class StatusChecker {
private EmailSender emailSender;
public void check() {
Status status = loadStatus;
if(status.isNotNormal()) {
emailSender.sendEmail(status);
}
}
}
SMS로 바로 알려주는 기능을 추가하는 요구가 들어온다면 코드는 다음과 같아진다.
public class StatusChecker {
private EmailSender emailSender;
private SmsSender smsSender;
public void check() {
Status status = loadStatus;
if(status.isNotNormal()) {
emailSender.sendEmail(status);
smsSender.sendSms(status);
}
}
}
SMS처럼 계속 기능을 추가하는 요구가 계속된다면 위 코드처럼 StatusChecker가 변경된다.
StatusChecker는 시스템의 상태가 불안정해지면 이 사실을 EmailSender와 SmsSender 객체에게 알려주는데, 여기서 핵심은 상태가 변경될 때 정해지지 않은 임의의 객체에게 변경 사실을 알려준다는 점이다.
옵저버 패턴은 이렇게 한 객체의 상태 변화를 정해지지 않은 여러 다른 객체에 통지하고 싶을 때 사용된다.
옵저버 패턴의 구조는 다음과 같다.
옵저버 패턴에는 크게 주제(Subject)와 옵저버 객체가 등장하는데, 주제 객체는 다음의 두 가지 책임을 갖는다.
- 옵저버 목록을 관리하고, 옵저버를 등록하고 제거할 수 있는 메서드를 제공한다.
- add() : 옵저버를 목록에 등록
- remove() : 옵저버를 목록에 삭제
- 상태의 변경이 발생하면 등록된 옵저버에 변경 내역을 알린다.
- notifyStatus() 메서드가 등록된 옵저버 객체의 onAbnormalStatus() 메서드를 호출한다.
StatusSubject 클래스는 옵저버 목록을 List 타입으로 보관할 수 있다.
import java.util.ArrayList;
public abstract class StatusSubject {
private List<StatusObserver> observers = new ArrayList<StatusObserver>();
public void add(StatusObserver observer) {
observers.add(observer);
}
public void remove(StatusObserver observer) {
observers.remove(observer);
}
public void notifyStatus(Status status) {
for(StatusObserver observer : observers) {
observer.onAbnormalStatus(status);
}
}
}
notifyStatus() 메서드는 observers List에 등록된 각 StatusObserver 객체의 onAbnormalStatus() 메서드를 호출하는데, 이렇게 옵저버 객체의 메서드를 호출하는 방식으로 상태에 변화가 있음을 옵저버 객체에게 알린다.
Status 상태 변경을 알려야 하는 StatusChecker 클래스는 다음과 같이 StatusSubject 클래스를 상속받아 구현한다.
public class StatusChecker extends StatusSubject {
public void check() {
Status status = loadStatus();
if (status.isNotNormal()) {
super.notifyStatus(status);
}
}
private Status loadStatus() {
// ...
}
}
StatusChecker 클래스는 비정상 상태가 감지되면 상위 클래스의 notifyStatus() 메서드를 호출해서 등록된 옵저버 객체들에 상태 값을 전달한다.
옵저버 객체를 구현한 클래스는 주제(Subject) 객체가 호출하는 메서드에서 필요한 기능을 구현하면 된다.
앞 예제의 경우, StatusSubject 타입 객체에 등록되는 옵저버 인터페이스인 StatusObserver는 다음 코드와 같이 Subject 객체로부터 상태 변화를 전달받을 수 있는 메서드인 onAbnormalStatus() 메서드를 정의하고 있다.
public interface StatusObserver {
void onAbnormalStatus(Status status);
}
옵저버 구현 클래스인 StatusEmailSender 클래스는 다음 코드처럼 위 인터페이스를 상속받아 기능을 구현한다.
public class StatusEmailSender implements StatusObserver {
@Override
public void onAbnormalStatus(Status status) {
sendEmail(status);
}
private void sendEmail(Status status) {
// 이메일 전송 코드
}
}
subject 객체의 상태에 변화가 생길 때 그 내용을 통지받도록 하려면, 옵저버 객체를 subject 객체에 등록해 주어야 한다.
예를 들어, 시스템의 상태가 비정상이 될 때 StatusChecker 객체가 StatusEmailSender 객체에 통지하게 하려면 다음 코드처럼 StatusEmailSender 객체를 StatusChecker 객체에 옵저버로 등록해 주어야 한다.
StatusChecker checker = new StatusChecker();
checker.add(new StatusEmailSender()); // 옵저버로 등록
위와 같이 옵저버로 등록되면, 시스템이 비정상 상태가 될 때마다 StatusChecker 객체가 StatusEmailSender 객체의 onAbnormalStatus() 메서드를 호출해서 상태 정보를 통지해준다.
따라서 StatusEmailSender 객체는 시스템이 비정상 상태가 될 때 담당자에게 이메일로 통보해 줄 수 있게 된다.
옵저버 패턴의 적용할 때의 장점은 주제 클래스 변경 없이 상태 변경을 통지 받을 옵저버를 추가할 수 있다는 점이다.
예를 들어, 장애가 발생할 때 SMS를 이용해서 문자를 전송하고 싶다면, 해당 기능을 구현한 옵저버 객체를 StatusChecker 객체에 등록해 주기만 하면 된다.
StatusChecker checker = ...;
// 새로운 타입의 옵저버가 추가되어도 StatusChecker 코드는 바뀌지 않는다.
checker.add(new StatusEmailSender()); // 옵저버로 등록
옵저버 객체에게 상태 전달 방법
옵저버 객체가 기능을 수행하기 위해 Subject 객체의 상태가 필요할 수 있다.
예를 들어, FaultStatusSmsSender 클래스는 다음과 같이 구현한다.
- 장애 상태인 경우에만 SMS를 전송
- 비정상 상태(응답 속도가 느려진 상태)에는 메시지를 전송하지 않음
이 경우 FaultStatusSmsSender 클래스는 상태 값을 확인해야 한다.
옵저버 객체에서 콘크리트 Subject 객체에 직접 접근하는 방법을 사용하기도 한다.
아래 코드는 옵저버 객체에서 특정 타입의 Subject 객체를 사용하는 코드다.
public class SpeicalStatusObserver implements StatusObserver {
private StatusChecker statusChecker;
private Siren siren;
public SpeicalStatusObserver(StatusChecker statusChecker) {
this.statusChecker = statusChecker;
}
@Override
public void onAbnormalStatus(Status status) {
// 특정 타입의 주체 객체에 접근
if (status.isFault() && statusChecker.isContinuousFault()) {
siren.begin();
}
}
}
SpeicalStatusObserver 클래스의 onAbnormalStatus() 메서드는 status 파라미터와 statusChecker 필드를 이용해서 사이렌의 실행 조건을 판단하고 있다.
SpeicalStatusObserver 클래스에서 StatusChecker 클래스로의 의존이 발생하게 되는데, 이렇게 콘크리트 옵저버 클래스는 필요에 따라 특정한 콘크리트 주제 클래스에 의존하게 된다.
옵저버에서 Subject 객체 구분
옵저버 패턴은 GUI 프로그래밍 영역에서 제일 많이 사용된다.
버튼이 눌릴 때 로그인 기능을 호출한다고 할 때, 버튼이 Subject 객체가 되고 로그인 모듈을 호출하는 객체가 옵저버가 된다.
한 Subject에 대한 다양한 구현 클래스가 존재한다면, 옵저버 객체 관리 및 통지 기능을 제공하는 추상 클래스를 제공함으로써 불필요하게 동일한 코드가 여러 Subject 클래스에서 중복되는 것을 방지할 수 있다.
해당, 해당 Subject 클래스가 한 개뿐이라면 옵저버 관리를 위한 추상 클래스를 따로 만들 필요는 없다.
옵저버 패턴 구현의 고려 사항
- Subject 객체의 통지 기능 실행 주체
- 옵저버 인터페이스의 분리
- 통지 시점에서의 Subject 객체 상태
- 옵저버 객체의 실행 제약 조건
1. Subject 객체의 통지 기능 실행 주체
앞서 StatusChecker 예에서는 등록된 옵저버에 통지하는 주체가 StatusChecker 클래스였다.
그런데, 필요에 따라 StatusChecker를 사용하는 코드에서 통지 기능을 수행할 수도 있을 것이다.
Subject 객체의 상태가 바뀔 때마다 옵저버에게 통지를 해 주어야 한다면, Subject 객체에서 직접 통지 기능을 실행하는 것이 구현에 유리하다.
왜냐하면, Subject 객체를 사용하는 코드에서 통지 기능을 실행한다면 상태를 변경하는 모든 코드에서 통지 기능을 함께 호출해 주어야 하는데, 이런 방식은 통지 기능을 호출하지 않는 등 개발자의 실수를 유발할 수 있기 때문이다.
반대로, 한 개 이상의 Subject 객체의 연속적인 상태 변경 이후에 옵저버에게 통지를 해야 한다면, Subject 객체가 아닌 그의 상태를 변경하는 코드에서 통지 기능을 실행해 주도록 구현하는 것이 통지 시점을 관리하기가 수월하다.
2. 옵저버의 인터페이스의 개수
한 Subject 객체가 통지할 수 있는 상태 변경 내역의 종류가 다양한 경우에는 각 종류 별로 옵저버 인터페이스를 분리해서 구현하는 것이 좋다.
모든 종류의 상태 변경을 하나의 옵저버 인터페이스로 처리할 경우, 옵저버 인터페이스는 거대해질 것이다.
모든 종류의 상태 변화를 수신하는 인터페이스가 존재할 경우, 콘크리트 옵저버 클래스는 모든 메서드를 구현해 주어야 한다.
실제로 콘크리트 옵저버 클래스에서 구현할 메서드가 하나일지라도 나머지 메서드의 빈 구현을 만들어 주어야 한다.
Subject 객체 입장에서도 각 상태마다 변경의 이유가 다르기 때문에, 이들을 한 개의 옵저버 인터페이스로 관리하는 것은 향후에 변경을 어렵게 만드는 요인이 될 수 있다.
3. 통지 시점에서 Subject 객체의 상태에 결함이 없어야 한다
옵저버 객체가 올바르지 않은 상태 값을 사용하게 되는 문제가 발생하지 않도록 만드는 방법 중의 하나는 상태 변경과 통지 기능에 템플릿 메서드 패턴을 적용하는 것이다.
4. 옵저버 객체의 실행에 대한 제약 규칙을 정해야 한다
예를 들어 Subject 객체가 옵저버에 통지하기 위해 사용되는 메서드를 아래와 같이 구현했다고 하자.
private void notifyToObserver() {
for (StatusObserver o : observers) {
o.onStatusChange();
}
}
public void changeState(int state) {
internalChangeState(newState);
notifyToObserver();
}
만약 10개의 옵저버 객체가 있고, 각 옵저버 객체의 onStatusChange() 메서드마다 실행 시간이 십 분 이상 걸린다면 어떻게 될까?
이 경우, changeState() 메서드를 호출한 코드는 모든 옵저버 객체의 onStatusChange() 메서드 실행이 종료될 때까지 100분 이상 기다려야 한다.
또는 한 개의 옵저버로 인해 다른 옵저버의 실행이 지연되는 상황이 발생할 수도 있다.
따라서 옵저버 인터페이스를 정의할 때에는 옵저버 메서드의 실행 제한에 대한 명확한 기준이 필요하다.
예를 들어 StatusObserver.onStatusChange() 메서드는 수 초 이내에 응답해야 하고 긴 작업을 수행해야 할 경우 별도 쓰레드로 실행해야 한다는 등의 제약 조건이 필요하다.
이 외에는 다음과 같은 것들이 있다.
- 옵저버 객체에서 Subject 객체의 상태를 다시 변경하면 어떻게 구현할 것인가에 대한 문제
- 옵저버 자체를 비동기로 실행하는 문제
이런 문제는 주어진 상황에 따라 대답이 달라질 수 있다.
'JAVA > 디자인 패턴' 카테고리의 다른 글
추상 팩토리(Abstract Factory) 패턴 (0) | 2022.03.06 |
---|---|
파사드(Facade) 패턴 (0) | 2022.03.05 |
어댑터(Adapter) 패턴 (0) | 2022.03.01 |
프록시(proxy) 패턴 (0) | 2022.03.01 |
데코레이터(Decorator) 패턴 (0) | 2022.03.01 |
- Total
- Today
- Yesterday
- BAEKJOON
- kotest
- 정규표현식
- MSA
- Java
- 디자인 패턴
- AWS
- 클린 코드
- Kotlin
- JPA
- Spring Boot
- node.js
- 코테
- BOJ
- 이펙티브 자바
- 객체지향
- Algorithm
- 알고리즘
- 디자인패턴
- programmers
- 클린 아키텍처
- Spring
- 백준
- kkoon9
- 프로그래머스
- Effective Java
- 이팩티브 자바
- C++
- Olympiad
- 테라폼
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |