4. 옵저버 패턴 (Observer)
상태가 변할 때 구독자들에게 자동으로 알려주는 옵저버 패턴을 알아봅니다.
한 줄 요약
어떤 객체에 일이 생겼을 때 그걸 알고 싶은 쪽이 스스로 구독 신청을 해 두고, 객체는 구독자 목록에 있는 모두에게 알리기만 하는 패턴이에요.
문제: 누구에게 연락할지 전부 알고 있는 객체
쇼핑몰에서 품절된 운동화가 다시 들어왔다고 해 볼게요. 이때 할 일이 여럿이에요.
- 재고 화면의 수량을 새로 고쳐요.
- 재입고 알림을 신청한 손님들에게 문자를 보내요.
가장 먼저 떠오르는 방법은 상품이 들어올 때 알아야 할 곳에 하나하나 직접 연락하는 거예요.
[재고화면]을 설계하자:
<새로고침>을 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
틀"[화면] {'상품'} 남은 수량: {'수량'}"을 출력하자
[문자발송]을 설계하자:
<보내기>를 만들자 ([문자열]인 '받는사람', [문자열]인 '내용'):
틀"[문자 → {'받는사람'}] {'내용'}"을 출력하자
(참고: 상품이 들어오면 알아야 할 곳에 하나하나 직접 연락해요)
[상품]을 설계하자:
'이름'을 [문자열]인 ""으로 정하자
'수량'을 [숫자]인 0으로 정하자
'화면'을 [재고화면]인 새로운 [재고화면]()으로 정하자
'문자'를 [문자발송]인 새로운 [문자발송]()으로 정하자
처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
<입고하기>를 만들자 ([숫자]인 '개수'):
'나'의 '수량'을 ('나'의 '수량' + '개수')로 정하자
'나'의 '화면'의 <새로고침>('나'의 '이름', '나'의 '수량')을 실행하자
'나'의 '문자'의 <보내기>("민지", 틀"{'나'의 '이름'} 재입고 알림!")를 실행하자
'나'의 '문자'의 <보내기>("서준", 틀"{'나'의 '이름'} 재입고 알림!")를 실행하자
'운동화'를 [상품]인 새로운 [상품]("운동화")로 정하자
'운동화'의 <입고하기>(5)를 실행하자
실행 결과
[화면] 운동화 남은 수량: 5
[문자 → 민지] 운동화 재입고 알림!
[문자 → 서준] 운동화 재입고 알림!
잘 돌아가요. 그런데 쇼핑몰이 커지면 아파져요.
- 상품이 연락할 곳을 전부 알아야 해요.
[상품]은 “수량이 늘었다”만 알면 되는 객체인데,[재고화면]과[문자발송]을 직접 들고 있고 손님 이름까지 외우고 있어요. - 새 손님이 알림을 신청하려면 코드를 고쳐야 해요. 하은이가 알림을 신청하면
<입고하기>에 줄을 하나 더 넣어야 해요. 프로그램이 돌아가는 중에는 신청을 받을 방법이 없어요. - 알림을 끌 수도 없어요. 서준이가 “이제 알림 안 받을래요”라고 해도, 코드에서 그 줄을 지우는 수밖에 없어요.
- 새로 알려야 할 곳이 생길 때마다 상품이 커져요. “관리자에게 기록 남기기”, “추천 목록 갱신”이 생길 때마다
[상품]에 새 객체와 새 줄이 붙어요.
해결책: 구독 신청을 받고, 알리기만 하기
옵저버 패턴은 연락하는 방향을 바꿔요. 상품이 누구에게 연락할지 정하는 게 아니라, 알고 싶은 쪽이 신청해요.
- 소식을 받고 싶은 쪽이 지킬 약속, 곧 규정을 만들어요. 구독자라면
<소식받기>를 할 줄 알아야 해요. - 상품은 구독자 목록만 들고 있어요.
<구독하기>로 목록에 넣고,<구독취소>로 목록에서 빼요. - 일이 생기면 상품은 목록에 있는 모두에게
<소식받기>를 불러 주기만 해요. 상대가 화면인지 손님인지는 몰라도 돼요. - 소식을 받은 쪽은 저마다 알아서 할 일을 해요. 화면은 새로 고치고, 손님은 문자를 받아요.
비유: 유튜브 구독과 알림
유튜버는 새 영상을 올릴 뿐이에요. 누가 알림을 받을지는 시청자가 정해요. 구독 버튼을 누르면 목록에 들어가고, 알림을 끄면 목록에서 빠져요. 유튜버는 구독자가 몇 명이든, 누가 들어오고 나가든 영상 올리는 방법을 바꿀 필요가 없어요. 시청자 한 명 한 명이 바로 “옵저버(지켜보는 사람)“예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 구독자 약속 | [구독자] (규정) |
소식을 받는 쪽이 할 줄 알아야 하는 일을 정해요 |
| 소식을 내는 쪽 | [상품] |
구독자 목록을 관리하고, 일이 생기면 모두에게 알려요 |
| 구체적인 구독자 | [재고화면], [손님] |
소식을 받으면 저마다 할 일을 해요 |
하리로 만들기
같은 쇼핑몰을 옵저버 패턴으로 다시 만들어 볼게요. 이번에는 중간에 서준이가 알림을 끄는 장면도 넣었어요.
(참고: 1단계 — 소식을 받고 싶은 쪽이 지킬 약속)
[구독자]를 규정하자:
<소식받기>가 있어야 한다 ([문자열]인 '상품', [숫자]인 '수량')
(참고: 2단계 — 상품은 구독자 목록만 들고 있다가, 일이 생기면 모두에게 알려요)
[상품]을 설계하자:
'이름'을 [문자열]인 ""으로 정하자
'수량'을 [숫자]인 0으로 정하자
'구독자들'을 []로 정하여 숨기자
처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
<구독하기>를 만들자 ([구독자]인 '누구'):
'나'의 '구독자들'에 '누구'를 추가하자
<구독취소>를 만들자 ([구독자]인 '누구'):
'남은사람'을 []로 정하자
'나'의 '구독자들'의 '구독자'마다 반복하자:
만약 ('구독자'가 '누구'와 다르다) 라면:
'남은사람'에 '구독자'를 추가하자
'나'의 '구독자들'을 '남은사람'으로 정하자
<입고하기>를 만들자 ([숫자]인 '개수'):
'나'의 '수량'을 ('나'의 '수량' + '개수')로 정하자
'나'의 '구독자들'의 '구독자'마다 반복하자:
'구독자'의 <소식받기>('나'의 '이름', '나'의 '수량')를 실행하자
(참고: 3단계 — 소식을 받는 쪽은 저마다 할 일을 알아서 해요)
[구독자]를 따르는 [재고화면]을 설계하자:
<소식받기>를 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
틀"[화면] {'상품'} 남은 수량: {'수량'}"을 출력하자
[구독자]를 따르는 [손님]을 설계하자:
'이름'을 [문자열]인 ""으로 정하자
처음 만들어질 때 ([문자열]인 '이름') 다음과 같이 하자:
'나'의 '이름'을 '이름'으로 정하자
<소식받기>를 만들자 ([문자열]인 '상품', [숫자]인 '수량'):
틀"[문자 → {'나'의 '이름'}] {'상품'} 재입고 알림!"를 출력하자
'운동화'를 [상품]인 새로운 [상품]("운동화")로 정하자
'민지'를 [구독자]인 새로운 [손님]("민지")으로 정하자
'서준'을 [구독자]인 새로운 [손님]("서준")으로 정하자
'운동화'의 <구독하기>(새로운 [재고화면]())를 실행하자
'운동화'의 <구독하기>('민지')를 실행하자
'운동화'의 <구독하기>('서준')을 실행하자
'운동화'의 <입고하기>(5)를 실행하자
"(서준이 알림을 껐어요)"를 출력하자
'운동화'의 <구독취소>('서준')을 실행하자
'운동화'의 <입고하기>(3)을 실행하자
실행 결과
[화면] 운동화 남은 수량: 5
[문자 → 민지] 운동화 재입고 알림!
[문자 → 서준] 운동화 재입고 알림!
(서준이 알림을 껐어요)
[화면] 운동화 남은 수량: 8
[문자 → 민지] 운동화 재입고 알림!
처음 입고는 리팩터링 전과 결과가 같아요. 두 번째 입고에서는 서준이가 빠졌는데, [상품]의 코드는 한 줄도 바뀌지 않았어요.
단계별로 다시 보기
- 1단계 — 약속 만들기.
[구독자]는 “소식을 받을 수 있으면 구독자”라는 약속이에요. 상품은 이 약속만 믿고<소식받기>를 불러요. - 2단계 — 목록만 관리하기.
[상품]에는[재고화면]도, 손님 이름도 없어요.'구독자들'이라는 목록 하나뿐이에요. 이 목록은숨기자로 만들어서, 밖에서는<구독하기>와<구독취소>로만 바꿀 수 있어요. - 구독 취소는 “빼고 다시 만들기”.
<구독취소>는 목록을 돌면서 떠나는 사람이 아닌 구독자만 새 목록에 담아요.('구독자'가 '누구'와 다르다)는 두 객체가 같은 객체인지를 비교해요. 이름이 같은 다른 손님이 있어도 헷갈리지 않아요. - 3단계 — 저마다 알아서. 소식을 받은
[재고화면]과[손님]은 하는 일이 전혀 달라요. 그래도 상품은 신경 쓰지 않아요.
알리는 순서는 어떻게 정해지나요?
구독한 순서대로예요.'구독자들'은 목록이라서,<구독하기>로 넣은 순서대로<소식받기>가 불려요. 순서가 중요한 일(예: 화면을 먼저 고치고 문자를 보내기)이 있다면 구독하는 순서를 맞춰 주거나, 아예 순서에 기대지 않게 만드는 게 좋아요.
새 구독자를 더해 보면
“관리자 기록장”도 입고 소식을 받고 싶다고 해 볼게요.
- 앞의 방식:
[상품]에[기록장]객체를 넣고,<입고하기>에 줄을 하나 더 넣어야 해요. - 옵저버 패턴:
[구독자]를 따르는 [기록장]을 만들고,'운동화'의 <구독하기>(새로운 [기록장]())로 신청만 하면 돼요.[상품]은 그대로예요.
직접 해 보기
위 코드에[기록장]을 더해 보세요. 소식을 받으면[기록] 운동화 입고, 지금 8개처럼 출력해요. 첫 입고 전에 구독시키고, 결과에 기록이 두 번 나오는지 확인해 보세요. 그다음 하은이라는 손님도 더해서, 두 번째 입고 전에 구독시켜 보세요.
언제 쓰면 좋을까요?
- 한 객체에 일이 생겼을 때 알아야 할 곳이 여럿이고, 그 수가 바뀔 수 있을 때
- 누가 소식을 받을지가 실행 중에 정해질 때 (구독 신청, 알림 끄기)
- 소식을 내는 쪽이 받는 쪽의 구체적인 종류를 몰라도 되게 하고 싶을 때
- 화면, 알림, 기록처럼 서로 관계없는 일들이 같은 사건에 반응해야 할 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 새 구독자를 더해도 소식을 내는 쪽을 고치지 않아요 | 누가 소식을 받는지 코드만 봐서는 한눈에 안 보여요 |
| 실행 중에 구독하고 취소할 수 있어요 | 알리는 순서에 기대면 헷갈리기 쉬워요 |
| 소식을 내는 쪽과 받는 쪽이 서로 자세히 몰라도 돼요 | 구독자가 많으면 한 번 알릴 때 할 일이 많아져요 |
| 구독자가 또 소식을 내면 연쇄적으로 이어져서 따라가기 어려울 수 있어요 |
너무 이른 패턴은 독이에요.
알려야 할 곳이 딱 하나이고 바뀔 일도 없다면, 그 객체를 바로 부르는 게 더 읽기 쉬워요. 알릴 곳이 늘어나거나, 실행 중에 신청과 취소를 받아야 할 때 옵저버로 바꿔도 늦지 않아요.