옵저버 패턴(Observer Pattern)

전홍영·2024년 2월 26일
0

디자인 패턴

목록 보기
5/12
post-thumbnail

옵저버 패턴??

Observer는 관찰자, 감시자라는 뜻이다. 따라서 한 객체를 관찰하고 있다가 객체의 상태가 변경되면 그 객체에 의존하는 다른 객체가 자동으로 갱신된느 방식으로 (1:N) 의존성을 가지고 있다고 있다.

옵저버 패턴에는 구독자와 발행자, 이렇게 2가지의 역할이 있다. 구독자는 발행자를 관찰하고 있다가 발행자에 변경 사항이 발생하면 발행자가 구독자들에게 변경된 사항을 통지하고 구독자들은 통지를 받아 조치를 취하는 행동을 말한다.

이를 우리의 일상 속에 적용해보면, 우리는 유튜브나 신문, 매거진 등의 매체를 구독하고 사용하고 있을 것이다. 옵저버 패턴의 구독자가 바로 이러한 매체를 구독하고 있는 우리라고 생각해보자. 이러한 매체를 구독하고 구독한 매체에서 영상이나 기사를 업로드하면 우리한테 메시지가 오거나 알림이 뜬다. 바로 이 행위가 발행자가 변경된 사항을 통지하는 행위라고 할 수 있다. 따라서 구독한 매체가 변경된 사실을 구독자인 우리에게 변경된 사실을 통지하는 이 구조가 옵저버 패턴이라고 할 수 있다.

구조

옵저버 패턴은 2가지의 역할이 있다고 했다. 발해자와 구독자 이렇게 2가지의 역할로 나눌 수 있다.

발행자

Subject || Observable || Publisher

발행자는 객체의 상태 변경 여부, 상태 변경에 대한 메시지를 발행하는 주체이다.

구독자

Oserver || Subscriber

구독자는 객체의 상태 변경에 대한 자세한 정보를 전달 받는 구독자이자 상태 변경에 대한 정보가 담겨 있는 메시지를 구독한다.

작동원리

작동원리를 알아보자. 발행자(Subject)는 객체의 변경 사항이 생기면 구독자(Observer)에게 변경 사항에 대한 메시지를 발행한다고 했다. 이를 그림으로 보면 아래와 같다.

Subject에 변경이 생기면 update라는 메서드를 통해 구독하고 있는 Observer에게 메시지를 보낸다. 그러면 해당 Observer들은 그에 대응하는 행위를 실행하게 되는 원리이다.

이를 코드로 더 자세히 알아보자.

Subject라는 객체가 있고 Observers라는 옵저버 배열이 있다. 그리고 attach()와 detach() 메서드를 통해 Observer를 추가하고 제거한다.

그리고 변경이 생기면 notify()를 통해 Observer들에게 메시지를 보내고 Observer는 메시지를 받고 update(content) 메서드를 실행하여 행위를 취하게 된다.

예시 코드

순수 자바코드로 된 예시

Subject

public interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    boolean notifyMessage(Message message);
}

ConcreteSubject

public class ConcreteSubject implements Subject {
    private final Set<Observer> observers;

    public ConcreteSubject(Set<Observer> observers) {
        this.observers = observers;
    }

    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public boolean notifyMessage(Message message) {
        return observers.stream()
                .filter(observer -> observer.update(message))
                .count() == observers.size();
    }
}

Observer

public interface Observer<T> {
    boolean update(T message);
}

ConcreteObserver

public class ConcreteObserver implements Observer<ConcreteMessage> {
    @Override
    public boolean update(ConcreteMessage message) {
        System.out.println(message.getMessage());
        return true;
    }
}

Message

public interface Message {
    String getMessage();
}

ConcreteMessage

public class ConcreteMessage implements Message{
    @Override
    public String getMessage() {
        return "발행자가 발행한 메시지입니다.";
    }
}

Test

class ObserverPatternTest {
    @Test
    void 발행자가_구독자에게_메시지를_전달한다() {
        Subject subject = new ConcreteSubject(new HashSet<>());
        Observer observer = new ConcreteObserver();

        ConcreteMessage message = new ConcreteMessage();

        subject.attach(observer);
        subject.notifyMessage(message);
    }
}

ApplicationEventPublisher 사용한 예시

ApplicaitonEvetnPublisher는 ApplicationContext가 상속하는 인터페이스 중 하나이며, 옵저버 패턴의 구현체로 이벤트 프로그래밍에 필요한 기능을 제공해준다.

ApplicationEventPublisher의 예제는 구체적인 상황에 비춰서 예시 코드를 작성했다. User의 정보가 변경 될 때 ApplicationEventPublisher를 통해서 이벤트를 Observer에게 전달한다.

User에 관한 다른 코드는 이 글에서 생략하고 참고링크를 통해 볼 수 있다.

Subject

@Component
@RequiredArgsConstructor
public class UserSubject {
    private final ApplicationEventPublisher applicationEventPublisher;
    private final UserRepository userRepository;

    @Transactional
    public User updateUserInfo(UserInfoUpdateRequest userInfoUpdateRequest, String nickname) {
        User user = userRepository.findByNickname(nickname).orElseThrow();
        User updateUser = user.updateInfo(userInfoUpdateRequest.nickname(), userInfoUpdateRequest.email());
        applicationEventPublisher.publishEvent(userInfoUpdateRequest);
        return updateUser;
    }
}

Observer

@Slf4j
@Component
public class UserObserver {
    @EventListener
    public void updateUserInfo(UserInfoUpdateRequest userInfoUpdateRequest) {
        log.info("유저 정보가 업데이트 되었습니다. {}", userInfoUpdateRequest);
    }
}

Test / Result

@Test
void 발행자가_구독자에게_메시지를_전달한다_2() {
    UserInfoUpdateRequest userInfoUpdateRequest = new UserInfoUpdateRequest("newNickname", "newEmail");
    User updateUserInfo = userSubject.updateUserInfo(userInfoUpdateRequest, "nickname");
    Assertions.assertThat(updateUserInfo.getNickname()).isEqualTo("newNickname");
}
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.nickname 
    from
        user u1_0 
    where
        u1_0.nickname=?
2024-02-26T11:26:44.799+09:00  INFO 60946 --- [    Test worker] c.d.d.o.observer.UserObserver            : 유저 정보가 업데이트 되었습니다. UserInfoUpdateRequest[nickname=newNickname, email=newEmail]
Hibernate: 
    update
        user 
    set
        email=?,
        nickname=? 
    where
        id=?

위의 결과를 보았을 때 사용자의 상태 변경이 적용되어 커밋되기 전에 이벤트가 발생되었음을 볼 수 있다. @EventListner를 사용할 경우, 이벤트를 발생하는 시점에 바로 Listener(Observer)에게 이벤트 알림과 메시지가 전달되기 때문에, 트랜잭션에서 예외가 발생하더라도 옵저버에게 메시지가 전달되는 케이스가 발생할 수 있다.

이를 방지하기 위해서는 @TransactionalEventListener으로 Observer를 등록하면 트랜잭션이 커밋된 이후에 옵저버 이벤트 메시지를 발생하게 할 수 있다.

Observer

@Slf4j
@Component
public class UserObserver {
    @TransactionalEventListener
    public void updateUserInfo(UserInfoUpdateRequest userInfoUpdateRequest) {
        log.info("유저 정보가 업데이트 되었습니다. {}", userInfoUpdateRequest);
    }
}
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.nickname 
    from
        user u1_0 
    where
        u1_0.nickname=?
Hibernate: 
    update
        user 
    set
        email=?,
        nickname=? 
    where
        id=?
2024-02-26T11:35:58.167+09:00  INFO 62072 --- [    Test worker] c.d.d.o.observer.UserObserver            : 유저 정보가 업데이트 되었습니다. UserInfoUpdateRequest[nickname=newNickname, email=newEmail]

update 쿼리 이후에 log가 찍힌 것을 보아 트랜잭션이 커밋된 이후 이벤트가 발생한 것을 볼 수 있다.

결론

옵저버 패턴은 매우 유용한 패턴이라는 생각이 들었다. 내가 많은 개발을 하지는 않았지만 개발을 하다보면 변경 사항이 생기면 이에 대응하는 이벤트가 발생해야 하는 경우가 종종 있었다. 이를 이러한 옵저버 패턴을 적용하여 구현하게 된다면 훨씬 유지 보수하기도 쉽운 코드를 작성할 수 있을 것 같다.

참고 코드 링크

profile
Don't watch the clock; do what it does. Keep going.

0개의 댓글