Observer Pattern 활용한 캐시 동기화 설계 가이드

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.EventObjectjava.util.EventListener를 기반으로 Spring의 ApplicationEventApplicationListener가 동작합니다.

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에서도 동일하게 적용 가능하며, 데이터 변경 시 캐시를 자동으로 동기화하는 데 효과적입니다.

태그: Observer Pattern Event Listener Spring Events Redis Cache Data Synchronization

7월 22일 20:35에 게시됨