Observer Pattern 이해
Observer Pattern은 객체 간 일대다(one-to-many) 의존성을 정의합니다. 한 객체(Subject)의 상태가 변경되면, 이에 의존하는 모든 객체(Observer)들이 자동으로 알림을 받고 업데이트됩니다. 이 패턴은 게시-구독(Publish-Subscribe) 패턴이나 이벤트 리스너(Event Listener) 패턴이라고도 불립니다. 예를 들어, 유튜브에서 구독자가 채널을 구독하면, 새 동영상이 업로드될 때 알림을 받는 것과 같습니다. ### 핵심 구성 요소 - **Subject (주제)**: Observer를 등록, 해제, 알림하는 기능을 제공합니다. - **Observer (관찰자)**: Subject의 상태 변화를 감지하고 이에 반응하는 update() 메서드를 구현합니다.
Observer Pattern 구현 예제
시나리오: 상품 데이터 변경 시 Redis 캐시 업데이트
전자상거래 프로젝트를 가정해봅시다. 서비스 시작 시 상품(goods), 주문(order) 같은 주요 엔티티를 MySQL에서 조회하여 Redis 캐시에 로드합니다. 데이터 관리 서비스가 DB의 데이터를 추가, 수정, 삭제할 때마다 캐시 계층에 변경을 알려야 합니다. 캐시 계층은 GoodsCache, OrderCache 같은 여러 클래스로 구성됩니다.
Step 1: Subject (주제) 인터페이스
public interface DataChangeSubject {
void attach(DataObserver observer);
void detach(DataObserver observer);
void notifyAllObservers();
}
Step 2: Observer (관찰자) 인터페이스
public interface DataObserver {
void refresh();
}
Step 3: Subject 구현
public class DataManager implements DataChangeSubject {
private List<DataObserver> observers = new ArrayList<>();
@Override
public void attach(DataObserver observer) {
observers.add(observer);
}
@Override
public void detach(DataObserver observer) {
observers.remove(observer);
}
@Override
public void notifyAllObservers() {
for (DataObserver obs : observers) {
obs.refresh();
}
}
public void modifyData() {
System.out.println("DataManager: DB 데이터 수정 완료");
notifyAllObservers();
}
}
Step 4: Observer 구현
public class GoodsCache implements DataObserver {
@Override
public void refresh() {
System.out.println("GoodsCache: DB에서 데이터 조회 후 Redis 업데이트");
}
}
public class OrderCache implements DataObserver {
@Override
public void refresh() {
System.out.println("OrderCache: DB에서 데이터 조회 후 Redis 업데이트");
}
}
Step 5: 클라이언트 코드
public class CacheSyncClient {
public static void main(String[] args) {
DataManager manager = new DataManager();
manager.attach(new GoodsCache());
manager.attach(new OrderCache());
manager.modifyData();
}
}
위 예제는 Observer Pattern의 기본 구조를 보여줍니다. java.util 패키지에는 Observer 인터페이스와 Observable 클래스가 내장되어 있어 더 쉽게 구현할 수 있습니다. 하지만 Subject와 Observer 간 강한 결합이 단점이며, 이를 중개자(Publisher)를 통해 느슨하게 만들 수 있습니다.
Spring Framework에서 Observer Pattern 활용: 이벤트 리스너
Spring은 Observer Pattern의 개념을 이벤트 리스너(Event Listener) 메커니즘으로 확장했습니다. 이 방식은 이벤트(Event), 리스너(Listener), 퍼블리셔(Publisher) 역할을 분리합니다. java.util.EventObject와 java.util.EventListener를 기반으로 Spring의 ApplicationEvent와 ApplicationListener가 동작합니다.
1. 서비스 시작 시 캐시 로드
@Component
public class StartupCacheLoader implements ApplicationListener<ContextRefreshedEvent> {
@Autowired(required = false)
private List<StartupCacheTask> tasks;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null && tasks != null) {
tasks.forEach(StartupCacheTask::execute);
}
}
}
public abstract class StartupCacheTask {
public abstract void execute();
}
2. 데이터 변경 이벤트 및 소스
public class DataChangeEvent extends ApplicationEvent {
public DataChangeEvent(DataChangeSource source) {
super(source);
}
}
public class DataChangeSource {
private String entityType; // 예: "goods", "order"
private String action; // 예: "create", "update", "delete"
// getter, setter 생략
}
3. 리스너(Listener) 구현: 캐시 클래스
@Repository
public class GoodsCache extends StartupCacheTask {
private static final String ENTITY = "goods";
@Override
public void execute() {
System.out.println("서버 시작: 모든 상품 Redis에 로드");
}
@EventListener(DataChangeEvent.class)
public void handleDataChange(DataChangeEvent event) {
DataChangeSource source = (DataChangeSource) event.getSource();
if (ENTITY.equals(source.getEntityType()) && "update".equals(source.getAction())) {
refreshCache();
}
}
private void refreshCache() {
System.out.println("GoodsCache: Redis 캐시 갱신 완료");
}
}
@EventListener 애너테이션은 ApplicationListener 구현보다 간결합니다.
4. 퍼블리셔(Publisher): 서비스 계층
@Service
public class GoodsService {
@Autowired
private ApplicationEventPublisher publisher;
public void modifyGoods() {
System.out.println("GoodsService: DB 상품 정보 수정");
DataChangeSource source = new DataChangeSource();
source.setEntityType("goods");
source.setAction("update");
publisher.publishEvent(new DataChangeEvent(source));
}
}
5. 실행 결과
@SpringBootTest
class CacheSyncTest {
@Test
void testCacheSync() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example");
GoodsService service = context.getBean(GoodsService.class);
service.modifyGoods();
}
}
실행 시 콘솔 출력:
서버 시작: 모든 상품 Redis에 로드
GoodsService: DB 상품 정보 수정
GoodsCache: Redis 캐시 갱신 완료
정리
이 패턴은 Spring Boot에서도 동일하게 적용 가능하며, 데이터 변경 시 캐시를 자동으로 동기화하는 데 효과적입니다.