안녕하세요. 오늘은 Composite 패턴에 대해 디렉터리를 예를 들어 알아보려고 합니다.
Composite라는 말은 합성물이라는 의미로 쓰이고 있습니다.
컴포지트 패턴(Composite pattern)이란 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다. 라고 정의 할 수 있습니다.
컴퓨터 파일 시스템에 디렉터리라는 것이 있고, 그 디렉터리 안에는 어떤 파일이 있거나 다른 디렉터리가 있기도 하죠.
디렉토리와 파일을 합해서 디렉토리 엔트리라고 부르기도 합니다.
디렉토리 엔트리라는 이름으로 디렉토리와 파일과 같은 종류로 간주하고 있는데 예를 들어 어떤 디렉토리 안에 무엇이 있는지를 차례대로 조사한다고 가정할 때 차례대로 조사하는 것은 하위 디렉토리일수도 있고, 어떤 파일일지도 모르죠.
그래서 이 디렉토리 엔트리를 차례대로 조사하는 것이지요.
디렉토리 파일을 모아서 디렉토리 엔트리로 취급하듯이 바구니와 내용물을 같은 종류로 취급하면 편리한 경우가 있는데 이 바구니 안에는 내용물을 넣을 수도 있고, 더 작은 도시락통을 넣을수도 있고, 물 병도 담을 수 있죠. 그리고 도시락 통이 삼단형으로 된 도시락 통이라면 그 안에 더 작은 도시락 통도 들어갈 수 있겠죠?
그럼 도시락통안에 도시락통을 넣는식의 재귀적인 구조를 만들 수 있게 되죠.
이 처럼 바구니와 그 안에 들어갈 내용물을 동일하게 하여 재귀적인 구조를 만들기 위한 디자인 패턴이라 생각하면 되겠습니다.
컴포지트 패턴의 구조는 아래와 같은데 아래와 같은 구조로 코드를 구성해보도록 하겠습니다.
package bami;
public abstract class Entry {
public abstract String GetName();
public abstract int GetSize();
public Entry Add(Entry entry) throws FileTreatmentException {
throw new FileTreatmentException();
}
public void PrintList() {
PrintList("");
}
protected abstract void PrintList(String prefix);
public String toString() {
return GetName();
}
}
먼저 추상 클래스인 Entry 클래스를 작성하도록 합니다.디렉토리 엔트리를 표현하고 있고, 하위 클래스인 File 클래스와 Directory 클래스를 만들고 있습니다.
티렉토리 엔트리는 이름을 가지고 있고, 이름을 얻기 위한 메소드로 GetName()을 준비하며 하위 클레스에서 구현을 하고 있습니다.
또한 디렉토리 엔트리는 크기를 가지고 있는데 이 크기를 얻기 위해 GetSize() 메소드를 준비하여 하위 클래스에서 구현하도록 합니다.
디렉토리 안에서 파일이나 디렉토리를 넣는 메소드로 Add()를 사용하고 있고, PrintList() 메소드는 종류를 표시하는 메소드 입니다.
여기서 인수가 없는 PrintList()와 인수가 있는 PrintList(String)이 있는데 이는 PrintList() 메소드의 오버로드를 한 것입니다.
호출 할 때 인수의 형태에 따라서 오버로드된 것 중에서 적절한 메소드가 선택 후 실행 됩니다. 여기서는 PrintList()는 Public으로 공개되고, PrintList(String)는 Protected로 Entry의 하위 클래스에서만 사용하도록 하였습니다.
package bami;
public class File extends Entry {
private String Name;
private int Size;
public File(String name, int size) {
this.Name = name;
this.Size = size;
}
public String GetName() {
return Name;
}
public int GetSize() {
return Size;
}
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
}
}
그 다음 작성할 것은 File 클래스입니다. File 클래스는 파일을 표현해주는 클래스인데 Entry 클래스의 하위 클래스로 선언해줍니다. 필드는 두개고, 파일의 이름을 표현하는 name과 크기를 나타내는 Size입니다.
생성자에서는 이름과 크기를 제공해서 File의 인스턴스를 만들어주는데
예를 들어
new File("Readme.txt", 1000)
같이 입력하면 Readme.txt라는 이름으로 크기가 1000인 파일이 작성되죠.
물론 가상으로 만들어지는 파일이고, 실제의 파일 시스템에서는 이 파일이 만들어지지 않습니다.
GetName()메소드와 GetSize() 메소드는 각각 파일의 이름과 크기를 반환하는 메소드입니다.
상위 클래스에서 위임된 PrintList(String)는 여기에서 구현하고, PrintList(String)는 prefix와 자신의 문자열 표현을 "/"로 구분해서 표시하고 있습니다. 여기에서 "/" + this라는 연산을 하고 있지만 문자열과 오브젝트를 더하면 자동적으로 그 오브젝트의 toString 메소드가 호출되죠.
package bami;
import java.util.Iterator;
import java.util.ArrayList;
public class Directory extends Entry {
private String name; // 디렉토리 이름
private ArrayList directory = new ArrayList(); // 디렉토리 엔트리의 집합
public Directory(String name) {
this.name = name;
}
public String getName() {
// 이름을 얻는다
return name;
}
public int getSize() {
// 크기를 얻는다
int size = 0;
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
size += entry.getSize();
}
return size;
}
public Entry add(Entry entry) {
// 엔트리 추가
directory.add(entry);
return this;
}
protected void printList(String prefix) {
System.out.println(prefix + "/" + this);
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry)it.next();
entry.printList(prefix + "/" + name);
}
}
}
그 다음은 Directory 클래스를 작성해보도록 하겠습니다.
말 그대로 디렉토리를 표현하는 클래스이고, Entry 클래스의 하위 클래스로 정의되어 있습니다.
필드는 name이라는 디렉토리의 이름을 나타내는 필드와 Directory라는 디렉토리 엔트리를 저장해두기 위한 필드가 존재하고 있습니다.
Directory 클래스에는 디렉토리의 크기를 동적으로 계산하기 때문에 크기를 나타내는 필드가 필요하지 않습니다.
getName() 메소드는 name 필드를 반환할 뿐이지만 getSize() 메소드는 계산도 실행됩니다.
directory의 요소를 하나씩 꺼내서 그 크기를 합한 값이 반환값이 됩니다.
get += entry.getSize();
이 부분은 변수 size에 entry의 크기를 더하고 있찌만 이 entry는 File의 인스턴스일지 모르고, Directroy의 인스턴스 일지 모르기 때문에 두 경우 모두 동일한 메소드 getSize()로 크기를 얻을 수 있습니다.
이것이 컴포지트 패턴의 특징으로 그릇과 내용물의 동일시를 나타내고 있죠.
그래서 File의 인스턴스든, Directory의 인스턴스든 entry는 Entry의 하위 클래스로서 다른 클래스가 만들어져도, getSize 메소드를 구현하고 있기 때문에 Directory 클래스의 이 부분을 굳이 수정할 필요가 없습니다.
entry가 Directory의 인스턴스인 경우 entry.getSize()라는 식을 쓰면 Directory안에 엔트리의 크기를 하나하나 더하게 되고, 그 안에 또 다른 디렉토리가 있으면 하위 디렉토리의 getSize를 재귀적으로 호출하게 됩니다.
이를 통해 컴포지트 패턴의 재귀적 구조가 그대로 getSize라는 메소드의 재귀적 호출에 대응하고 있다는 것을 알 수 있죠.
add 메소드는 디렉토리 안에 파일이나 디렉토리를 추가하기 위한 것인데 인수로 주어진 entry는 그것이 실제 Directory 클래스의 인스턴스인지, File클래스의 인스턴스인지 조사할 필요 없이
direcotry.add(entry);
로 direcotry 필드에 추가되죠..
package bami;
public class FileTreatmentException extends RuntimeException {
public FileTreatmentException() {
}
public FileTreatmentException(String msg) {
super(msg);
}
}
다음은 FileTreatmentException클래스를 작성해줍니다.
파일에 대해서 add 메소드를 잘못 호출했을 때 제공되는 예외처리입니다.
이 예외처리는 java의 클래스 라이브러리에서 제공되는 것이 아니라 이 예제 프로그램을 위해 정의한 것입니다.
그 다음 Main 클래스 입니다.
package bami;
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.PrintList();
System.out.println("");
System.out.println("Making user entries...");
Directory Bami = new Directory("Bami");
Directory Moon = new Directory("Moon");
Directory Kim = new Directory("Kim");
usrdir.add(Bami);
usrdir.add(Moon);
usrdir.add(Kim);
Bami.add(new File("diary.html", 100));
Bami.add(new File("Composite.java", 200));
Moon.add(new File("memo.tex", 300));
Kim.add(new File("game.doc", 400));
Kim.add(new File("junk.mail", 500));
rootdir.PrintList();
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}
'프로그래밍(Basic) > 디자인 패턴(Java)' 카테고리의 다른 글
[바미] Strategy 패턴에 대해 알아봅시다. (0) | 2022.01.25 |
---|---|
[바미] Bridge 패턴에 대해 알아봅시다. (0) | 2022.01.24 |
[바미] Builder 패턴에 대해 알아봅시다. (0) | 2021.10.25 |
[바미] Prototype 패턴에 대해 알아봅시다. (0) | 2021.10.04 |
[바미] Singleton 패턴에 대해 알아봅시다. (0) | 2021.09.23 |