본문 바로가기
Lecture

자바 디자인 패턴: 플라이웨이트, 프록시, 명령, 해석 패턴

by Renechoi 2023. 7. 6.

 

1.  플라이웨이트 패턴 Flyweight Pattern 

 

 

반복자 패턴이란? 

- 공유해서 쓰는 패턴 

- 인스턴스가 구별되는 이유는 멤버 변수 값이 다르기 때문이다. 

- 규모가 작고 인스턴스마다 특성이 따로 없는 경우 공유해서 사용한다. 

 

 

 

 

의도와 동기 

- 매번 new를 하는 것이 아니라 효율성을 위해 낭비를 절감한다.

- 예를 들어, 각 단어를 각각 표현하기보다는 문자를 공유하여 표현하면 비용이 훨씬적게 소요됨

- 각 객체가 부가적인 상태 (글꼴등...)이 있다면 따로 관리해야 하는 경우가 있음

- 작은 여러개의 객체를 관리해야 할때 주로 사용

 

-> 따로 관리해야 하는 것들이 많다면 이 패턴은 좋지 않다. 

 

클래스 다이어그램 

 

 

- Flyweight

각 객체가 사용할 인터페이스를 정의한다.

 

- CocreteFlyweight

공유될 수 있는 실제적 객체를 구현

 

UnSharedCocreteFlyweight

- 각 인스턴스마다 가지게 되는 부가적인 특성이 있다면 구현한다.

 

FlyweightFactory

- Flyweight에 pool을 관리한다. 각 Flyweight 객체는 Singleton으로 생성한다.

 

 

 

예시 

 

 

점선으로 된 글자를 표기하는 예제를 살펴보자.

 

예를 들어 다음과 같이 숫자를 출력해야 할 때, 이 점숫자를 매번 새롭게 만들지 않아도 된다. 

....######......
..##......##....
..##......##....
..##......##....
..##......##....
..##......##....
....######......
................

 

팩토리에서 인스턴스를 출력할 수 있도록 해준다. 

public class BigString {
    // "큰 문자"의 배열
    private BigChar[] bigchars;
    // 생성자
    public BigString(String string) {
        bigchars = new BigChar[string.length()];
        BigCharFactory factory = BigCharFactory.getInstance();
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i] = factory.getBigChar(string.charAt(i));
        }
    }
    // 표시
    public void print() {
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i].print();
        }
    }
}

 

 

메인에서 호출이 될 때 어떻게 되는지를 살펴보자. 

 

public static void main(String[] args) {

    BigString bs = new BigString("123abc123");
    bs.print();
}

 

호출 될 때마다 해당 숫자를 매번 새롭게 만드는 것이 아니다. 

 

public class BigString {
    // "큰 문자"의 배열
    private BigChar[] bigchars;
    // 생성자
    public BigString(String string) {
        bigchars = new BigChar[string.length()];
        BigCharFactory factory = BigCharFactory.getInstance();
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i] = factory.getBigChar(string.charAt(i));
        }
    }
    // 표시
    public void print() {
        for (int i = 0; i < bigchars.length; i++) {
            bigchars[i].print();
        }
    }
}

 

 

 

결론 

 

- 공유를 통해서 인스턴스의 수를 절약 

 

 

 

 

 

 

 

 

2.  프록시 패턴 Proxy Pattern 

 

 

프록시 패턴이란? 

- 객체에 대한 접근을 제어하기 위한 대리자이다. 

 

 

 

의도와 동기 

- 어떤 객체가 생성 초기에 초기화 비용이 많이 들어간 복잡한 경우, 대리자를 통해 간단하게 한다.

- 실제적으로 비용이 많이 들어가는 처리일 때 객체를 만들도록 한다.

 

종류 

- 원격지 프록시: 서로 다른 주소 공간에 존재하는 객체를 대리하는 로컬 객체

- 가상 프록시: 고비용의 객체는 필요한 경우만 생성

- 보호용 프록시: 실제 객체에 대한 접근 권한을 제어하기 위한 경우 

 

-> 프록시는 기본적으로 Real Object와 동일한 기능을 제공하고, 클라이언트 입장에서 동일한 객체라고 생각하고 사용하게 된다. 

 

 

 

클래스 다이어그램 

- Proxy

실제 참조할 대상을 관리

실제 객체와 동일한 메서드 제공

클라이언트에게 먼저 노출되는 객체

 

- RealSubject

프록시가 대리하는 실제 객체

 

- Subject

Proxy와 Real의 공통 메서드 선언

 

 

예시 

 

간단한 프린트 예제를 살펴보자. 

 

실제 프린트 행위 이외에 다른 정보들을 리턴하는 등의 기능들은 프록시 객체가 해도 무방하다. 이후 실제 프린팅을 하는 기능을 할 때는 실제 객체를 가져와서 하도록 한다. 

 

 

먼저 printable이라는 인터페이스를 다음과 같이 정의한다. 

 

public interface Printable {
    void setPrinterName(String name);   // 이름의 설정
    String getPrinterName();            // 이름의 취득
    void print(String string);          // 문자열 표시(프린트아웃)
}

 

해당 인터페이스를 구현하는 구현체는 다음과 같다. 

 

public class Printer implements Printable {
   private String name;

   public Printer() {
      heavyJob("Printer의 인스턴스를 생성중");
   }

   public Printer(String name) {                   // 생성자
      this.name = name;
      heavyJob("Printer의 인스턴스(" + name + ")를 생성중");
   }

   public void setPrinterName(String name) {       // 이름의 설정
      System.out.println("real : setPrinterName()");
      this.name = name;
   }

   public String getPrinterName() {                // 이름의 취득
      System.out.println("real : getPrinterName()");
      return name;
   }

   public void print(String string) {              // 이름을 붙여서 표시
      System.out.println("=== " + name + " ===");
      System.out.println(string);
   }

   private void heavyJob(String msg) {             // 무거운 작업
      System.out.print(msg);
      for (int i = 0; i < 5; i++) {
         try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
         }
         System.out.print(".");
      }
      System.out.println("완료");
   }
}

 

 

이때 이 프린터에 대한 프록시를 다음과 같이 작성할 수 있다. 

 

public class PrinterProxy implements Printable {
   private String name;            // 이름
   private Printer real;           // "본인"

   public PrinterProxy() {
   }

   public PrinterProxy(String name) {      // 생성자
      this.name = name;
   }

   public synchronized void setPrinterName(String name) {  // 이름의 설정
      if (real != null) {
         real.setPrinterName(name);  // "본인"에게도 설정한다.
      }
      System.out.println("proxy : setPrinterName()");
      this.name = name;
   }

   public String getPrinterName() {    // 이름의 취득
      System.out.println("proxy : getPrinterName()");
      return name;
   }

   public void print(String string) {  // 표시
      realize();
      real.print(string);
   }

   private synchronized void realize() {   // "본인"을 생성
      if (real == null) {
         real = new Printer(name);
      }
   }
}

 

여기서 주목할 점은 다른 메서드에 대해서는 프록시 객체가 가진 메서드로 처리해서 응답을 하지만, 실제 print를 하는 행위에 대해서는 실제 Print객체를 만들어서 작동하도록 한다는 것이다. 

 

public void print(String string) {  // 표시
   realize();
   real.print(string);
}

private synchronized void realize() {   // "본인"을 생성
   if (real == null) {
      real = new Printer(name);
   }
}

 

실제 print가 만들어지는 동안에는 lock이 필요하다.  따라서 동기화 메서드로 생성해주고 프린트 작업을 하게 한다.

 

다음과 같은 예제를 돌려보자. 

 

public static void main(String[] args) {
   Printable print = new PrinterProxy("Alice");
   System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
   print.setPrinterName("Bob");
   System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
   print.print("Hello, world.");

   print.print("test");
   print.setPrinterName("Tomas");
   System.out.println("현재의 이름은" + print.getPrinterName() + "입니다.");
}

 

 

결론 

 

- 프록시 패턴은 객체에 접근하는데 보호, 가상등의 역할을 한다.

 

 

 

 

3.  명령 패턴 Command Pattern 

 

 

명령 패턴이란? 

- 명령을 클래스로 만들어서 사용하는 패턴

- 요청에 대한 프로토콜을 객체로 보낼 수 있는데 이를 Command로 한다. 

-  > 항상 동일한 방식으로 오기 때문에 동일한 메서드로 처리할 수 있다. 

 

 

의도와 동기 

- 요청을 객체로 만들어 전달한다.

- 요청을 히스토리로 관리할 수 있어 롤백도 할 수 있고, 한 꺼번에 실행하게 할 수도 있다. 

- 클라이언트 서버간의 프로토콜로 사용할 수 있다.

- e.g. 메뉴, 프로토콜 

 

 

클래스 다이어그램 

 

- Command

각 명령이 수행할 메서드 선언

 

- CocreteCommand

실제 명령이 호출되도록 execute 구현

 

- Client

ConcreteCommand 객체를 생성하고 처리 객체로 정의

 

- Invoker

Command 처리를 수행할 것을 요청

 

- Receiver

Command를 처리함

 

 

이를 테면 메서드로서 execute를 갖고 모든 커맨드마다 해당 메서드를 실행할 수 있도록 하여 일관성 있게 관리할 수 있다. 

 

 

 

결론 

 

 

 

- 명령 자체를 객체화 하여 여러 다른 객체에 명령이 전달되거나 명령이 조합될 수도 있다.

- 새로운 프로토콜이 추가되기 쉽다.

- 부가적인 정보가 많은 경우는 비효율적일 수 있다.

 

 

 

 

 

 

 

 

4.  해석 패턴 Interpreter Pattern 

 

 

해석 패턴이란? 

- 문법 규칙을 클래스로 표현

- 간단한 프로그램을 해석하기 위한 패턴 

- 게임 같은 데서 용어를 사용하는 문법을 만들려고 하는 경우 사용해볼 수 있다. 

 

-> 일종의 해석기다. 

 

 

 

의도와 동기 

- 간단한 언어에 대한 해석기 패턴
- 각 문법에 대한 해석을 클래스로 표현
- 미니언어나 게임에서 사용하는 간단한 언어에 대한 문법 해석기

 

 

클래스 다이어그램 

- AbstractExpress

추상 구문 트리에 속한 모든 노드가 가져야할 공통의 메서드 interpte() 오퍼레이션 선언

 

- TerminalExpression

터미널 기호에 대한 해석방법을 구현

 

- NonterminalExpression

재귀적으로 호출되어 다른 AbstractExpression으로 구문을 패쓰하게 된다.

 

- Context

번역기에 대한 포괄적인 정보 포함

 

- Client

Syntax를 실제로 정의하고 추상 구문 트리를 생성하고 각 문법 표현 클래스의 interpret() 메서드를 호출한다.

 

예시 

 

특정한 문법이 주어졌을 때 그 언어를 해석하는 해석기를 만들어보자. 

 

자동차를 움직이는 간단한 언어

- 프로그램의 시작: program

- 프로그램의 끝 : end

- 자동차를 움직이게 하는 명령들

go : 앞으로 1미터 전진 right : 오른쪽으로 향함 left : 왼쪽으로 향함 repeat : 반복명령

 

 

 

어떤 명령을 받게 되면 valid를 체크해서 node로 변환 

 

public abstract class Node {
    public abstract void parse(Context context) throws ParseException;
}

 

예를 들어 아래와 같은 명령에 대해서 

// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {
    private Node node;
    public void parse(Context context) throws ParseException {
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }
    public String toString() {
        return node.toString();
    }
}

 

repeat로 시작하면 RepeatCommandNode에게 던진다. 

 

// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {
    private String name;
    public void parse(Context context) throws ParseException {
        name = context.currentToken();
        context.skipToken(name);
        if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
            throw new ParseException(name + " is undefined");
        }
    }
    public String toString() {
        return name;
    }
}

 

위와 같은 방식으로 명령이 들어올 때 각각의 Node로 객체를 던져서 해석을 하게 되는 개념이라고 보면 된다. 

 

Chain Of Respoinsibility와 유사하지만 현재는 문법에 의해서 메시지를 전달한다. 

 

 

 

 

 

 

결론 

 

- 문법을 고안하고 어떻게 해석기를 만드느냐의 문제 

 


참고자료

- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)

반응형