하리Hari

17. 중재자 패턴 (Mediator)

서로 얽힌 객체들이 직접 부르지 않고 중재자 한 곳을 거쳐 함께 일하게 하는 중재자 패턴을 알아봅니다.

한 줄 요약
서로 부르던 객체들이 서로를 모르게 하고, 무슨 일이 생기면 중재자 한 곳에만 알리게 하는 패턴이에요. 누가 무엇을 할지는 중재자가 정해요.

문제: 기기들이 서로를 직접 부르는 코드

스마트홈을 만든다고 해 볼게요. 기기마다 정해진 규칙이 있어요.

  • 알람이 울리면 커피머신이 커피를 내리고 커튼이 열려요.
  • TV를 켜면 조명이 어두워져요.
  • 외출 버튼을 누르면 TV와 조명이 꺼지고 커튼이 닫혀요.

가장 먼저 떠오르는 방법은 기기가 자기 다음에 움직일 기기들을 직접 들고 있다가 부르는 거예요.

[커피머신]을 설계하자:
    <내리기>를 만들자 ():
        "· 커피머신: 커피를 내려요"를 출력하자

[커튼]을 설계하자:
    <열기>를 만들자 ():
        "· 커튼: 열려요"를 출력하자

    <닫기>를 만들자 ():
        "· 커튼: 닫혀요"를 출력하자

[조명]을 설계하자:
    <밝기>를 만들자 ([숫자]인 '정도'):
        틀"· 조명: 밝기 {'정도'}"을 출력하자

(참고: 기기들이 서로를 직접 알아요. TV는 조명을 알아요)
[TV]를 설계하자:
    '조명'을 비어있음으로 정하자

    처음 만들어질 때 ('조명') 다음과 같이 하자:
        '나'의 '조명'을 '조명'으로 정하자

    <켜기>를 만들자 ():
        "TV: 켜졌어요"를 출력하자
        '나'의 '조명'의 <밝기>(30)을 실행하자

    <끄기>를 만들자 ():
        "· TV: 꺼졌어요"를 출력하자

(참고: 알람은 커피머신과 커튼을 알아요)
[알람]을 설계하자:
    '커피머신'을 비어있음으로 정하자
    '커튼'을 비어있음으로 정하자

    처음 만들어질 때 ('커피머신', '커튼') 다음과 같이 하자:
        '나'의 '커피머신'을 '커피머신'으로 정하자
        '나'의 '커튼'을 '커튼'으로 정하자

    <울리기>를 만들자 ():
        "알람: 따르릉!"을 출력하자
        '나'의 '커피머신'의 <내리기>()를 실행하자
        '나'의 '커튼'의 <열기>()를 실행하자

(참고: 외출 버튼은 TV, 조명, 커튼을 알아요)
[외출버튼]을 설계하자:
    'TV'를 비어있음으로 정하자
    '조명'을 비어있음으로 정하자
    '커튼'을 비어있음으로 정하자

    처음 만들어질 때 ('TV', '조명', '커튼') 다음과 같이 하자:
        '나'의 'TV'를 'TV'로 정하자
        '나'의 '조명'을 '조명'으로 정하자
        '나'의 '커튼'을 '커튼'으로 정하자

    <누르기>를 만들자 ():
        "외출 버튼: 눌렸어요"를 출력하자
        '나'의 'TV'의 <끄기>()를 실행하자
        '나'의 '조명'의 <밝기>(0)을 실행하자
        '나'의 '커튼'의 <닫기>()를 실행하자

'커피머신'을 새로운 [커피머신]()으로 정하자
'커튼'을 새로운 [커튼]()으로 정하자
'조명'을 새로운 [조명]()으로 정하자
'TV'를 새로운 [TV]('조명')로 정하자
'알람'을 새로운 [알람]('커피머신', '커튼')으로 정하자
'외출버튼'을 새로운 [외출버튼]('TV', '조명', '커튼')으로 정하자

'알람'의 <울리기>()를 실행하자
'TV'의 <켜기>()를 실행하자
'외출버튼'의 <누르기>()를 실행하자

실행 결과
알람: 따르릉!
· 커피머신: 커피를 내려요
· 커튼: 열려요
TV: 켜졌어요
· 조명: 밝기 30
외출 버튼: 눌렸어요
· TV: 꺼졌어요
· 조명: 밝기 0
· 커튼: 닫혀요

잘 돌아가요. 그런데 기기와 규칙이 늘어나면 아파져요.

  • 기기들이 그물처럼 얽혀 있어요. TV는 조명을, 알람은 커피머신과 커튼을, 외출 버튼은 TV와 조명과 커튼을 알아요. 기기가 늘수록 줄이 거미줄처럼 늘어나요.
  • 기기를 다른 집에 가져갈 수 없어요. [알람]은 만들 때부터 커피머신과 커튼을 받아야 해요. 커피머신이 없는 집에서는 알람 하나 쓰기도 어려워요.
  • 규칙을 바꾸려면 기기를 고쳐야 해요. “TV를 켜면 커튼도 닫기”가 생기면 [TV]가 커튼까지 알아야 해요. TV 클래스, TV의 생성자, TV를 만드는 줄을 모두 고쳐야 하죠. TV는 TV일 뿐인데요.
  • 집 전체의 규칙이 한눈에 보이지 않아요. 아침에 무슨 일이 일어나는지 알려면 [알람]을, 외출할 때 일을 알려면 [외출버튼]을 열어 봐야 해요. 규칙이 기기들 속에 흩어져 있어요.

해결책: 기기들 사이에 중재자 하나 두기

중재자 패턴은 기기들 사이의 줄을 모두 끊고, 가운데에 중재자를 하나 둬요.

  1. 중재자의 약속을 정해요. “무슨 일이 있었는지 전해 받기” 하나면 돼요.
  2. 기기는 중재자 하나만 알아요. 자기 일을 한 뒤 “이런 일이 있었어요”라고 중재자에게 알리기만 해요.
  3. 중재자가 집 전체의 규칙을 알아요. 알림을 받으면 누가 무엇을 할지 정해서 시켜요.
  4. 기기끼리는 서로를 전혀 몰라요.

비유: 공항 관제탑

공항에는 비행기가 수십 대 오가지만, 조종사들이 서로 무전으로 “제가 먼저 내릴게요”라고 정하지 않아요. 모두 관제탑하고만 이야기해요. 관제탑이 활주로 상황을 보고 “A는 내리고, B는 기다리세요”라고 정해 줘요. 비행기는 다른 비행기를 몰라도 되고, 관제탑 하나만 알면 돼요. 관제탑이 바로 중재자예요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
중재자 약속 [중재자] (규정) <알림받기>를 약속해요
구체적인 중재자 [집관리자] 기기들을 알고, 무슨 일이 생기면 누가 무엇을 할지 정해요
기기 뼈대 [기기] 중재자에 연결하고, 중재자에게 알리는 방법을 줘요
기기들 [TV], [알람], [커튼] 등 자기 일만 하고, 일이 생기면 중재자에게 알려요

하리로 만들기

같은 스마트홈을 중재자 패턴으로 다시 만들어 볼게요.

(참고: 1단계 — 중재자의 약속. 기기에게서 "이런 일이 있었어요"를 전해 받아요)
[중재자]를 규정하자:
    <알림받기>가 있어야 한다 ([문자열]인 '일')

(참고: 2단계 — 모든 기기의 뼈대. 기기는 다른 기기를 모르고, 중재자 하나만 알아요)
[기기]를 설계하자:
    '중재자'를 비어있음으로 정하여 숨기자

    <연결하기>를 만들자 ([중재자]인 '중재자'):
        '나'의 '중재자'를 '중재자'로 정하자

    <알리기>를 만들자 ([문자열]인 '일'):
        '나'의 '중재자'의 <알림받기>('일')를 실행하자

[기기]를 바탕으로 [커피머신]을 설계하자:
    <내리기>를 만들자 ():
        "· 커피머신: 커피를 내려요"를 출력하자

[기기]를 바탕으로 [커튼]을 설계하자:
    <열기>를 만들자 ():
        "· 커튼: 열려요"를 출력하자

    <닫기>를 만들자 ():
        "· 커튼: 닫혀요"를 출력하자

[기기]를 바탕으로 [조명]을 설계하자:
    <밝기>를 만들자 ([숫자]인 '정도'):
        틀"· 조명: 밝기 {'정도'}"을 출력하자

(참고: TV, 알람, 외출 버튼은 자기 일을 하고 중재자에게 알리기만 해요. 그다음 일은 몰라요)
[기기]를 바탕으로 [TV]를 설계하자:
    <켜기>를 만들자 ():
        "TV: 켜졌어요"를 출력하자
        '나'의 <알리기>("TV켜짐")을 실행하자

    <끄기>를 만들자 ():
        "· TV: 꺼졌어요"를 출력하자

[기기]를 바탕으로 [알람]을 설계하자:
    <울리기>를 만들자 ():
        "알람: 따르릉!"을 출력하자
        '나'의 <알리기>("알람울림")을 실행하자

[기기]를 바탕으로 [외출버튼]을 설계하자:
    <누르기>를 만들자 ():
        "외출 버튼: 눌렸어요"를 출력하자
        '나'의 <알리기>("외출")을 실행하자

(참고: 3단계 — 집관리자(중재자). "무슨 일이 생기면 누가 무엇을 하는지"가 모두 여기에 모여 있어요)
[중재자]를 따르는 [집관리자]를 설계하자:
    '커피머신'을 비어있음으로 정하여 숨기자
    '커튼'을 비어있음으로 정하여 숨기자
    '조명'을 비어있음으로 정하여 숨기자
    'TV'를 비어있음으로 정하여 숨기자

    처음 만들어질 때 ('커피머신', '커튼', '조명', 'TV') 다음과 같이 하자:
        '나'의 '커피머신'을 '커피머신'으로 정하자
        '나'의 '커튼'을 '커튼'으로 정하자
        '나'의 '조명'을 '조명'으로 정하자
        '나'의 'TV'를 'TV'로 정하자

    <알림받기>를 만들자 ([문자열]인 '일'):
        '일'에 따라 나누자:
            "알람울림" 인 경우:
                '나'의 '커피머신'의 <내리기>()를 실행하자
                '나'의 '커튼'의 <열기>()를 실행하자
            "TV켜짐" 인 경우:
                '나'의 '조명'의 <밝기>(30)을 실행하자
            "외출" 인 경우:
                '나'의 'TV'의 <끄기>()를 실행하자
                '나'의 '조명'의 <밝기>(0)을 실행하자
                '나'의 '커튼'의 <닫기>()를 실행하자

(참고: 4단계 — 기기들을 만들고, 모두 같은 중재자에 연결해요)
'커피머신'을 새로운 [커피머신]()으로 정하자
'커튼'을 새로운 [커튼]()으로 정하자
'조명'을 새로운 [조명]()으로 정하자
'TV'를 새로운 [TV]()로 정하자
'알람'을 새로운 [알람]()으로 정하자
'외출버튼'을 새로운 [외출버튼]()으로 정하자

'관리자'를 새로운 [집관리자]('커피머신', '커튼', '조명', 'TV')로 정하자
'TV'의 <연결하기>('관리자')를 실행하자
'알람'의 <연결하기>('관리자')를 실행하자
'외출버튼'의 <연결하기>('관리자')를 실행하자

'알람'의 <울리기>()를 실행하자
'TV'의 <켜기>()를 실행하자
'외출버튼'의 <누르기>()를 실행하자

실행 결과
알람: 따르릉!
· 커피머신: 커피를 내려요
· 커튼: 열려요
TV: 켜졌어요
· 조명: 밝기 30
외출 버튼: 눌렸어요
· TV: 꺼졌어요
· 조명: 밝기 0
· 커튼: 닫혀요

결과는 리팩터링 전과 한 줄도 다르지 않아요. 하지만 이제 기기들 사이의 줄이 모두 사라졌고, 집의 규칙은 [집관리자] 한 곳에 모여 있어요.

단계별로 다시 보기

  • 1단계 — 중재자의 약속. [중재자]는 <알림받기> 하나만 약속해요. 기기는 상대가 집관리자인지 사무실관리자인지 몰라도, 이 약속만 믿고 알려요.
  • 2단계 — 기기는 중재자만 알아요. [기기]에는 다른 기기가 하나도 없어요. '중재자' 하나와, 거기에 알리는 <알리기>만 있어요. [TV]의 <켜기>는 “TV켜짐”을 알릴 뿐, 그 뒤에 조명이 어두워진다는 것은 몰라요.
  • 3단계 — 규칙은 중재자에게. [집관리자]의 <알림받기>를 보면 집 전체의 규칙이 한눈에 보여요. 알람이 울리면, TV가 켜지면, 외출하면 각각 누가 무엇을 하는지요.
  • 4단계 — 연결하기. 기기들을 만든 뒤 알리는 쪽 기기를 같은 중재자에 연결해요. 기기를 만들 때는 아무것도 넘기지 않아요. 그래서 [알람]은 커피머신이 없는 집에서도 그대로 쓸 수 있어요.

커피머신과 커튼도 [기기]를 바탕으로 했어요
이 예제에서 커피머신, 커튼, 조명은 아직 아무것도 알리지 않아요. 그래도 같은 뼈대를 쓰면, 나중에 “커피가 다 되면 알리기”처럼 알릴 일이 생겼을 때 <알리기>를 바로 쓸 수 있어요.

중재자 자리에는 약속을 따르는 것만 연결할 수 있어서, 엉뚱한 것을 연결하면 그 자리에서 막혀요.

[중재자]를 규정하자:
    <알림받기>가 있어야 한다 ([문자열]인 '일')

[기기]를 설계하자:
    '중재자'를 비어있음으로 정하여 숨기자

    <연결하기>를 만들자 ([중재자]인 '중재자'):
        '나'의 '중재자'를 '중재자'로 정하자

'TV'를 새로운 [기기]()로 정하자

(참고: 중재자의 약속을 따르지 않는 것에는 연결할 수 없어요)
'TV'의 <연결하기>("거실 리모컨")을 실행하자

새 규칙을 더해 보면

“TV를 켜면 커튼도 닫기”가 생겼다고 해 볼게요.

  • 앞의 방식: [TV]가 커튼을 알아야 해요. TV 클래스에 '커튼'을 더하고, 생성자를 고치고, TV를 만드는 줄에도 커튼을 넘겨야 해요.
  • 중재자 패턴: [집관리자]의 "TV켜짐" 인 경우:에 '나'의 '커튼'의 <닫기>()를 실행하자 한 줄만 더하면 돼요. [TV]는 그대로예요.

직접 해 보기

  1. 위의 “TV를 켜면 커튼도 닫기”를 직접 더해 보세요.
  2. 새 기기 [로봇청소기]를 [기기]를 바탕으로 만들고 <청소하기>를 줘 보세요. 외출 버튼을 누르면 청소를 시작하게 하려면, 로봇청소기 클래스와 [집관리자]만 고치면 돼요. [외출버튼]은 고칠 필요가 없어요.

언제 쓰면 좋을까요?

  • 객체들이 서로 많이 불러서 그물처럼 얽혀 있을 때
  • 객체를 다른 곳에서 다시 쓰고 싶은데, 다른 객체들이 줄줄이 딸려 올 때
  • “무슨 일이 생기면 누가 무엇을 하는지”를 한곳에서 보고 고치고 싶을 때
  • 화면의 입력칸, 버튼, 체크박스처럼 서로 영향을 주는 부품이 많을 때

좋은 점과 아쉬운 점

좋은 점 아쉬운 점
객체들이 서로를 몰라서, 따로 떼어 쓰기 쉬워요 규칙이 모두 모이다 보니 중재자가 점점 커지고 복잡해질 수 있어요
규칙이 한곳에 모여 한눈에 보이고, 고치기도 쉬워요 중재자가 모든 것을 알게 되어, 중재자를 잘못 고치면 집 전체가 흔들려요
새 규칙을 더해도 기기는 그대로예요 한 번 거쳐 가는 만큼, 누가 누구를 움직였는지 따라가기가 조금 멀어져요

너무 이른 패턴은 독이에요.
기기가 둘이고 한쪽이 다른 쪽을 한 번 부르는 정도라면, 직접 부르는 게 더 단순해요. 줄이 서로 얽히기 시작하고, 규칙을 찾으려고 여러 클래스를 열어 보게 될 때 중재자를 꺼내도 늦지 않아요.

다른 패턴과의 관계

  • 옵저버 패턴과 닮았어요. 둘 다 “일이 생기면 알린다”예요. 옵저버는 알림을 받은 쪽이 스스로 무엇을 할지 정하고, 중재자는 중재자가 누구에게 무엇을 시킬지 정해요. 중재자 안에서 옵저버를 써서 알림을 받기도 해요.
  • 퍼사드 패턴과도 비슷해 보여요. 퍼사드는 밖에서 안쪽 여러 객체를 쉽게 쓰도록 앞에 서는 창구고, 안쪽 객체들은 퍼사드를 몰라요. 중재자는 안쪽 객체들이 서로 대신 중재자와 이야기해서, 객체들도 중재자를 알아요.
  • 커맨드 패턴과 함께 쓸 수 있어요. 중재자가 기기에게 시키는 일을 명령 객체로 만들면, 규칙을 바꾸거나 기록하고 되돌리기가 쉬워져요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.