디자인 패턴, 옵저버(Observer Pattern)
소프트웨어를 개발하다 보면 하나의 데이터가 변경되었을 때 여러 기능이 함께 동작해야 하는 경우가 많습니다. 게시글이 등록되면 알림을 보내야 하고, 주문이 완료되면 재고를 차감해야 하며, 회원 정보가 변경되면 로그를 남기거나 통계를 갱신해야 합니다.
소프트웨어를 개발하다 보면 하나의 데이터가 변경되었을 때 여러 기능이 함께 동작해야 하는 경우가 많습니다. 게시글이 등록되면 알림을 보내야 하고, 주문이 완료되면 재고를 차감해야 하며, 회원 정보가 변경되면 로그를 남기거나 통계를 갱신해야 합니다.
이처럼 특정 객체의 상태가 변경되었을 때 연관된 객체들에게 자동으로 변경 사실을 전달하는 패턴을 옵저버(Observer) 패턴이라고 합니다.
옵저버 패턴은 행동(Behavioral) 패턴에 속하며 객체 간의 결합도를 낮추면서도 이벤트 기반 구조를 구현할 수 있다는 특징을 가지고 있습니다.
예를 들어 게시글 작성 기능을 개발한다고 가정해보겠습니다.
class BoardService {
async createPost(postDto: CreatePostDto) {
const post = await this.postRepository.save(postDto);
await this.notificationService.send(post);
await this.logService.write(post);
await this.statisticsService.update(post);
return post;
}
}위 코드처럼 구현하면 게시글 생성 서비스가 알림, 로그, 통계 기능까지 모두 알고 있어야 합니다. 새로운 기능이 추가될 때마다 BoardService를 수정해야 하며 점점 의존성이 증가하게 됩니다.
이러한 문제를 해결하기 위해 옵저버 패턴을 적용할 수 있습니다.
class EventBus {
private observers = [];
subscribe(observer) {
this.observers.push(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}옵저버는 특정 이벤트를 구독하고 있다가 이벤트가 발생하면 자신이 해야 할 작업을 수행합니다.
class NotificationObserver {
update(post) {
console.log('알림 발송');
}
}
class LogObserver {
update(post) {
console.log('로그 저장');
}
}게시글 생성 서비스는 이제 알림이나 로그 기능을 직접 알 필요 없이 이벤트만 발생시키면 됩니다.
class BoardService {
async createPost(postDto) {
const post = await this.postRepository.save(postDto);
eventBus.notify(post);
return post;
}
}실제로 운영했던 사내 인트라넷 프로젝트에서도 비슷한 구조를 적용한 경험이 있습니다. 게시글 등록, 댓글 작성, 쪽지 수신 등의 이벤트가 발생하면 실시간 알림 기능이 동작해야 했습니다. 초기에는 서비스 내부에서 직접 알림 로직을 호출하는 방식으로 개발했지만 기능이 늘어나면서 서비스 간 의존성이 복잡해졌습니다.
이를 해결하기 위해 이벤트 발행 구조를 도입하였고 게시글 작성, 댓글 등록 등의 이벤트가 발생하면 알림 서비스가 해당 이벤트를 구독하여 처리하도록 변경했습니다. 이후에는 이메일 발송이나 로그 적재 기능이 추가되더라도 기존 비즈니스 로직을 수정하지 않고 이벤트 구독자만 추가하면 되었습니다.
NestJS에서도 이러한 구조를 쉽게 구현할 수 있습니다.
@OnEvent('board.created')
handleBoardCreatedEvent(payload: Board) {
this.notificationService.send(payload);
}게시글 생성 시 이벤트만 발행합니다.
this.eventEmitter.emit('board.created', post);이렇게 되면 게시판 서비스는 알림 서비스의 존재를 알 필요가 없으며 새로운 기능이 추가되어도 기존 코드를 수정하지 않아도 됩니다.
옵저버 패턴의 가장 큰 장점은 확장성입니다. 이벤트를 발행하는 객체와 실제 동작을 수행하는 객체를 분리함으로써 유지보수성을 높일 수 있습니다. 또한 실시간 알림, 메시지 큐, 이벤트 기반 아키텍처 등 현대적인 시스템 설계의 기반이 되는 패턴이기도 합니다.
반면 이벤트 흐름이 많아질 경우 어느 시점에 어떤 코드가 실행되는지 추적하기 어려워질 수 있으며 디버깅 난이도가 증가할 수 있습니다. 따라서 이벤트 명명 규칙을 명확하게 정의하고 로그를 함께 관리하는 것이 중요합니다.
대규모 서비스일수록 객체 간 결합도를 낮추는 것이 중요한데, 옵저버 패턴은 이러한 문제를 해결하기 위한 가장 대표적인 디자인 패턴 중 하나입니다. 특히 알림, 로그, 통계, 메시지 발송과 같이 특정 이벤트에 반응하는 기능이 많은 서비스라면 적극적으로 활용해볼 만한 패턴입니다.
코드와 비즈니스, 그 사이에서 배운 것들을 기록하고 공유합니다.