본문 바로가기

Computer Science/Design Pattern

[게임 프로그래밍 패턴] 15. 서비스 중개자 패턴

서비스 중개자 패턴 (Service Mediator Pattern)

303 : 서비스를 구현한 구체 클래스는 숨긴 채로 어디에서나 서비스에 접근할 수 있게 한다.

서비스 중개자 패턴

서비스는 여러 기능을 추상 인터페이스로 정의한다. 구체 서비스 제공자는 이런 서비스 인터페이스를 상속받아 구현한다. 이와 별도인 서비스 중개자는 서비스 제공자의 실제 자료형과 이를 등록하는 과정은 숨긴채 적절한 서비스 제공자를 찾아 서비스에 대한 접근을 제공한다.

언제 쓸 것인가?

무엇이든지 프로그램 어디에서나 접근할 수 있게 하면 문제가 생기기 쉽다. 접근해야 할 객체가 있다면 필요한 객체를 인수로 넘겨줄 수는 없는지부터 생각해보자. 하지만 객체를 직접 넘기는 방식이 불필요하거나 코드를 어렵게 만들 경우, 그리고 본질적으로 하나뿐인 시스템인 경우 고려해볼 수 있다.

주의사항

서비스 중개자 패턴에서는 두 코드가 커플링되는 의존성을 런타임 시점까지 미루는 부분이 가장 어렵다.

  1. 서비스가 실제로 등록되어 있어야 한다.
  2. 서비스는 누가 자기를 가져다가 놓는지 모른다.

예제코드

public abstract class Audio {
    public abstract void playSound(int soundId);
    public abstract void stopSound(int soundId);
    public abstract void stopAllSounds();
}
public class ConsoleAudio extends Audio {
    @Override    
    public void playSound(int soundId) {
        System.out.println(soundId + "를 재생합니다.");
    }

    @Override    
    public void stopSound(int soundId) {
        System.out.println(soundId + "를 중단합니다.");
    }

    @Override    
    public void stopAllSounds() {
        System.out.println("모든 소리를 끕니다.");
    }
}
public class Locator {
    private static Audio service;

    public static Audio getAudio() {
        return service;
    }

    public static void provide(Audio audio) {
        service = audio;
    }
}
public class Main {
    public static void main(String[] args) {
        ConsoleAudio audio = new ConsoleAudio();
        Locator.provide(audio);

        Audio thisAudio = Locator.getAudio();
        thisAudio.playSound(VERY_LOUD_BANG);
    }
}
  • 서비스 제공자가 서비스를 등록하기 전에 서비스를 사용하려고 한다면 NULL을 반환하여 Exception이 발생할 수 있다. NULL 서비스 제공자를 정의해서 등록되지 않았을 경우를 대체하는 방법을 사용할 수 있다.

      public class Locator {
          private static Audio service;
          private static NullAudio nullService;
    
          static void initialize() {
              service = nullService;
          }
    
          public static Audio getAudio() {
              return service;
          }
    
          public static void provide(Audio audio) {
              if (audio == null) {
                  service = nullService;
              } else {
                  service = audio;
              }
          }
      }
      public class NullAudio extends Audio {
    
          @Override    
          public void playSound(int soundId) {
              // 아무것도 하지 않는다.    }
    
          @Override    
          public void stopSound(int soundId) {
              // 아무것도 하지 않는다.    }
    
          @Override    
          public void stopAllSounds() {
              // 아무것도 하지 않는다.    }
      }
  • 데커레이터 패턴을 이용한 로그 삽입

      public class LoggedAudio extends Audio {
          private Audio wrapped;
    
          public LoggedAudio(Audio audio) {
              this.wrapped = audio;
          }
          private void log(String log) {
              System.out.println("[LOG] " + log);
          }
    
          @Override    public void playSound(int soundId) {
              log("사운드 출력");
              wrapped.playSound(soundId);
          }
    
          @Override    public void stopSound(int soundId) {
              log("사운드 중지");
              wrapped.stopSound(soundId);
          }
    
          @Override    public void stopAllSounds() {
              log("모든 사운드 중지");
              wrapped.stopAllSounds();
          }
      }

디자인 결정

  1. 서비스는 어떻게 등록되는가?
    1. 외부 코드에서 등록
      - 빠르고 간단하다.
      - 서비스 제공자를 어떻게 만들지 제어할 수 있다.
      - 게임 실행 도중에 서비스를 교체할 수 있다.
      - 서비스 중개자가 외부 코드에 의존한다는 단점이 있다.
    2. 컴파일할 때 바인딩
      - 빠르다.
      - 서비스는 항상 사용 가능하다.
      - 서비스를 쉽게 변경할 수 없다.
    3. 런타임에 설정 값 읽기
      - 다시 컴파일하지 않고도 서비스를 교체할 수 있다.
      - 프로그래머가 아니어도 서비스를 바꿀 수 있다.
      - 등록 과정을 코드에서 완전히 빼냈기 때문에 하나의 코드로 여러 설정을 동시에 지원할 수 있다.
      - 복잡하다.
      - 서비스 등록에 시간이 걸린다는 단점이 있다.
  2. 서비스를 못 찾으면 어떻게 할 것인가?
    1. 사용자가 알아서 처리하게 한다.
      - 실패했을 때 어떻게 처리할지를 사용자 쪽에서 정할 수 있다.
      - 서비스 사용자 쪽에서 실패를 처리해야 한다.
    2. 게임을 멈춘다.
      - 사용자 측에서는 서비스가 없는 경우를 처리하지 않아도 된다.
      - 서비스를 찾지 못하면 게임이 중단된다.
    3. 널 서비스를 반환한다.
      - 외부 코드에서는 서비스가 없는 경우를 처리하지 않아도 된다.
      - 서비스를 사용할 수 없을 때에도 게임을 계속 진행할 수 있다.
  3. 서비스의 범위는 어떻게 잡을 것인가?
    1. 전역에서 접근 가능한 경우
      - 전체 코드에서 같은 서비스를 쓰도록 한다.
      - 언제 어디에서 서비스가 사용되는지를 제어할 수 없다.
    2. 접근이 특정 클래스에 제한되면
      - 커플링을 제어할 수 있다.
      - 중복 작업을 해야 할 수 있다는 단점이 있다.