하리Hari

4. 옵저버 패턴 (Observer)

상태가 변할 때 구독자들에게 자동으로 알려주는 옵저버 패턴을 알아봅니다.

한 줄 요약
어떤 객체에 일이 생겼을 때 그걸 알고 싶은 쪽이 스스로 구독 신청을 해 두고, 객체는 구독자 목록에 있는 모두에게 알리기만 하는 패턴이에요.

문제: 누구에게 연락할지 전부 알고 있는 객체

쇼핑몰에서 품절된 운동화가 다시 들어왔다고 해 볼게요. 이때 할 일이 여럿이에요.

  • 재고 화면의 수량을 새로 고쳐요.
  • 재입고 알림을 신청한 손님들에게 문자를 보내요.

가장 먼저 떠오르는 방법은 상품이 들어올 때 알아야 할 곳에 하나하나 직접 연락하는 거예요.

[재고화면]을 설계하자:
    <새로고침>을 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
        틀"[화면] {'상품'} 남은 수량: {'수량'}"을 출력하자

[문자발송]을 설계하자:
    <보내기>를 만들자 ([문자열]인 '받는사람', [문자열]인 '내용'):
        틀"[문자 → {'받는사람'}] {'내용'}"을 출력하자

(참고: 상품이 들어오면 알아야 할 곳에 하나하나 직접 연락해요)
[상품]을 설계하자:
    '이름'을 [문자열]인 ""으로 정하자
    '수량'을 [숫자]인 0으로 정하자
    '화면'을 [재고화면]인 새로운 [재고화면]()으로 정하자
    '문자'를 [문자발송]인 새로운 [문자발송]()으로 정하자

    처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
        '나'의 '이름'을 '이름'으로 정하자

    <입고하기>를 만들자 ([숫자]인 '개수'):
        '나'의 '수량'을 ('나'의 '수량' + '개수')로 정하자
        '나'의 '화면'의 <새로고침>('나'의 '이름', '나'의 '수량')을 실행하자
        '나'의 '문자'의 <보내기>("민지", 틀"{'나'의 '이름'} 재입고 알림!")를 실행하자
        '나'의 '문자'의 <보내기>("서준", 틀"{'나'의 '이름'} 재입고 알림!")를 실행하자

'운동화'를 [상품]인 새로운 [상품]("운동화")로 정하자
'운동화'의 <입고하기>(5)를 실행하자

실행 결과
[화면] 운동화 남은 수량: 5
[문자 → 민지] 운동화 재입고 알림!
[문자 → 서준] 운동화 재입고 알림!

잘 돌아가요. 그런데 쇼핑몰이 커지면 아파져요.

  • 상품이 연락할 곳을 전부 알아야 해요. [상품]은 “수량이 늘었다”만 알면 되는 객체인데, [재고화면]과 [문자발송]을 직접 들고 있고 손님 이름까지 외우고 있어요.
  • 새 손님이 알림을 신청하려면 코드를 고쳐야 해요. 하은이가 알림을 신청하면 <입고하기>에 줄을 하나 더 넣어야 해요. 프로그램이 돌아가는 중에는 신청을 받을 방법이 없어요.
  • 알림을 끌 수도 없어요. 서준이가 “이제 알림 안 받을래요”라고 해도, 코드에서 그 줄을 지우는 수밖에 없어요.
  • 새로 알려야 할 곳이 생길 때마다 상품이 커져요. “관리자에게 기록 남기기”, “추천 목록 갱신”이 생길 때마다 [상품]에 새 객체와 새 줄이 붙어요.

해결책: 구독 신청을 받고, 알리기만 하기

옵저버 패턴은 연락하는 방향을 바꿔요. 상품이 누구에게 연락할지 정하는 게 아니라, 알고 싶은 쪽이 신청해요.

  1. 소식을 받고 싶은 쪽이 지킬 약속, 곧 규정을 만들어요. 구독자라면 <소식받기>를 할 줄 알아야 해요.
  2. 상품은 구독자 목록만 들고 있어요. <구독하기>로 목록에 넣고, <구독취소>로 목록에서 빼요.
  3. 일이 생기면 상품은 목록에 있는 모두에게 <소식받기>를 불러 주기만 해요. 상대가 화면인지 손님인지는 몰라도 돼요.
  4. 소식을 받은 쪽은 저마다 알아서 할 일을 해요. 화면은 새로 고치고, 손님은 문자를 받아요.

비유: 유튜브 구독과 알림

유튜버는 새 영상을 올릴 뿐이에요. 누가 알림을 받을지는 시청자가 정해요. 구독 버튼을 누르면 목록에 들어가고, 알림을 끄면 목록에서 빠져요. 유튜버는 구독자가 몇 명이든, 누가 들어오고 나가든 영상 올리는 방법을 바꿀 필요가 없어요. 시청자 한 명 한 명이 바로 “옵저버(지켜보는 사람)“예요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
구독자 약속 [구독자] (규정) 소식을 받는 쪽이 할 줄 알아야 하는 일을 정해요
소식을 내는 쪽 [상품] 구독자 목록을 관리하고, 일이 생기면 모두에게 알려요
구체적인 구독자 [재고화면], [손님] 소식을 받으면 저마다 할 일을 해요

하리로 만들기

같은 쇼핑몰을 옵저버 패턴으로 다시 만들어 볼게요. 이번에는 중간에 서준이가 알림을 끄는 장면도 넣었어요.

(참고: 1단계 — 소식을 받고 싶은 쪽이 지킬 약속)
[구독자]를 규정하자:
    <소식받기>가 있어야 한다 ([문자열]인 '상품', [숫자]인 '수량')

(참고: 2단계 — 상품은 구독자 목록만 들고 있다가, 일이 생기면 모두에게 알려요)
[상품]을 설계하자:
    '이름'을 [문자열]인 ""으로 정하자
    '수량'을 [숫자]인 0으로 정하자
    '구독자들'을 []로 정하여 숨기자

    처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
        '나'의 '이름'을 '이름'으로 정하자

    <구독하기>를 만들자 ([구독자]인 '누구'):
        '나'의 '구독자들'에 '누구'를 추가하자

    <구독취소>를 만들자 ([구독자]인 '누구'):
        '남은사람'을 []로 정하자
        '나'의 '구독자들'의 '구독자'마다 반복하자:
            만약 ('구독자'가 '누구'와 다르다) 라면:
                '남은사람'에 '구독자'를 추가하자
        '나'의 '구독자들'을 '남은사람'으로 정하자

    <입고하기>를 만들자 ([숫자]인 '개수'):
        '나'의 '수량'을 ('나'의 '수량' + '개수')로 정하자
        '나'의 '구독자들'의 '구독자'마다 반복하자:
            '구독자'의 <소식받기>('나'의 '이름', '나'의 '수량')를 실행하자

(참고: 3단계 — 소식을 받는 쪽은 저마다 할 일을 알아서 해요)
[구독자]를 따르는 [재고화면]을 설계하자:
    <소식받기>를 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
        틀"[화면] {'상품'} 남은 수량: {'수량'}"을 출력하자

[구독자]를 따르는 [손님]을 설계하자:
    '이름'을 [문자열]인 ""으로 정하자

    처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
        '나'의 '이름'을 '이름'으로 정하자

    <소식받기>를 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
        틀"[문자 → {'나'의 '이름'}] {'상품'} 재입고 알림!"를 출력하자

'운동화'를 [상품]인 새로운 [상품]("운동화")로 정하자
'민지'를 [구독자]인 새로운 [손님]("민지")으로 정하자
'서준'을 [구독자]인 새로운 [손님]("서준")으로 정하자

'운동화'의 <구독하기>(새로운 [재고화면]())를 실행하자
'운동화'의 <구독하기>('민지')를 실행하자
'운동화'의 <구독하기>('서준')을 실행하자
'운동화'의 <입고하기>(5)를 실행하자

"(서준이 알림을 껐어요)"를 출력하자
'운동화'의 <구독취소>('서준')을 실행하자
'운동화'의 <입고하기>(3)을 실행하자

실행 결과
[화면] 운동화 남은 수량: 5
[문자 → 민지] 운동화 재입고 알림!
[문자 → 서준] 운동화 재입고 알림!
(서준이 알림을 껐어요)
[화면] 운동화 남은 수량: 8
[문자 → 민지] 운동화 재입고 알림!

처음 입고는 리팩터링 전과 결과가 같아요. 두 번째 입고에서는 서준이가 빠졌는데, [상품]의 코드는 한 줄도 바뀌지 않았어요.

단계별로 다시 보기

  • 1단계 — 약속 만들기. [구독자]는 “소식을 받을 수 있으면 구독자”라는 약속이에요. 상품은 이 약속만 믿고 <소식받기>를 불러요.
  • 2단계 — 목록만 관리하기. [상품]에는 [재고화면]도, 손님 이름도 없어요. '구독자들'이라는 목록 하나뿐이에요. 이 목록은 숨기자로 만들어서, 밖에서는 <구독하기>와 <구독취소>로만 바꿀 수 있어요.
  • 구독 취소는 “빼고 다시 만들기”. <구독취소>는 목록을 돌면서 떠나는 사람이 아닌 구독자만 새 목록에 담아요. ('구독자'가 '누구'와 다르다)는 두 객체가 같은 객체인지를 비교해요. 이름이 같은 다른 손님이 있어도 헷갈리지 않아요.
  • 3단계 — 저마다 알아서. 소식을 받은 [재고화면]과 [손님]은 하는 일이 전혀 달라요. 그래도 상품은 신경 쓰지 않아요.

알리는 순서는 어떻게 정해지나요?
구독한 순서대로예요. '구독자들'은 목록이라서, <구독하기>로 넣은 순서대로 <소식받기>가 불려요. 순서가 중요한 일(예: 화면을 먼저 고치고 문자를 보내기)이 있다면 구독하는 순서를 맞춰 주거나, 아예 순서에 기대지 않게 만드는 게 좋아요.

새 구독자를 더해 보면

“관리자 기록장”도 입고 소식을 받고 싶다고 해 볼게요.

  • 앞의 방식: [상품]에 [기록장] 객체를 넣고, <입고하기>에 줄을 하나 더 넣어야 해요.
  • 옵저버 패턴: [구독자]를 따르는 [기록장]을 만들고, '운동화'의 <구독하기>(새로운 [기록장]())로 신청만 하면 돼요. [상품]은 그대로예요.

직접 해 보기
위 코드에 [기록장]을 더해 보세요. 소식을 받으면 [기록] 운동화 입고, 지금 8개처럼 출력해요. 첫 입고 전에 구독시키고, 결과에 기록이 두 번 나오는지 확인해 보세요. 그다음 하은이라는 손님도 더해서, 두 번째 입고 전에 구독시켜 보세요.

언제 쓰면 좋을까요?

  • 한 객체에 일이 생겼을 때 알아야 할 곳이 여럿이고, 그 수가 바뀔 수 있을 때
  • 누가 소식을 받을지가 실행 중에 정해질 때 (구독 신청, 알림 끄기)
  • 소식을 내는 쪽이 받는 쪽의 구체적인 종류를 몰라도 되게 하고 싶을 때
  • 화면, 알림, 기록처럼 서로 관계없는 일들이 같은 사건에 반응해야 할 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
새 구독자를 더해도 소식을 내는 쪽을 고치지 않아요 누가 소식을 받는지 코드만 봐서는 한눈에 안 보여요
실행 중에 구독하고 취소할 수 있어요 알리는 순서에 기대면 헷갈리기 쉬워요
소식을 내는 쪽과 받는 쪽이 서로 자세히 몰라도 돼요 구독자가 많으면 한 번 알릴 때 할 일이 많아져요
구독자가 또 소식을 내면 연쇄적으로 이어져서 따라가기 어려울 수 있어요

너무 이른 패턴은 독이에요.
알려야 할 곳이 딱 하나이고 바뀔 일도 없다면, 그 객체를 바로 부르는 게 더 읽기 쉬워요. 알릴 곳이 늘어나거나, 실행 중에 신청과 취소를 받아야 할 때 옵저버로 바꿔도 늦지 않아요.

다른 패턴과의 관계

  • 전략 패턴과 모양이 비슷해요. 둘 다 “규정을 따르는 객체에게 일을 맡겨요”. 전략은 하나를 골라 끼워 일을 맡기고, 옵저버는 목록에 있는 모두에게 소식을 알려요.
  • 커맨드 패턴과 함께 쓸 수 있어요. 구독자 대신 “할 일” 커맨드 객체를 목록에 담아 두고, 일이 생기면 모두 실행하게 만들 수도 있어요.
  • 싱글톤 패턴과 자주 만나요. 프로그램 전체의 소식(로그인, 설정 변경)을 내는 객체는 하나만 있어야 해서, 싱글톤으로 만들고 여러 곳에서 구독하는 경우가 많아요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.