Hi yoahn 개발블로그

디자인 패턴 기말 정리 본문

sswu

디자인 패턴 기말 정리

hi._.0seon 2022. 5. 30. 16:10
반응형

1. Decorator

- 장식물과 내용물을 동일시하기

  • 중심이 되는 객체에, 장식과 같은 부가적인 기능들을 하나식 입혀서 좀 더 목적에 어울리는 객체를 만들자

1.1 예제 프로그램

  • Display 클래스
    • 복수행으로 구성되는 문자열을 표시하기 위한 추상 클래스
    • getColumns()
      가로의 문자 수를 얻기 위한 메소드
    • getRows()
      행의 수를 얻기 위한 메소드
    • getRowText()
      지정한 행의 문자열을 얻기 위한 메소드
    • show()
      모든 행을 화면에 표시하는 메소드
      - TemplateMethod 패턴 적용
더보기
public abstract class Display {
    public abstract int getColumns();               
    public abstract int getRows();                  
    public abstract String getRowText(int row);    
    public void show() {                            
        for (int i = 0; i < getRows(); i++) {
            System.out.println(getRowText(i));
        }
    }
}
  • StringDisplay 클래스
    • 한줄의 문자열을 표시하는 클래스
    • string 필드
      • 표시할 문자열을 저장함
    • getColumns()
      • string.getBytes().length 를 사용하여 문자열이 차지하는 바이트 수를 반환함
    • getRows()
      • 1을 반환
    • getRowText(int row)
      입력 매개변수 row 가 0일 때만 string 필드를 반환한다.
    • 이 클래스는, 여러 케이크의 중심에 있는 스펀지 케이크에 해당함
더보기
public class StringDisplay extends Display {
    private String string;                         
    public StringDisplay(String string) {           
        this.string = string;
    }
    public int getColumns() {                      
        return string.getBytes().length;
    }
    public int getRows() {                         
        return 1;
    }
    public String getRowText(int row) {            
        if (row == 0) {
            return string;
        } else {
            return null;
        }
    }
}
  • Border 클래스
    • '장식'을 나타내는 추상 클래스
    • Display 의 하위 클래스로 정의됨
      • 장식이 내용물과 동일한 메소드를 가진다.
      • 장식과 내용물을 동일시 할 수 있다. : (장식과 내용물을 동일시 하기)
      • 장식이 내용물이 될 수 있다는 것은 장식 클래스를 내용물로 해서 또 다른 장식을 붙일 수 있다는 의미
    • display 필드: Display 형으로 선언됨
      • 장식이 감사고 있는 "내용물"을 가리킨다.
      • 이 필드는 StringDisplay 뿐만 아니라 Border 도 될 수 있다.
        (이유: Border 도 Display 의 하위 클래스 이므로)
      • Decorator, Composite = 동일시 하기, 재귀적 패턴
    • 상위 클래스와 동일한 함수를 제공한다는 것을 표현
    • 생성자의 매개변수로 받은 객체를 활용하여 메소드를 구현
더보기
public abstract class Border extends Display {
    protected Display display;          
    protected Border(Display display) { 
        this.display = display;
    }
}
  • SideBorder 클래스
    • Border 의 하위 클래스
    • 구체적인 장식의 일종
    • 문자열 좌우에 정해진 문자로 장식한다.
    • 생성자에서, 내용물(display)과 장식 문자(ch)가 지정됨
    • getColumns()
      내용물의 문자 수에 2를 더한다
    • getRows()
      내용물의 getRows()를 호출한다.
    • getRowText()
      내용물의 Text 양쪽에 장식 문자를 연결하여 반환한다.
더보기
public class SideBorder extends Border {
    private char borderChar;                        
    public SideBorder(Display display, char ch) {   
        super(display);
        this.borderChar = ch;
    }
    public int getColumns() {                       
        return 1 + display.getColumns() + 1;
    }
    public int getRows() {                         
        return display.getRows();
    }
    public String getRowText(int row) {            
        return borderChar + display.getRowText(row) + borderChar;
    }
}
  • FullBorder 클래스
    • Border 의 하위 클래스
    • 상하좌우에 장식을 한다.
    • SideBorder와 달리, 장식할 문자가 미리 고정되어 있다.
    • makeLine(char ch, int count)
      ch 를 count 갯수 만큼 연속해서 문자열로 만드는 메소드
    • getRowText(int row)
      입력인자 row 가 0이거나 내용물의 전체 줄 수 + 1 과 같으면 문자열 상단 또는 하단에 장식할 문자열을 만든다.
더보기
public class FullBorder extends Border {
    public FullBorder(Display display) {
        super(display);
    }
    public int getColumns() {                   
        return 1 + display.getColumns() + 1;
    }
    public int getRows() {                      
        return 1 + display.getRows() + 1;
    }
    public String getRowText(int row) {        
        if (row == 0) {                 // 첫번째 줄 장식문자
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else if (row == display.getRows() + 1) {     // 마지막 줄 장식문자                
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else {                         가운데 출력할 문자 양 옆에 문자 추가
            return "|" + display.getRowText(row - 1) + "|";
        }
    }
    private String makeLine(char ch, int count) {       
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < count; i++) {
            buf.append(ch);
        }
        return buf.toString();
    }
}

Display
Border
Main
동작 흐름

1.2 등장 역할

  • Component
    • 기능을 추가할 때 핵심이 되는 역할
    • Display 클래스
  • ConcreteComponent
    • Component 역할을 구현한 구체적인 클래스
    • StringDisplay
  • Decorator의 역할
    • Component 역할과 동일한 인터페이스를 가짐
    • 장식자이면서, 장식할 대상이 되기도 한다. (component 의 하위 클래스이면서 멤버변수)
    • Border 클래스
  • Concrete Decorator
    • 구체적인 장식자
    • SideBorder, FullBorder 클래스

  • Border 클래스가 Display 클래스의 하위 클래스
    • 장식을 나타내는 Border 클래스가, 내용물을 나타내는 Display 와 동일한 인터페이스를 가진다.
    • 장식하는 클래스가, 다시 장식의 대상이 될 수 있다.
  • Composite 과 마찬가지로 재귀적인 구조
    • 목적이 서로 다르다.
    • Composite 패턴
      • container 가 다시 내용물이 될 수 있다.
      • Composite 가 Component(Composite의 대상)가 될 수 있다.
    • Decorator 패턴
      • 장식하는 클래스가 다시 장식 대상이 될 수 있다.
      • Decorator 가 Component 가 될 수 있다.
  • 내용물을 변경하지 않고 기능을 추가할 수 있다.
    • 내용물 변경 없이 새로운 장식을 계속해서 부착
    • 포장되는 대상을 변경하지 않고 기능을 추가할 수 있다.
    • Decorator 패턴에서는 위임이 사용된다.
      • SideBorder 의 getColumns() 내에서는 display.getColumns()를 호출한다.
  • 단순한 장식으로도 다양한 기능을 추가할 수 있다.
    • 간단한 구체적인 장식을 많이 준비해두고, 그것들을 자유롭게 조합하여 새로운 장식을 만들 수 있다.
    • 단점: 작은 객체를 여러개 만들어야 함 -> 메모리에 부담
  • java.io 패키지와 Decorator 패턴
    • 입출력 관련 패키지 java.io 에, Decorator 패턴이 사용됨
      Reader = Display, FileReader(ConcreteComponent), BufferedReader(
      • 파일로부터 데이터를 읽어들일 때
        Reader reader = new FileReader("datafile.txt")
      • 버퍼링 기능 추가
        Reader reader = new BufferedReader(new FileReader("datafile.txt"));
      • 생성자 밖에 계속 새로운 생성자가 추가됨
      • 장식은 아니지만, 컴포넌트가 다시 새로운 데코레이터가 됨
      • Composite == 컨테이너가 컨테이너 안에 들어감
      • 줄번호 관리 기능 추가
        Reader reader = new LineNumberReader(new BufferedReader(new FileReader("datafile.txt")));
      • 줄 번호 관리 - 버퍼링 실행 안함
        Reader reader = new LineNumberReader(new FileReader("datafile.txt"));
      • 또 다른 예시
        Reader reader = new LineNumberReader(new BufferedReader( new InputStreamReader( socker.getInputStream()));
    • javax.swing.border 패키지에도, 화면에 표시되는 컴포넌트에 추가할 수 있는 장식용 클래스들이 모여있다.
      -> Border를 씌운 컴포넌트에 다시 Border를 씌울 수 있다.
  • Decorator 패턴을 사용하면, 유사한 작은 클래스들이 많아지는 단점이 있다.
    • 이유: 하나의 큰 장식 대신, 작은 장식자들로 나누어지므로

1.3 보강

상속 - 하위 클래스와 상위 클래스의 동일시

  • Child 인스턴스를 Parent 형 변수에 그대로 대입하고, Parent로부터 상속받은 메소드를 그대로 불러낼 수 있다. 하위 클래스를 상위 클래스로 간주
  • 상위 클래스를 하위 클래스로 간주: 하위 클래스의 메소드를 불러내기 위해서는 type cast 가 필요함
    Parent obj = new Child();
    ((Child) obj).childMethod();
  • Parent 객체는 Child 객체로 형변환 할 수 없음

위임 - 자신과 위임할 곳을 동일시

  • Rose 와 Violet 이 똑같은 메소드를 가지고 있고, Rose는 Violet에게 위임한다.

  • '공통' 이란 정보가 보이지 않음 연결되어 있다는 정보를 알 수 없음
  • Rose 와 Violet 이 똑같은 메소드인 method를 제공한다는 조건이 소스 코드에 표현되지 않음
  • Flower라는 공통 상위 클래스를 정의하면, '공통 정보 공유'가 명확해진다.

Flower 공통 상위 클래스/인터페이스 정의

1.4 연습문제

1) UpDownBorder 클래스

public class UpDownBorder extends Border {
    private char borderChar;
    public UpDownBorder(Display display, char ch) {
        super(display);
        this.borderChar = ch;
    }
    public int getColumns() {
        return display.getColumns();
    }
    public int getRows() {
        return 1 + display.getRows() + 1;
    }
    public String getRowText(int row) {
        if (row == 0 || row == display.getRows() + 1) {
            // display.getRows() -> 장식 전의 문자열
            return makeLine(borderChar, getColumns());
        } else {
            return display.getRowText(row - 1);
        }
    }
    private String makeLine(char ch, int count) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < count; i++) {
            buf.append(ch);
        }
        return buf.toString();
    }
}

2) MultiStringDisplay 클래스 (여러줄의 문자열 표시)

public class MultiStringDisplay extends Display {
    private ArrayList<String> body = new ArrayList();
    private int columns = 0;
    public void add(String msg) {
        body.add(msg);
        updateColumn(msg);
    }
    public int getColumns() {
        return columns;
    }
    public int getRows() {
        return body.size();
    }
    public String getRowText(int row) {
        return (String)body.get(row);
    }
    private void updateColumn(String msg) {
        if (msg.getBytes().length > columns) {  // 문자열들 중 길이가 가장 긴 값을 지정
            columns = msg.getBytes().length;
        }
        for (int row = 0; row < body.size(); row++) {
            int fills = columns - ((String)body.get(row)).getBytes().length;
            if (fills > 0) {
                body.set(row, body.get(row) + spaces(fills));
            }
        }
    }
    private String spaces(int count) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < count; i++) {
            buf.append(' ');
        }
        return buf.toString();
    }
}

 

2. Visitor

"데이터 구조 안을 돌아다니면서 처리를 수행하는 Visitor 패턴"

  • 데이터 구조 안에 저장되어 있는 많은 요소에 대해서, 각 요소에 대해 무언가 "처리"해 나가고자 한다.
    • 데이터 구조를 나타내고 있는 클래스 안에 "처리"를 기술해야 한다고 생각하면 X
    • "처리"가 여러 종류라고 한다면
      -> 새로운 처리가 필요해질 때마다, 데이터 구조를 나타내는 클래스를 수정해야 한다. -> 다른 방법 필요
  • 데이터 구조와 처리를 분리
    • 데이터 구조를 돌아다니는 "방문자"를 정의해서, 이 방문자가 "처리"를 담당하도록 함
    • 새로운 처리를 추가하고 싶을 때는 -> 새로운 "방문자" 만든다.
  • 데이터 구조는, 문을 두드리는 "방문자"를 받아들이기만 하면 된다.

방문자가 데이터 처리를 담당

  • 방문자가 방문하는 데이터 구조
    • Composite 패턴의 예제에서 사용된 파일과 디렉토리를 다시 사용
  • 파일과 디렉토리로 구성된 데이터 구조를 방문자가 방문하면서, 파일 리스트를 출력하는 예제

2.1 예제 프로그램

예제 프로그램 구조

코드

Element

  • Element 인터페이스
    • 방문자를 받아들이는 클래스를 위한 인터페이스
    • accept(Visitor v)
      Visitor 타입을 입력 인자로 받아들임
  • Entry 클래스
    • Composite 패턴에 등장했던 File 이나 Directory 를 위한 추상 클래스
    • Element 인터페이스를 구현함
    • add()
      Directory 클래스에서만 add() 가 유효하므로, Entry 클래스에서는 일단 에러로 처리한다.
    • iterator()
      - 요소에 대한 Iterator를 얻을 때 호출되는 메소드
      - Directory 클래스에서만 iterator() 가 유효하므로, Entry 클래스에서는 일단 에러로 처리한다.
      (ArrayList의 iterator() 호출)
  • File 클래스
    • accept(Visitor v)
      • 클라이언트가 File 객체에게 "방문자를 받아들이세요" 라고 요청할 때 호출하는 메소드
        (클라이언트는 Visitor 를 매개변수로 하여 accept(v)를 호출할 것
      • 입력 인자로 들어온 방문자의 visit 메소드를 호출한다.
        - 이때, 현재 자신 객체를 인자로 하여 호출
        - Visitor의 v.visit(File)메소드가 실행된다.
        - 방문자가 방문하면 방문자에게 "나는 File 객체입니다. 나의 일을 처리해주세요"라고 요청하는 것과 비슷하다.

Directory

  • Directory 클래스
    • iterator()
      디렉토리가 유지하고 있는 엔트리들에 대한 Iterator 를 반환
    • accept(Visitor)
      - 입력 인자로 들어온 Visitor에게, 자기 자신을 매개 변수로 해서 v.visit()을 호출한다.
      - 그러면, Visitor 의 visit(Directory) 메소드가 실행된다.
        -> 방문자가 방문하면 방문자에게 "나는 Directory 객체입니다. 나의 일을 처리해 주세요"라고 요청하는 것과 비슷하다.
  • FileTreatmentException 클래스
    • File 엔트리에 무엇인가 추가 하고자 할 때 발생되는 예외

  • Visitor 클래스
    • 방문자를 나타내는 추상 클래스
    • visit(File)
      File 을 방문했을 때 File 클래스가 호출하는 메소드
    • visit(Directory)
      Directory 를 방문했을 때 Directory클래스가 호출하는 메소드
    • 메소드 오버로드 이용
      -> visit 메소드의 이름은 같은데, 인자의 종류가 다르다.
  • ListVisitor 클래스
    • Visitor 클래스의 하위 클래스
    • 실제 데이터구조(File, Directory) 를 옮겨 다니면서, 리스트를 출력하는 일을 한다.
    • currentdir 필드
      • 현재 주목하고 있는 디렉토리 명을 저장함
    • visit(File)
      • 입력 인자로 받아들인 "File 에 대해 수행해야 할 처리" 가 기술되어 있다.
      • 알고리즘
        • 현재 디렉토리와 File의 toString() 반환 값을 연결하여 현재 파일의 전체 경로를 출력한다.
    • visit(Directory)
      • 입력 인자로 받아들인 "Directory 에 대해 수행해야 할 처리"가 기술되어 있다.
      • 알고리즘
        • 먼저, 디렉토리 전체 경로 및 크기를 출력한다.
        • 현재 디렉토리를 임시로 savedir에 저장한다.
        • 현재 디렉토리를, 입력 인자로 들어온 디렉토리로 변경한다. (방문할 디렉토리)
        • 입력 인자로 들어온 디렉토리의 이터레이터를 얻는다.
        • 입력 인자로 들어온 디렉토리가 가지는 원소들을 차례로 방문하면서, accept(this)를 호출하여 방문자가 방문했음을 알린다.
        • while 루프가 끝나면, currentdir을 원래 디렉토리로 복귀시킨다
          -> 계속 자기 자신을 넘겨주기 때문에, 값이 변경되면 전체 값에 영향
      • 복잡한 재귀적인 호출
        • accept메소드는 visit 메소드를 호출하고, visit 메소드는 accept 메소드를 호출한다.
        • accept() { visit() -> accept() }
package Sample;
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());              

            System.out.println("");
            System.out.println("Making user entries...");
            Directory Kim = new Directory("Kim");
            Directory Lee = new Directory("Lee");
            Directory Park = new Directory("Park");
            usrdir.add(Kim);
            usrdir.add(Lee);
            usrdir.add(Park);
            Kim.add(new File("diary.html", 100));
            Kim.add(new File("Composite.java", 200));
            Lee.add(new File("memo.tex", 300));
            Park.add(new File("game.doc", 400));
            Park.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());              
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}
  • Main 클래스
    • Composite 패턴
      • main() 메소드가 엔트리 리스트를 출력하기 위해서 rootdir.printList() 를 호출
      • 엔트리의 내용을 출력하는 책임을, File 이나 Directory 클래스가 가지고 있다.
    • Visitor 패턴
      • main() 메소드가 엔트리 리스트를 출력하기 위해서, rootdir.accept(new ListVisitor()) 를 호출하였다.
      • 엔트리의 내용을 출력하는 책임을, ListVisitor 클래스가 가지고 있다.

ListVisitor

  • 방문한 곳이 디렉토리이면, 현재 디렉토리 경로 및 크기를 출력하고,
    각 내용물에 대해서 방문자를 받아들이라고 요청한다.
  • 방문한 곳이 파일이면, 현재 디렉토리와 파일 크기를 출력한다.

하나의 Directory 와 두 개의 File 이 있는 경우의 시퀀스 다이어그램

2.2 등장 역할

  • Visitor 의 역할
    • 데이터 구조 내의 각각의 구체적인 요소(ConcreteAcceptor 역할) 에 visit(xxx) 메소드를 선언하는 역할
    • visit(xxx) 메소드는, 실제 xxx 를 처리하기 위한 실제 코드가 하위 클래스에 의해 제공된다.
    • Visitor 클래스
  • ConcreteVisitor의 역할
    • Visitor 역할의 인터페이스를 실제로 구현하는 역할
    • ListVisitor 클래스
  • Acceptor 역할
    • Visitor 역할이 방문할 장소를 나타내는 역할
    • 방문자를 받아들이는 accept(Visitor)메소드를 선언한다.
    • Element 인터페이스
  • ConcreteAcceptor
    • Acceptor 역할의 인터페이스를 구현하는 역할
    • File, Directory 클래스
  • ObjectStructures (객체의 구조)역할
    • Acceptor 역할을 집합으로 취급할 수 있도록 해주는 메소드를 제공한다.
    • 예제에서는 Directory 클래스가 해당됨
      가지고 있는 요소들을 얻어갈 수 있도록, iterator 메소드를 제공함

Visitor 패턴 다이어그램

2.3 힌트

  • 더블 디스패치
    • 디스패치: 급파하다, 발송하다, 신속히 처리하다.
    • Visitor 와 Acceptor는 서로 대응 관계
      ConcreteAcceptor 와 ConcreteVisitor의 역할을 하는 한 쌍에 의해 실제의 처리가 결정된다.

상호 호출 관계

  • 복잡한 일을 하는 이유
    • Visitor 패턴의 목적
      "처리"를 "데이터 구조"로부터 분리하는 것
    • 다른 처리를 하는 ConcreteVisitor를 추가할 수 있다.
    • 기존의 ConcreteVisitor의 기능을 확장하기 쉽다.
    • 부품의 독립성을 높여준다.
  • Open-Closed Principle
    • 확장에 대해서는 열려있고
      (클래스 설계 시 특별한 이유가 없는 한 장래의 확장을 허락해야 한다.)
    • 수정에 대해서는 닫혀있다.
      (확장을 하더라도 기존의 클래스는 수정할 필요가 없어야 한다.)
  • ConcreteVisitor 추가는 간단하지만, ConcreteElement 추가는 어렵다.
    -> Element 구조가 바뀌면 이를 처리하는 모든 Visitor의 내부도 변경되어야 함

2.4 연습문제

1) FileFindVisitor 클래스 추가

public class FileFindVisitor extends Visitor {
    private String filetype;
    private ArrayList found = new ArrayList();
    public FileFindVisitor(String filetype) {
        this.filetype = filetype;
    }
    public Iterator getFoundFiles() {
        return found.iterator();
    }
    public void visit(File file) {
        if (file.getName().endsWith(filetype)) {
            found.add(file);
        }
    }
    public void visit(Directory directory) {
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
    }
}
  • rootdir.accept(FileFindVisitor)
    • visitor 가 방문해서 File 타입인 경우, 파일 확장자가 일치하면 found 리스트에 파일을 추가
  • getFoundFiles() 로 이터레이터를 받아 찾은 파일들을 확인할 수 있다.
  • endsWith()
    특정 문자열을 매개변수로 받아 그 문자열로 끝나는지 여부 반환
  • lastIndexOf()
    해당 함수를 호출한 문자열의 뒤부터 탐색하여 매개변수로 받은 문자열이 가장 처음 나오는 인덱스를 반환
  • substring(start_ind, end_ind)
    문자열을 자르는 메소드
  • equals()
    String 값을 비교하는 메소드

2) SizeVisitor 클래스 비교하기

public class SizeVisitor extends Visitor {
    private int size = 0;
    public int getSize() {
        return size;
    }
    public void visit(File file) {
        size += file.getSize();
    }
    public void visit(Directory directory) {
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            entry.accept(this);
        }
    }
}
  • dir이면 내부를 순회하고, File 이면 file 사이즈 추가

Directory

public int getSize() {
    SizeVisitor v = new SizeVisitor();  
    accept(v);                          
    return v.getSize();                 
}
  • getSize() 를 호출하면 size가 계산되도록 수정
  • 해당 파일사이즈 / 디렉토리 하위에 포함되는 파일 크기의 합이 저장됨

3) ElementArrayList 클래스 작성

ArrayList에 Element 인터페이스 기능을 가진 클래스

class ElementArrayList extends ArrayList implements Element {
    public void accept(Visitor v) {
        Iterator it = iterator();
        while (it.hasNext()) {
            Element e = (Element)it.next();
            e.accept(v);
        }
    }
}
  • ArrayList 를 상속받았기 때문에 ElementArrayList 는 ArrayList
  • iterator() 함수가 ArrayList 에 있기 때문에 가져와서 순회할 수 있음

4) final 이 선언된 클래스는 하위 클래스를 만들 수 없다.

  • String 클래스
    • Open-Closed Principle 을 위반하고 있다.
    • java 시스템은, String 클래스는 확장이 되지 않는다는 전제하에 스피드 향상이나 메모리 양을 위한 최적화를 실행하고 있기 때문에 위반함

3. Facade 패턴

  • 객체 지향 프로그램은 많은 클래스와 객체들로 이루어지고, 서로 복잡한 관련을 맺고 있다.
    • 이들을 모두 이해하고 제어하기 힘듦
    • 이들을 제어하기 위한 '창구'역할을 담당하는 클래스를 만들자
  • facade: 건물의 정면
  • 복잡한 내부는 숨기고, 높은 레벨의 인터페이스를 외부에 제공한다.

3.1 예제 프로그램

  • 사용자의 웹페이지를 작성하는 프로그램
  • Facade 패턴의 예를 보이기 위해서는 "복잡하게 얽혀 있는 많은 클래스"가 필요
패키지명 이름 해설
Anonymous Main 동작 테스트용 클래스
pagemaker Database 메일 주소로부터 사용자명을 얻는 클래스
HtmlWriter HTML 파일을 작성하는 클래스
PageMaker 메일 주소로부터 사용자의 웹페이지를 작성하는 클래스

클래스 다이어그램

클래스 다이어그램

import pagemaker.PageMaker;

public class Main {
    public static void main(String[] args) {
        PageMaker.makeWelcomePage("youngjin@youngjin.com", "welcome.html");
    }
}

Main

  • pagemaker 패키지의 PageMaker 클래스를 이용해서, youngjin@youngjin.com 를 key 로 한 value를 가지고 "welcome.html"이라는 HTML 문서를 완성한다.
  • Main 클래스는 Database나 HtmlWriter 클래스를 직접 이용하지 않고, high-level API 를 직접 제공하는 PageMaker 의 메소드만을 사용하여 원하는 작업을 한다.
package pagemaker;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class PageMaker {
    private PageMaker() {   
    }
    // 사용자의 이름을 찾기 위한 키값: 메일 주소, 작성할 html 파일 이름
    public static void makeWelcomePage(String mailaddr, String filename) {
        try {
            Properties mailprop = Database.getProperties("maildata"); // 파일 읽어들이기
            String username = mailprop.getProperty(mailaddr); // key: mailaddr
            HtmlWriter writer = new HtmlWriter(new FileWriter(filename)); // html 파일 작성
            writer.title("Welcome to " + username + "'s page!");
            writer.paragraph(username + "의 페이지에 오신걸 환영합니다.");
            writer.paragraph("메일을 기다리고 있습니다.");
            writer.mailto(mailaddr, username);
            writer.close();
            System.out.println(filename + " is created for " + mailaddr + " (" + username + ")");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

PageMaker

  • 퍼싸드 역할
  • Database 클래스와 HtmlWriter 클래스를 조합하여, 지정된 사용자의 웹 페이지를 작성하기 위한 클래스
  • makeWelcomePage(String mailaddr, String filename)
    • Database 클래스
      maildata.txt 파일로부터 속성 집합 읽음 -> 속성 중에서 입력 인자로 들어온 주소를 키값으로 하여 value(이름)를 얻음
    • HtmlWriter 클래스
      입력 인자로 들어온 filename 파일에 HTML 문서를 작성
import java.io.Writer;
import java.io.IOException;

public class HtmlWriter {
    private Writer writer;
    public HtmlWriter(Writer writer) {  
        this.writer = writer;
    }
    public void title(String title) throws IOException {    
        writer.write("<html>");
        writer.write("<head>");
        writer.write("<title>" + title + "</title>");
        writer.write("</head>");
        writer.write("<body>\n");
        writer.write("<h1>" + title + "</h1>\n");
    }
    public void paragraph(String msg) throws IOException {  
        writer.write("<p>" + msg + "</p>\n");
    }
    public void link(String href, String caption) throws IOException {  
        paragraph("<a href=\"" + href + "\">" + caption + "</a>");
    }
    public void mailto(String mailaddr, String username) throws IOException {   
        link("mailto:" + mailaddr, username);
    }
    public void close() throws IOException {  
        writer.write("</body>");
        writer.write("</html>\n");
        writer.close();
    }
}

HtmlWriter

  • 간단한 웹 페이지를 작성하는 클래스
  • 생성자
    Writer 타입의 인스턴스를 받아서 사용
  • title()
    헤더 및 타이틀에 대한 html 태그를 작성하는 메소드
  • paragraph()
    문단을 작성하는 메소드
  • link()
    하이퍼링크를 만드는 메소드
  • mailto()
    메일주소 링크를 만드는 메소드
    link() 호출
  • close()
    HTML 출력 마무리, 끝냄
  • 제약조건
    title() 이 제일 먼저 호출되어야 함
package pagemaker;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Database {
    private Database() {    
    }
    public static Properties getProperties(String dbname) { 
        String filename = dbname + ".txt";
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(filename));
        } catch (IOException e) {
            System.out.println("Warning: " + filename + " is not found.");
        }
        return prop;
    }
}

Database

  • 데이터베이스 명을 지정하면, 그 이름에 해당하는 프로퍼티 파일을 열어서 Properties 를 만드는 클래스
    • properties.load(File)
      key=value 로 파싱해서 데이터 가져오기 -> properties 형태
  • getProperties(String dbname)
    • dbname.txt 파일로부터 여러 속성 값을 읽어 들여 Properties인스턴스를 생성한 후, 이를 리턴하는 메소드
    • Properties extends Hashtable<Object, Object>
      key 와 value 쌍으로 된 속성의 집합을 유지하는 클래스
  • maildata.txt
    속성 저장한 파일
    key=value
    -> 이름 가져오는데 사용

PageMaker 역할

3.2 등장 역할

  • Facade 역할
    • 시스템을 구성하는 많은 역할의 '간단한 창구'가 되는 역할
    • 높은 레벨에서 간단한 인터페이스를 시스템 외부에 제공
    • PageMaker 클래스
  • 서브시스템을 구성하고 있는 역할
    • Facade 역할로부터 호출되는 서브시스템을 구성하는 클래스들
    • Facade 역할을 의식하고 만들지는 않음
    • Database, HtmlWriter 클래스
  • Client
    Facade 패턴을 이용하는 역할
    Main 클래스

3.3 힌트

  • Facade 역할이 하는 일은
    • 복잡한 것을 단순하게 보여준다
    • 내부에서 작동하고 있는 많은 클래스들의 관계나 사용법을 의식하지 않도록 해주는 역할
    • 외부에 보이는 API를 적게 해주고 단순하게 해준다.
      • API 가 적다는 것은 외부와 결합이 약해진다는 의미
      • 이는 패키지를 부품으로 재이용하기 쉽다는 의미
      • 패키지 설계 시에 어떤 클래스를 public 으로 할지 생각
      • pagemaker 클래스의 대표인 Facade) PageMaker 만 public 으로 하고 나머지는 default 접근자로 설정
        -> 외부 패키지에서 접근할 수 없게 막음
  • 재귀적인 Facade 패턴의 적용
    • 여러 패키지들이 있고, 패키지마다 Facade 역할이 정의되어 있다면, 이들 Facade 들에 대한 Facade 역할을 재귀적으로 정의할 수 있다.

 

3.4 예제프로그램 

1) 외부에서 절대로 Database 클래스나 HtmlWriter 클래스를 이용할 수 없도록 하려면,

  • 두 클래스 앞의 public 키워드 없앰
  • deafult 접근자 -> 패키지 내부에서만 접근 가능

 

2) 메일 주소 링크만을 포함하는 파일을 만드는 makeLinkPage() 메소드를 PageMaker 클래스에 추가하기

public static void makeLinkPage(String filename) {          
        try {
            HtmlWriter writer = new HtmlWriter(new FileWriter(filename));
            writer.title("Link page");
            Properties mailprop = Database.getProperties("maildata");
            Enumeration en = mailprop.propertyNames();
            while (en.hasMoreElements()) {
                String mailaddr = (String)en.nextElement();
                String username = mailprop.getProperty(mailaddr, "(unknown)");
                writer.mailto(mailaddr, username);
            }
            writer.close();
            System.out.println(filename + " is created.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • maildata.txt 내용을 이용해서 메일 주소 링크 만을 포함하는 파일을 만드는 makeLinkPage() 메소드를 PageMaker 클래스에 추가
  • properties.propertyNames() => Enumeration 리턴
    키 값이 전부 들어있는 데이터
  • Enumeration -> 모든 키 값들이 들어있음
    • en.hasMoreElements()
      다음 원소가 있는지 체크
    • en.nextElement()
      다음 원소를 반환
  • properties.getProperty("key", "default");
    key: value 가져올 키 값
    deafult: 키값에 해당하는 값이 없으면 사용

4. Observer

  • 관찰자
  • 관찰 대상의 상태가 변하면, 관찰자에게 통지된다.
  • 객체의 상태 변화에 따른 처리를 기술할 때 유용하게 사용된다.

  • 숫자 여러개를 생성하는 객체를 관찰자가 관찰해서, 그 값을 표시하는 프로그램 작성
  • 관찰자의 종류에 따라 표시 방법이 다르다
    • DigitObserver: 값을 숫자로 표시함
    • GraphObserver: 값을 간이 그래프로 표시함

observer

  1. 관찰대상인 RandomNumberGenerator 객체가 난수를 하나 발생,
  2. 등록된 모든 관찰자의 update 메소드를 호출해서 알려줌
이름 해설
Observer 관찰자를 나타내는 인터페이스
NumberGenerator 수를 생성하는 객체를 나타내는 추상 클래스
RandomNumberGenerator 랜덤하게 수를 생성하는 클래스
DigitObserver 숫자로 수를 표시하는 클래스
GraphObserver 간이 그래프로 수를 표시하는 클래스
Main 동작 테스트용 클래스

클래스 다이어그램

4.1 예제

import java.util.ArrayList;
import java.util.Iterator;

public abstract class NumberGenerator {
    private ArrayList observers = new ArrayList();
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    public void deleteObserver(Observer observer) {
        observers.remove(observer);
    }
    public void notifyObservers() {
        Iterator it = observers.iterator();      
        while (it.hasNext()) {                 
            Observer o = (Observer)it.next();  
            o.update(this);                   
        }                                     
    }                                      
    public abstract int getNumber();
    public abstract void execute();
}

NumberGenerator

  • 수를 생성하는 클래스
  • execute(), getNumber() 추상 메소드
  • observers 필드: NumberGenerator 를 관찰하고 있는 Observer 들을 보관하는 필드
  • addObserver(observer)
    Observer 를 추가할 때 호출하는 메소드
  • deleteObserver(Observer)
    Observer를 삭제할 때 호출하는 메소드
  • notifyObservers()
    • Observer 전원에게 "나의 내용이 갱신되었기 때문에 당신의 표시도 갱신해라"고 알려주는 메소드
    • Observer 들의 upddate(this) 메소드를 차례차례 호출한다.
import java.util.Random;

public class RandomNumberGenerator extends NumberGenerator {
    private Random random = new Random();
    private int number;
    public int getNumber() {
        return number;
    }
    public void execute() {
        for (int i = 0; i < 20; i++) {
            number = random.nextInt(50);
            notifyObservers();           
        }
    }
}

RandomNumberGenerator

  • NumberGenerator 의 하위 클래스
  • 난수를 생성한다
    java.util.Random 클래스 이용
  • number 필드: 생성된 난수를 저장하는 변수
  • execute()
    • 0 ~ 49 까지의 난수 20개를 생성하고, 그때마다 notifyObservers 를 호출하여 관찰자들에게 통지한다.
    • Random 클래스의 nextInt() 사용 
public class DigitObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.println("DigitObserver:" + generator.getNumber());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

DigitObserver

  • Observer 인터페이스를 구현한 구체적인 관찰자
  • 관찰한 수를 숫자로 표시함
  • update(NumberGenerator)
    • 인자로 전달된 NumberGenerator의 getNumber를 이용하여 수를 얻어서 화면에 출력
    • 출력한 후, 표시된 모습을 잘 알 수 있도록 Thread.sleep() 을 이용하여 스피드를 늦춘다.
    • Thread.sleep(100)
      (100/1000 = 0.1 초) 동안 CPU 를 사용하지 않고 쉼
public class GraphObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.print("GraphObserver:");
        int count = generator.getNumber();
        for (int i = 0; i < count; i++) {
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

GraphObserver

  • Observer 인터페이스를 구현한 구체적인 관찰자
  • 관찰한 수를 '간단한 그래프'로 표시함
    • 관찰한 숫자 만큼의 '*'를 출력한다.
public class Main {
    public static void main(String[] args) {
        NumberGenerator generator = new RandomNumberGenerator();
        Observer observer1 = new DigitObserver();
        Observer observer2 = new GraphObserver();
        generator.addObserver(observer1);
        generator.addObserver(observer2);
        generator.execute();
    }
}

Main

  • RandomNumberGenerator 인스턴스 1개 생성
  • 관찰자 2개 생성해서 등록
  • execute() 를 사용해서 수를 생성,
    난수가 발생될 때마다 관찰자들에게 통지됨

4.2 등장 역할

  • Subject (관찰 대상자 역할)
    • 관찰되는 쪽을 나타냄
    • Observer 역할을 등록하는 메소드 & 삭제하는 메소드 가짐
    • 현재의 상태를 얻어갈 때 호출하는 메소드도 제공함
    • NumberGenerator 인터페이스가 해당됨
  • ConcreteSubject (구체적인 관찰 대상자 역할)
    • 구체적인 '관찰되는 쪽'을 나타냄
    • 상태가 변하면, 등록된 Observer 역할에게 통보함
    • RandomNumberGenerator
  • Observer (관찰자 역할)
    • 관찰하는 쪽
    • Subject 역할로부터 상태 변화를 통보받는 역할
    • 예제에서는, Observer 인터페이스가 해당됨
      • 통보받을 때, 관찰자의 update() 메소드가 호출
  • ConcreteObserver (구체적인 관찰자 역할)
    • 구체적인 관찰하는 쪽
    • 상태 변화가 관찰 대상으로부터 통보되면 (update() 메소드가 호출되면, 그 메소드 안에서 Subject 역할의 현재 상태(getNumber())를 얻어서 적당한 일을 수행한다.
    • DigitObserver, GraphObserver

4.3 힌트

  • 교환 가능성 - 교체 용이성
    • RandomNumberGenerator 클래스
      • 자신을 관찰하는 Observer 가 실제로 어떤 클래스의 인스턴스인지 모름
    • DigitObserver 클래스
      • 자신이 관찰하고 있는 것이 어떤 인스턴스인지 신경쓰지 않음
      • 관찰 대상이 NumberGenerator 의 하위 클래스이고 getNumber 메소드를 가지고 있다는 것만 알고있다.
    • 관찰자와 관찰 대상을 논리적으로 분리 => 각각 쉽게 교체 가능
    • 확장성 / 교환 가능성이 높아진다.
  • 갱신을 위한 힌트 정보의 취급
    • NumberGenerator 는 update() 메소드를 사용해서 '갱신되었습니다'라고 Observer 에게 통지한다.
      • Observer 클래스의 update() 메소드의 파라미터는 NumberGenerator
      • Observer 는 입력인자로 들어온 NumberGenerator를 이용하여 원하는 정보 추출
        void update(NumberGenerator generator) {
            generator.getNumber() 이용
        }
      • NumberGenerator가, 관찰자가 필요한 정보만 넘겨줄 수도 있다. (시간 줄임)
        void update(NumberGenerator generator, int number) {
            //number
        }
      • 관찰 대상이, 관찰자가 필요한 정보를 넘겨줄 수 있어야 한다는 부담
        void update(int number) {
            // number 이용
        }
  • 실제 동작 형태 => 통지
    • Observer 의 역할이, 사실은 Subject로부터 통지가 오기를 기다리는 수동적인 역할을 한다.
    • 그래서 Observer 패턴을 Publish(발행) - Subscribe(구독) 패턴이라고도 한다.
  • Model / View / Controller (MVC)
    • Smalltalk 언어에서, 하나의 데이터 모델을 여러 형태로 보여주고자 할 때 사용되는 유명한 패턴이다.
    • Model 과 View 의 관계는, Observer 패턴에서 Subject와 Observer 의 역할과 서로 대응된다.
      Model 의 데이터 값에 따라 observer 가 보여주는게 변함
  • public interface Observer
    • void update(Observable o, Object arg)
  • 사용하기 쉽지 않음
    Subject 클래스가 다른 클래스의 하위 클래스인 경우, Observable 의 하위 클래스로 할 수 없다.
    - 다중상속 지원 안함

* Observer: 객체의 상태 변화를 다른 객체에게 통지하는 패턴 *

4.4 연습문제

1)

IncrementalNumberGenerator (연습문제-1)

public class IncrementalNumberGenerator extends NumberGenerator {
    private int number;
    private int end;
    private int inc;
    public IncrementalNumberGenerator(int start, int end, int inc) {
        this.number = start;
        this.end = end;
        this.inc = inc;
    }
    public int getNumber() {
        return number;
    }
    public void execute() {
        while (number < end) {
            notifyObservers();
            number += inc;
        }
    }
}
  • 시작하는 수와 마지막 수 및 증가분을 입력받는 생성자를 가진다.
    10 ~ 50 까지 5 씩 증가
  • 숫자가 만들어질 때마다 관찰자에게 통지한다.

2) 새로운 Observer 추가 - 그래픽 옵저버

FrameObserver (연습문제-2)

더보기
import java.awt.Frame;
import java.awt.TextField;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Button;
import java.awt.Graphics;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JTextField;

import java.awt.event.ActionEvent;

public class JFrameObserver extends JFrame implements Observer, ActionListener {
    private GraphText textGraph = new GraphText(60);
    private GraphCanvas canvasGraph = new GraphCanvas();
    private Button buttonClose = new Button("Close");

    public JFrameObserver() {
        super("FrameObserver");
        setLayout(new BorderLayout());
        setBackground(Color.lightGray);
        textGraph.setEditable(false);
        canvasGraph.setSize(500, 500);
        add(textGraph, BorderLayout.NORTH);
        add(canvasGraph, BorderLayout.CENTER);
        add(buttonClose, BorderLayout.SOUTH);
        buttonClose.addActionListener(this);
        pack(); // 내용에 맞게 창 크기 조절
//        show(); show 는 deprecated 됨
        setVisible(true); // 대체
    }
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        System.exit(0);
    }
    public void update(NumberGenerator generator) {
        textGraph.update(generator);
        canvasGraph.update(generator);
    }
}
class GraphText extends JTextField implements Observer {
    public GraphText(int columns) {
        super(columns);
    }
    public void update(NumberGenerator generator) {
        int number = generator.getNumber();
        String text = number + ":";
        for (int i = 0; i < number; i++) {
            text += '*';
        }
        setText(text);
    }
}
class GraphCanvas extends Canvas implements Observer {
    private int number;
    public void update(NumberGenerator generator) {
        number = generator.getNumber();
        repaint();
    }
    public void paint(Graphics g) {
        int width = getWidth();
        int height = getHeight();
        g.setColor(Color.white);
        g.fillArc(0, 0, width, height, 0, 360);
        g.setColor(Color.red);
        g.fillArc(0, 0, width, height, 90, - number * 360 / 50);
    }
}

 

  • g.fillArc(x, y, width, height, startAngle, arcAngle)
    • startAngle
      시작 각도
      0: 3시 위치
      (반 시계방향으로 증가 -> 90: 12시 위치)
    • arcAngle
      •  양수
        반시계방향
      • 음수
        시계방향
    • -360 * number / 50
      각도 계산

 

5. State

  • 어떤 것을 클래스로 표현할지는 설계하는 사람의 마음
  • State 패턴 => 상태를 클래스로 표현한 것
    • 클래스를 교체함으로써, '상태의 변화'를 나타낼 수 있고
    • 새로운 상태를 추가해야 할 때 무엇을 프로그램하면 되는지 명확해짐
  • 금고 경비 시스템
    • 시각마다 경비 상태가 변화하는 금고 경비 시스템
    • 호출 상황을 화면에 표시한다.
    • 프로그램 상의 1초 == 현실 세계의 1시간으로 가정

State 패턴을 사용한 의사 코드

  • State 패턴을 사용하면, 현재 상태를 체크하기 위한 if 문이 없다.
  • 묻혀 있던 상태를 외부로 끌어냈기 때문에

5.1 예제 프로그램

State 금고의 상태를 나타내는 인터페이스
DayState State 를 구현하고 있는 클래스. 주간의 상태를 나타냄
NightState State 를 구현하고 있는 클래스. 야간의 상태를 나타냄
Context 금고의 상태변화를 관리하고, 경비센터와 연락을 취하는 인터페이스
전체 시스템을 이용하는 인터페이스
SafeFrame Context를 구현하고 있는 클래스
버튼이나 화면 표시 등의 사용자 인터페이스를 가짐
Main 동작 테스트용  클래스

클래스 다이어그램

public interface State {
    public abstract void doClock(Context context, int hour);
    public abstract void doUse(Context context);
    public abstract void doAlarm(Context context);
    public abstract void doPhone(Context context);
}

State

  • 금고의 상태를 나타냄
  • 다음 이벤트가 발생했을 때 호출되는 인터페이스를 제공
    • 시각 설정 => doClock() 을 호출
    • 금고가 사용될 때 => doUse() 를 호출함
    • 비상벨이 울릴 때 => doAlarm() 을 호출
    • 일반 통화를 할 때 => doPhone() 을 호출
  • 각 메소드의 형식 인자 Context
    • 상태를 관리하거나 실제 경비센터를 호출하는 일을 하는 클래스
더보기
public class DayState implements State {
    private static DayState singleton = new DayState();
    private DayState() {
    }
    public static State getInstance() {
        return singleton;
    }
    public void doClock(Context context, int hour) {
        if (hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance());
        }
    }
    public void doUse(Context context) {
        context.recordLog("금고사용(주간)");
    }
    public void doAlarm(Context context) {
        context.callSecurityCenter("비상벨(주간)");
    }
    public void doPhone(Context context) {
        context.callSecurityCenter("일반통화(주간)");
    }
    public String toString() {
        return "[주간]";
    }
}

DayState

  • 주간의 상태를 나타내는 클래스
  • 하나의 상태만 필요, Singleton 패턴 사용
    • DayState 타입의 객체를 static 으로 선언하고 인스턴스 한개 생성
    • 생성자를 private
  • doClock() 시각을 설정하는 메소드
    • 인자에서 제공된 시각이 야간의 시각이면, 시스템의 상태를 야간으로 바꾼다.
  • "주간 상태" 에서 하는 일을 표현하는 메소드 (Context 의 메소드를 이용)
    - doUse() : 주간에 금고를 사용했을 때를 기록
    - doAlarm() : 비상벨로 경비센터를 호출
    - doPhone() : 경비 센터에 일반 통화
더보기
public class NightState implements State {
    private static NightState singleton = new NightState();
    private NightState() {
    }
    public static State getInstance() {
        return singleton;
    }
    public void doClock(Context context, int hour) {
        if (9 <= hour && hour < 17) { // 상태를 변경해야 하는지 시간을 체크
            context.changeState(DayState.getInstance());
        }
    }
    public void doUse(Context context) {
        context.callSecurityCenter("비상: 야간의 금고사용!");
    }
    public void doAlarm(Context context) {
        context.callSecurityCenter("비상벨(야간)");
    }
    public void doPhone(Context context) {
        context.recordLog("야간의 통화녹음");
    }
    public String toString() {
        return "[야간]";
    }
}

NightState 클래스

  • 야간의 상태를 나타내는 클래스
더보기
public interface Context {

    public abstract void setClock(int hour);
    public abstract void changeState(State state);
    public abstract void callSecurityCenter(String msg);
    public abstract void recordLog(String msg);
}

Context 클래스

  • 상태를 관리하거나 경비센터를 실제로 호출하는 클래스를 위한 인터페이스를 제공
더보기
import java.awt.Frame;
import java.awt.Label;
import java.awt.Color;
import java.awt.Button;
import java.awt.TextField;
import java.awt.TextArea;
import java.awt.Panel;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class SafeFrame extends Frame implements ActionListener, Context {
    private TextField textClock = new TextField(60);
    private TextArea textScreen = new TextArea(10, 60);
    private Button buttonUse = new Button("금고사용");
    private Button buttonAlarm = new Button("비상벨");
    private Button buttonPhone = new Button("일반 통화");
    private Button buttonExit = new Button("종료");

    private State state = DayState.getInstance();

    // 생성자
    public SafeFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        setLayout(new BorderLayout());
        // textClock레이아웃 
        add(textClock, BorderLayout.NORTH);
        textClock.setEditable(false);
        // textScreen 레이아웃
        add(textScreen, BorderLayout.CENTER);
        textScreen.setEditable(false);

        Panel panel = new Panel();
        panel.add(buttonUse);
        panel.add(buttonAlarm);
        panel.add(buttonPhone);
        panel.add(buttonExit);

        add(panel, BorderLayout.SOUTH);
        // 패널 사이즈에 창 크기 맞춤
        pack();
        show();
        // listener 등록
        buttonUse.addActionListener(this);
        buttonAlarm.addActionListener(this);
        buttonPhone.addActionListener(this);
        buttonExit.addActionListener(this);
    }
    // ActionEvent
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        if (e.getSource() == buttonUse) {           // 금고 사용
            state.doUse(this);
        } else if (e.getSource() == buttonAlarm) {  // 비상벨
            state.doAlarm(this);
        } else if (e.getSource() == buttonPhone) {  // 일반통화
            state.doPhone(this);
        } else if (e.getSource() == buttonExit) {   // 종료
            System.exit(0);
        } else {
            System.out.println("?");
        }
    }
    // 시간 설정
    public void setClock(int hour) {
        String clockstring = "현재 시간은";
        if (hour < 10) {
            clockstring += "0" + hour + ":00";
        } else {
            clockstring += hour + ":00";
        }
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this, hour);
    }
    // 상태 변경
    public void changeState(State state) {
        System.out.println(this.state + "에서" + state + "로 상태가 변했습니다.");
        this.state = state;
    }
    // 보안센터 전화
    public void callSecurityCenter(String msg) {
        textScreen.append("call! " + msg + "\n");
    }
    // 통화 기록
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}

SafeFrame 클래스

  • GUI 사용해서, 금고 경비 시스템 실현
  • Safe는 금고라는 뜻
  • Context 인터페이스를 구현
  • state 필드: 금고의 현재 상태를 저장하는 변수
    초기값 => 주간
  • 생성자
    • 배경색 설정
    • 레이아웃 매니저의 설정
    • 부품의 배치
    • 리스너의 설정
      addActionListener 메소드를 이용하여 ActionListener 를 구현한 객체를 버튼에 등록
      버튼이 눌려지면 등록된 ActionListener 객체의 actionPerformed() 를 호출
  • 버튼의 ActionListener 는 SafeFrame 자신이다.
  • actionPerformed(ActionEvent e)
    - 눌러진 버튼의 종류에 따라, state.doUse(this) / state.doAlarm(this) / state.doPhone(this) 중 하나를 호출
    - 현재 상태가 주간인지 야간인지 체크하는 코드가 필요하다.
  • setClock()
    시간 설정을 위해 클라이언트가 호출하는 메소드
  • callSecurityCenter()
    경비센터에 대한 호출을 표현함
  • recordLog()
    경비센터의 로그에 기록하는 일을 표현함
더보기
public class Main {
    public static void main(String[] args) {
        SafeFrame frame = new SafeFrame("State Sample");
        while (true) {
            for (int hour = 0; hour < 24; hour++) {
                frame.setClock(hour);   // ����
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}

Main

  • SafeFrame 인스턴스를 한개 만든 후,
  • 1초 간격으로 SafeFrame 의 setClock() 메소드를 호출 -> 시간 업데이트
    -> Thread.sleep(1000) 문장을 사용함

State 시퀀스 다이어그램

5.2 등장 역할

  • State
    • 상태를 나타내는 역할
    • 각 상태에 따라 다른 행동을 하는 통일된 인터페이스를 결정함
    • State 인터페이스
  • ConcreteState(구체적인 상태)의 역할
    • 구체적인 개개의 상태를 표현하는 역할
    • State 역할이 결정한 인터페이스를 구현
    • DayState, NightState
  • Context (상황, 전후관계, 문맥)의 역할
    • 현재의 상태를 나타내는 ConcreteState 역할을 가지고 있음
    • State 패턴 이용자가 필요로 하는 인터페이스를 결정함
    • Context, SafeFrame

5.3 힌트

분할해서 통치하기

  • divide and conquer
  • 복잡하고 규모가 큰 프로그램 => 작은 문제로 나누어서 풀기
  • State 패턴
    -> 개개의 구체적인 상태를 각각 클래스로 나누어서 표현함으로써 문제를 분할함
  • 상태의 종류가 많을수록 유용함
    • State 패턴을 사용하지 않는다면, 계속해서 상태를 검사하는 조건문이 필요하다.
    • 예: 상태가 10가지 -> if - else if 문이 10개

상태에 의존한 처리

  • SafeFrame의 setClock() 과 State 의 doClock() 의 관계
  • Main 클래스가 SafeFrame의 setClock() 메소드를 호출해서 시간을 설정하도록 함
  • setClock() 메소드 안에서는, state.doClock(this, hour) 를 호출하여 현재 state 에 그 처리를 위임함. '현재의 상태에 의존한 처리'로 취급
  • 방법
    • 추상메소드로 선언하고 인터페이스로 함
    • 구상 메소드로 구현하고 각각의 클래스로 함
  • 현재 state 에 따라 행동이 달라지게 됨

새로운 상태를 추가하는 것은 간단

-> 새로운 State 클래스를 만들어서 필요한 메소드를 구현하기만 하면 된다.

 

STD (State Transition Diagram)

시스템의 상태 변화를 표현하기 위해 사용되는 다이어그램

5.4 연습문제

  1. Context 인터페이스 -> SafeFrame 클래스에서 이미 Frame 클래스를 상속받아 다른 클래스는 상속받을 수 없음
  2. 주간, 야간의 범위를 변경하려면
    doClock() 안의 if 조건 변경해야함
  3. 점심 시간 상태 추가
    NoonState 클래스 추가
    -> 모든 클래스들에서 doClock() 범위 수정
public class NoonState implements State {
    private static NoonState singleton = new NoonState();
    private NoonState() {                            // 생성자는 private
    }
    public static State getInstance() {                 // 유일한 인스턴스
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 시간 설정
        if (hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance());
        } else if (9 <= hour && hour < 12 || 13 <= hour && hour < 17) {
            context.changeState(DayState.getInstance());
        }
    }
    public void doUse(Context context) {                // 금고 사용
        context.callSecurityCenter("비상:점심시간 금고사용!");
    }
    public void doAlarm(Context context) {              // 비상벨
        context.callSecurityCenter("비상벨(점심시간)");
    }
    public void doPhone(Context context) {              // 일반통화
        context.recordLog("점심시간 통화녹음");
    }
    public String toString() {
        return "[점심시간]";
    }
}

4. 비상 시 상태 추가
UrgentState 클래스
-> 다른 doAlarm 수정
context.chageState(UrgentState.getInstance()) 추가
-> 비상사태 전환 후, 원래 상태로 복원하는 수단이 없다는 문제점이 있음

public class UrgentState implements State {
    private static UrgentState singleton = new UrgentState();
    private UrgentState() {
    }
    public static State getInstance() {
        return singleton;
    }
    public void doClock(Context context, int hour) {
    	// 아무 처리 하지 않음
    }                                                           
    public void doUse(Context context) {                // 금고사용
        context.callSecurityCenter("비상:비상사태 금고사용!");
    }
    public void doAlarm(Context context) {              // 비상벨
        context.callSecurityCenter("비상벨(비상사태)");
    }
    public void doPhone(Context context) {              // 일반통화
        context.callSecurityCenter("일반통화(비상사태)");
    }
    public String toString() {
        return "[비상사태]";
    }
}

DayState, NightState 의 doAlarm()

public void doAlarm(Context context) {
    context.callSecurityCenter("비상벨(주간)");
    context.changeState(UrgentState.getInstance()); 
}

6. Proxy

  • 대리인
  • 본인을 대신해서 일을 처리하는 사람
  • 퍼포먼스를 위해서 사용함
  • proxy가 초기 작업이나 간단한 작업을 처리함
    복잡하거나 무거운 요청이 오면 실제 객체가 처리함

proxy 처리

6.1 예제 프로그램

이름을 가진 프린터

화면에 문자열을 표시하는 프로그램

Printer 이름이 붙은 프린터를 나타내는 클래스
Printable Printer 와 PrinterProxy 에서 구현하는 인터페이스
PrinterProxy 이름이 붙은 프린터를 나타내는 클래스
Main 동작 테스트용 클래스

클래스 다이어그램

Printable 인터페이스

public interface Printable {
    public abstract void setPrinterName(String name);
    public abstract String getPrinterName();
    public abstract void print(String string);
}
  • PrinterProxy 와 Printer 클래스를 동일시하기 위한 인터페이스
  • Printer 클래스 인스턴스 생성에 많은 시간이 걸림
    • heavyJob -> 5초 걸림

Printer 클래스

더보기
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) {
        this.name = name;
    }
    public String 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("완료");
    }
}
  • '본인'을 나타내는 클래스
  • 생성자: heavyJob(오래 걸리는 작업) 호출
  • setPrinterName() / getPrinterName()
  • print()
    proxy 가 할 수 없는 일
    프린터 이름과 문자열을 화면에 출력
  • heavyJob()
    Thread.sleep(1000) 후 '.' 찍는 일을 5번 수행

PrinterProxy

더보기
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);
        }
        this.name = name;
    }
    public String getPrinterName() {
        return name;
    }
    public void print(String string) {
        realize();
        real.print(string);
    }
    private synchronized void realize() {
        if (real == null) {            
            real = new Printer(name);
        }                           
    }
}
  • Printer 클래스의 대리인을 나타내는 클래스
  • name 필드: 대리인의 이름을 저장함
  • real 필드: '본인'에 대한 참조를 저장함 (원래 객체 참조)
  • setPrintername(): real이 null 이 아니면, 즉, 본인 객체가 이미 생성되어 있으면, 그 객체에도 이름을 설정한다.
  • print()
    realize() 호출 후, '본인' 객체의 print() 메소드를 호출한다. => 위임
  • realize()
    실제 일을 하는 '본인'객체가 생성되지 않았으면 생성한다.
  • Printer는 PrinterProxy의 존재를 모르지만 PrinterProxy는 Printer에 깊이 관여됨
    -> 연관관계가 깊어지면 커플링이 높아져서 유지보수가 어려워짐
  • setPrintname() 과 realize()는 synchronized로 선언됨
    • 멀티스레드 환경에서 동시에 printer name을 변경하거나 real 변수에 새로운 객체를 동시에 대입할 수 있어서

Main 클래스

더보기
public class Main {
    public static void main(String[] args) {
        Printable p = new PrinterProxy("Alice");
        System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
        p.setPrinterName("Bob");
        System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
        p.print("Hello, world.");
    }
}
// 실행 결과
이름은 현재 Alice입니다.
이름은 현재 Bob입니다.
Printer의 인스턴스 (Bob)을 생성 중.....완료
=== Bob ===
Hello, world.
  • PrinterProxy를 경유해서, Printer를 이용하는 클래스
  • 먼저, PrinterProxy 생성 후, 이름을 설정, "hello, world" 출력
  • 이름을 설정하거나 표시되는 동안에는, Printer 클래스의 인스턴스는 생성되지 않음
  • print() 메소드가 호출 될 때, Printer 객체가 생성된다.
    -> 필요할 때 생성됨

-> Printer 객체가 필요한 시점에 생성해서 printer 에게 일을 위임

6.2 등장 역할

  • Subject (주체) 역할
    • Proxy와 RealSubject를 동일시하기 위한 인터페이스를 정의
    • Printable 인터페이스가 해당
  • Proxy(대리인)의 역할
    • Client로부터 요구를 가능한 한 자신이 처리함
    • 혼자서 처리할 수 없을 때, RealSubject에게 위임
    • Proxy 역할은, RealSubject 역할이 정말로 필요해지면 그때 RealSubject역할을 생성
    • PrinterProxy
  • Client (의뢰자)
    • Main
    • Proxy 패턴을 이용하는 역할

6.3 힌트

  • 대리인을 사용해서 스피드업
    • Proxy 역할이 대리인이 됨, 할 수 있는 일은 모두 처리
    • 실제로 무거운 처리는 필요할 때까지 지연시킴 (인스턴스 생성)
    • 초기화하는데 시간이 많이 걸리는 대규모 시스템에서 유용함
      • 기동 시점에서 이용하지 않는 기능까지 전부 초기화하면, 애플리케이션 기동에 시간이 많이 걸림
      • 시간이 걸리는 아직 필요하지 않은 초기화 -> 필요한 시점까지 미루기
  • GOF의 예
    GUI 객체들을 포함한 문서 에디터
    문서를 열때는 그래픽 객체들 생성하지 않고, 화면에 표시할 때가 되면 생성
  • Proxy 와 RealSubject의 분리
    • RealSubject 안에 지연평가 기능을 넣을수도 있다.
    • 분리하는 것이 수정 및 관리에 용이함 (유지보수가 용이)
  • 대리와 위임
    • 대리인이 처리할 수 있는 일은 대리인이 처리하고 (Proxy)
      대리인이 처리할 수 없을 때 본인에게 위임한다(RealSubject)
  • Transparent
    • 클라이언트 입장에서는, 실제로 호출하는 것이 PrinterProxy 인지, Printer 객체인지 상관 없음
      (투명하게 보임 - 안보임)
  • HTTP Proxy
    • Proxy는 HTTP 서버와 HTTP 클라이언트 사이에 들어가, 웹 페이지의 캐싱 등을 실행하는 소프트웨어
    • 프록시 패턴 적용
      • 웹 브라우저가 웹 페이지를 표시할 때, 일일이 원격지에 있는 웹 서버로부터 페이지를 얻어오는 것이 아님
      • 대신, HTTP 프록시가 캐쉬한 페이지를 얻어온다.
      • 최신 정보가 필요하게 되었거나, 페이지의 유효기간이 다 되었을 때에 비로소 웹서버로 웹 페이지를 가지러 감
      • 웹 브라우저 => Client 역할
      • HTTP Proxy -> Proxy 역할
      • 웹서버 -> RealSubject 역할

HTTP Proxy

public class PrinterProxy implements Printable {
    private String name;
    private Printable real;                 
    private String className;    
    public PrinterProxy(String name, String className) {     
        this.name = name;
        this.className = className;                                                 
    }
    public synchronized void setPrinterName(String name) {
        if (real != null) {
            real.setPrinterName(name);
        }
        this.name = name;
    }
    public String getPrinterName() {
        return name;
    }
    public void print(String string) {
        realize();
        real.print(string);
    }
    private synchronized void realize() {
        if (real == null) {
            try {                                                                       
                real = (Printable)Class.forName(className).newInstance();               
                real.setPrinterName(name);                                              
            } catch (ClassNotFoundException e) {                                        
                System.err.println("클래스 " + className + " 가 발견되지 않습니다.");      
            } catch (Exception e) {                                                     
                e.printStackTrace();                                                    
            }                                                                           
        }
    }
}
  • Printable 멤버변수, String className
  • Class.forName(className).newInstance();
    className 이름으로 된 클래스의 인스턴스를 생성
  • 현재
    real = (Printable)Class.forName(className).getDeclaredConstructor().newInstance();
    이렇게 변경됨

멀티스레드 상황에서 동기화되어있지 않으면 데이터의 신뢰성과 안정성에 문제가 발생할 수 있음
Printer - name

PrinterProxy - name 값이 달라질 수 있음

7. Command

  • 클래스(객체)가 일을 처리할 때는, 자신의 클래스나 다른 클래스의 메소드를 호출한다.
  • Command패턴에서는, 실행하고자 하는 일이 메소드 호출이 아닌,
    '명령을 나타내는 클래스'의 인스턴스 생성으로 표현
  • 이점
    • history를 관리하고 싶을 때, Command클래스의 인스턴스의 집합을 관리하면 된다.
    • 명령의 집합을 보존해두면, 똑같은 명령을 재실행 할 수도 있고, 또는 여러개의 명령을 모아서 새로운 명령으로서 재사용할수도 있다.

7.1 예제 프로그램

  • 간단한 그림 그리기 프로그램
    • 마우스를 드래그하면 빨간 점이 연결되어 그림이 그려진다.
    • 'clear' 버튼을 누르면 점이 모두 지워진다.
    • 사용자가 마우스를 끌 때마다, "이 위치에 점을 그려라" 라는 명령이 DrawCommand 클래스의 인스턴스로 생성된다.
패키지 이름 해설
command Command '명령'을 표현하는 인터페이스
command MacroCommand '여러 개의 명령을 모은 명령'을 나타내는 클래스
drawer DrawCommand '점 그리기 명령'을 표현한 클래스
drawer Drawable '그리기 대상'을 표현한 인터페이스
drawer DrawCanvas '그리기 대상'을 구현한 클래스
Anonymous Main 동작 테스트용 클래스

클래스 다이어그램

 

Command 인터페이스

  • '명령'을 표현하기 위한 인터페이스
  • execute()
    • 무언가를 실행하는 메소드
    • 구체적으로 무슨 일을 하는지는 Command 인터페이스를 구현한 클래스가 결정
  • MacroCommand와 DrawCommand가 이 인터페이스 구현

MacroCommand

  • '여러개의 명령을 한데 모은 명령'을 나타냄
  • Composite 패턴이 사용됨
    • 여러 개의 명령을 모은 것이면서, 그 자체가 하나의 명령이 된다.
  • commands 필드
    • java.util.Stack 타입
      -> Undo 구현을 위해 Stack 사용
    • 실행한 command 집합
  • execute()
    • 자신이 가지고 있는 모든 명령의 execute를 실행
    • MacroCommand가 가지고 있는 명령들의 execute() 가 차례대로 실행됨
      (recursive call, Composite 패턴)
  • append()
    • MacroCommand 클래스에 새로운 Command(Command 인터페이스를 구현한 클래스의 인스턴스)를 추가하는 메소드
    • Stack 클래스의 push() 를 이용
    • 실수로 자기 자신을 추가하지 않도록 체크함
      -> 자기 자신이 추가되면, execute() 실행 시 무한루프가 돌게 된다.
  • undo()
    • commands 의 마지막 명령을 삭제하는 메소드
    • Stack 클래스의 pop() 을 이용함
  • clear()
    • commands 의 모든 명령을 삭제하는 메소드

DrawCommand 클래스

  • Command 인터페이스 구현
  • '그림 그리기 명령'을 표현함
  • drawable 필드: 그림 그리기를 실행할 대상을 저장함
  • position 필드: 그림 그리기를 행할 위치를 나타냄
    java.awt.Point 클래스: X좌표와 Y좌표를 갖는 클래스
    -> 2차원 평면 상의 위치를 나타냄
  • 생성자의 매개변수
    • drawable
      어떤 개체를 대상으로 하는 그리기 명령인가를 나타내기 위한 인자
    • position
      어디에 그리는 명령인가를 나타내기 위한 인자
    • '이 위치에 점을 그려라'라는 명령을 생성하는 생성자
  • execute()
    • drawable 필드의 draw() 메소드 호출
    • 실제 그리기를 실행하는 메소드

더보기
package Sample.command;

public interface Command {
    public abstract void execute();
}
package Sample.command;

import java.util.Stack;
import java.util.Iterator;

public class MacroCommand implements Command {

    private Stack commands = new Stack();

    public void execute() {
        Iterator it = commands.iterator(); 
        while (it.hasNext()) {             
            ((Command)it.next()).execute();
        }                               
    }
    public void append(Command cmd) {
        if (cmd != this) {
            commands.push(cmd);
        }
    }
    public void undo() {
        if (!commands.empty()) {
            commands.pop();
        }
    }
    public void clear() {
        commands.clear();
    }
}
package Sample.drawer;

import Sample.command.Command;
import java.awt.Point;

public class DrawCommand implements Command {

    protected Drawable drawable;
    private Point position;

    public DrawCommand(Drawable drawable, Point position) {
        this.drawable = drawable;
        this.position = position;
    }
    public void execute() {                  
        drawable.draw(position.x, position.y); 
    }                                      
}

Drawable 인터페이스

  • '그림 그리기 대상'을 표현함
  • draw(int x, int y) 메소드를 가짐
  • x, y: 그림 그릴 때의 좌표를 나타냄

DrawCanvas 클래스

  • implements Drawable 인터페이스
    extends java.awt.Canvas 클래스 상속
  • history
    지금까지 실행한 명령어 집합 가짐
  • 생성자
    폭, 높이, 그림 그리기 history를 받아서, DrawCanvas 인스턴스를 초기화한다.
  • paint()
    • DrawCanvas를 다시 그릴 필요가 생겼을 때, ,java.awt 프레임워크로부터 자동 호출됨
    • repaint() 메소드가 호출되면, 자동으로 paint() 메소드가 실행
    • history가 보관하고 있는 모든 그리기 명령들을 실행
      commands 필드 -> 루프를 돌면서 앞에 명령부터 실행
  • draw()
    • Graphics 객체를 얻어서 색을 빨간색으로 지정,
    • Graphics 객체의 fillOval(x, y, 사각형 가로, 사각형 세로 길이)를 호출해 원을 그린다

클래스 다이어그램

더보기
package Sample.drawer;

public interface Drawable {
    public abstract void draw(int x, int y);
}
package Sample.drawer;

import Sample.command.*;

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class DrawCanvas extends Canvas implements Drawable {

    private Color color = Color.red;
    private int radius = 6;
    private MacroCommand history;

    public DrawCanvas(int width, int height, MacroCommand history) {
        setSize(width, height);
        setBackground(Color.white);
        this.history = history;
    }
    public void paint(Graphics g) {
        history.execute();
    }
    public void draw(int x, int y) {
        Graphics g = getGraphics();
        g.setColor(color);
        g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
}

Main 클래스

  • 예제 프로그램을 작동시키는 클래스
  • JFrame 상속, ActionListener, MousesMotionListener, WindowListener 구현
  • history 필드
    DrawCanvas 생성 시 인자로 넘겨줌
    (Main 인스턴스와 DrawCanvas 인스턴스가 history를 공유)
  • canvas 필드
    그림 그리는 영역을 나타냄
  • clearButton 필드
    javax.swing.JButton 클래스
    그린 점 모두 지움
  • actionPerformed()
    • clearButton 이 눌러졌을 때 호출
      ActionEvent 발생 시 호출되는 이벤트 핸들러
    • history에 보관되어있던 모든 명령을 지우고, 캔버스의 repaint() 가 호출된다.
      현재 history에 아무 명령도 들어있지 않으므로, 캔버스에 아무 내용도 그려지지 않음
  • mouseDragged()
    • 사용자가 마우스를 drag 하면 이 메소드가 호출된다.
      • 그리기 명령을 나타내는 DrawCommand 객체를 생성한 후, history에 추가
      • cmd.execute() 를 호출하여 지정 위치에 빨간 점을 그린다. (fillOval())
  • windowClosing()
    • 창의 오른쪽 아이콘 중에 X 를 눌렀을 때 호출되는 메소드
    • System.exit() (현재 exitOnClose() 사용)
      을 사용해서 프로그램을 종료한다.
      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
  • 생성자
    • 버튼 클릭, 마우스 클릭 등의 이벤트를 받아들이는 리스너를 설정함
    • 여러가지 GUI 부품을 배치함
      • Box 객체 이용
      • Box
        가벼운 컨테이너, 일종의 패널
        (컨테이너는 레이아웃매니저를 가짐.(컴포넌트 배치 방법 정의))
        • BoxLayout.X_AXIS
          (수평) 가로로 배치 시 사용 [ 0 0 0 ]
        • BoxLayout.Y_AXIS
          (수직) 세로 배치 시 사용
          (프레임 사이즈가 바뀌어도 항상 수직으로 유지됨)
          0
          0
          0
        • BoxLayout을 쓰는 컨테이너
        • GridBagLayout 보다 쉽게 GridBagLayout과 비슷하도록 만들 수 있다.
      • Panel
        FlowLayout 을 쓰는 컨테이너
더보기
package Sample;
import Sample.command.*;
import Sample.drawer.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {

    private MacroCommand history = new MacroCommand();
    private DrawCanvas canvas = new DrawCanvas(400, 400, history);
    private JButton clearButton  = new JButton("clear");

    public Main(String title) {
        super(title);

        this.addWindowListener(this);
        canvas.addMouseMotionListener(this);
        clearButton.addActionListener(this);

        Box buttonBox = new Box(BoxLayout.X_AXIS);
        buttonBox.add(clearButton);
        Box mainBox = new Box(BoxLayout.Y_AXIS);
        mainBox.add(buttonBox);
        mainBox.add(canvas);
        getContentPane().add(mainBox);

        pack();
        show();
    }

    // ActionListener
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == clearButton) {
            history.clear();
            canvas.repaint();
        }
    }

    // MouseMotionListener
    public void mouseMoved(MouseEvent e) {
    }
    public void mouseDragged(MouseEvent e) {
        Command cmd = new DrawCommand(canvas, e.getPoint());
        history.append(cmd);
        cmd.execute();
    }

    // WindowListener
    public void windowClosing(WindowEvent e) {
        System.exit(0);
    }
    public void windowActivated(WindowEvent e) {}
    public void windowClosed(WindowEvent e) {}
    public void windowDeactivated(WindowEvent e) {}
    public void windowDeiconified(WindowEvent e) {}
    public void windowIconified(WindowEvent e) {}
    public void windowOpened(WindowEvent e) {}

    public static void main(String[] args) {
        new Main("Command Pattern Sample");
    }
}
  • 예제 프로그램 행동 방식
    • 마우스가 드래그 될 때마다, 어느 좌표에 빨간 점이 그려졌다는 명령을 나타내는 DrawCommand 명령어가 history에 추가된다.
      • 이력 유지, redo, undo 사용 가능
    • clearButton 이 눌려지면, history안의 모든 DrawCommand객체들이 사라짐

7.2 등장 역할

  • Command 역할
    • 명령의 인터페이스를 정의하는 역할
    • Command 인터페이스
  • ConcreteCommand 의 역할
    • Command 인터페이스를 구현
    • MacroCommand, DrawCommand
  • Receiver의 역할
    • Command 명령을 실행할 때 대상이 되는 역할
    • 명령을 받아들이는 객체
    • DrawCanvas
  • Client의 역할
    • ConcreteCommand 를 생성하고, Receiver를 할당하는 역할
    • Main이 해당됨
  • Invoker의 역할
    • 명령을 처음 실행
    • Main, DrawCanvas
      history.execute() 호출

7.3 힌트

  • 명령이 지니고 있어야 하는 정보
    • 예제:
      DrawCommand는 그리기 대상과 점의 위치에 대한 정보만 가짐
    • 필요에 따라 점의 크기, 색, 모양, 이벤트 발생 시각등에 대한 정보 추가 가능
  • 이력의 보존
    • history멤버 변수는 MacroCommand 타입
      지금까지 발생된 모든 그리기 명령(정보)를 가지고 있다.
    • 이 멤버 변수를 파일로 보존하면 '그리기 이력'이 보존된다.
  • 어댑터
    • Main 클래스가 구현하는 인터페이스 중
      WindowListener 인터페이스는 여러 메소드가 선언되어 있어 모두 구현해야 함
      windowClosing(), windowActivated(), windowClosed(), windowDeactivated(), windowDeiconified(), windowOpened()
    • Adapter는 인터페이스의 메소드들을 모두 빈칸으로 구현해두었기 때문에 필요한 것만 구현해주면 된다.
    • But, adapter는 클래스이므로 다중 상속이 불가능하다.
      -> 익명 클래스 사용
    • 인터페이스에 사용하지 않는 메소드들까지 구현해야하는 문제르 해결하기 위해 어댑터 클래스들이 java.awt.event 패키지에 제공됨
      • WindowAdapter를 상속받고 원하는 메소드만 재정의하면 됨
    • Anonymous inner class
      new MouseMotionAdapter() {
          public void mouseDragged(MouseEvent e) {
      }}
      • 내부적으로 MouseMotionAdapter 를 상속받고 mouseDragged() 메소드를 재정의한 자식 클래스가 만들어진 후 그 클래스의 객체가 생성됨
    • 익명의 이너 클래스를 이용하면 더 깔끔하게 사용 가능
    • 사용과 정의를 동시에 할 수 있다.
      한번만 사용될 클래스를 새로 정의하지 않고 바로 짜준 것
canvas.addMouseMotionListener(new MouseMotionAdapter() {      
    public void mouseDragged(MouseEvent e) {                   
        Command cmd = new DrawCommand(canvas, e.getPoint()); 
        history.append(cmd);                                    
        cmd.execute();                                          
    }
});
  • 익명의 이너 클래스를 컴파일 한 경우
    Main$1.class 파일이 생성된다.

* '명령'을 객체로 표현해서 이력을 보관하기도 하고, 재실행을 할 수도 있는 Command 패턴 *

7.4 예제

1) ColorCommand 클래스

public class ColorCommand implements Command {

    protected Drawable drawable;
    private Color color;
    // 생성자
    public ColorCommand(Drawable drawable, Color color) {
        this.drawable = drawable;
        this.color = color;
    }
    public void execute() {
        drawable.setColor(color);
    }
}
  • drawable.setColor(color)
    붓의 색을 변경한다.
public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {

    private MacroCommand history = new MacroCommand();
    private DrawCanvas canvas = new DrawCanvas(400, 400, history);
    private JButton clearButton  = new JButton("clear");

    private JButton redButton  = new JButton("red");        
    private JButton greenButton  = new JButton("green");    
    private JButton blueButton  = new JButton("blue");      

    //생성자
    public Main(String title) {
        super(title);

        this.addWindowListener(this);
        canvas.addMouseMotionListener(this);
        clearButton.addActionListener(this);
        redButton.addActionListener(this);      
        greenButton.addActionListener(this);    
        blueButton.addActionListener(this);     

        Box buttonBox = new Box(BoxLayout.X_AXIS);
        buttonBox.add(clearButton);
        buttonBox.add(redButton);   
        buttonBox.add(greenButton); 
        buttonBox.add(blueButton);  
        Box mainBox = new Box(BoxLayout.Y_AXIS);
        mainBox.add(buttonBox);
        mainBox.add(canvas);
        getContentPane().add(mainBox);

        pack();
        show();
    }

    // ActionListener
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == clearButton) {
            history.clear();
            canvas.init();                                      
            canvas.repaint();
        } else if (e.getSource() == redButton) {                       
            Command cmd = new ColorCommand(canvas, Color.red);  
            history.append(cmd);  // 색깔 버튼이 눌려지면 ColorCommand 객체를 command에 추가                  
            cmd.execute();                                         
        } else if (e.getSource() == greenButton) {                     
            Command cmd = new ColorCommand(canvas, Color.green);
            history.append(cmd);                                   
            cmd.execute();                                         
        } else if (e.getSource() == blueButton) {                      
            Command cmd = new ColorCommand(canvas, Color.blue); 
            history.append(cmd);                                   
            cmd.execute();                                         
        }
    }

2) 마지막으로 그린 점을 삭제

  • history의 undo 기능 사용 (가장 마지막 항목을 삭제한 뒤 repaint())

Main에 추가된 부분

 public void actionPerformed(ActionEvent e) {
        if (e.getSource() == clearButton) {
            history.clear();
            canvas.repaint();
        } else if (e.getSource() == undoButton) {       
            history.undo();                             
            canvas.repaint();                           
        }
    }

MacroCommand에 추가된 부분

public void undo() {
        if (!commands.empty()) {
            commands.pop();
        }
    }

3) Listener 인터페이스 대신 어댑터로 변경하기

Main생성자 코드 수정

this.addWindowListener(new WindowAdapter() {                  
    public void windowClosing(WindowEvent e) {                 
        System.exit(0);                                         
    }                                                          
});  

canvas.addMouseMotionListener(new MouseMotionAdapter() {
    public void mouseDragged(MouseEvent e) {                   
        Command cmd = new DrawCommand(canvas, e.getPoint()); 
            history.append(cmd);                                    
            cmd.execute();                                          
        }                                                           
});
반응형

'sswu' 카테고리의 다른 글

컴파일러 중간  (0) 2022.10.04
Dockerfile 레이어 수  (0) 2022.06.13
오픈소스 소프트웨어 기말 정리  (0) 2022.05.20
독일어 정리  (0) 2022.04.15
디자인패턴 중간 정리  (0) 2022.04.03
Comments