파일과 디렉토리는 이름을 가질 수 있다. 또한 디렉토리 안에는 파일과 디렉토리 모두 들어갈 수 있다.
하지만 트리구조인 파일 시스템에서 파일은 자식을 가질 수 없고 디렉토리는 자식들을 가질 수 있다는 차이점이 존재한다.
public class File {
private String name;
public File(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Directory {
private String name;
private List<File> files = new ArrayList<>(); // File만 가능
public Directory(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void add(File file) {
files.add(file);
}
}
Directory
는 여러 파일을 가질 수 있기 때문에 위와 같은 files
를 가진다면, Directory
를 추가할 수 없다. 반대로 Directory
리스트를 가진다면 File
을 추가할 수 없다.
File
과 Directory
모두 넣을 수 있기 위해 Object
리스트로 선언한다면 런타임 에러를 마주칠 수 있고 강제 형변환을 사용해야할 것이다.
따라서 개별 객체인 File
과 복합 객체인 Directory
를 추상화할 방법이 필요하고, 컴포지트 패턴이 그 해결책이다.
컴포지트 패턴의 정의는 다음과 같다.
컴포지트 패턴은 트리구조 구현 시 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.
컴포지트 패턴은 Leaf
(개별 객체)와 Composite
(복합 객체)를 Component
로 추상화한다.
코드를 살펴보자.
public abstract class Component {
public void add(Component component) {
throw new UnsupportedOperationException();
}
public void remove(Component component) {
throw new UnsupportedOperationException();
}
public Component getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public void setName(String name) {
throw new UnsupportedOperationException();
}
}
우선 Component
이다.
복합 객체는 자식들을 가지고 개별 객체는 그렇지 않으므로 두 클래스 간 메소드의 종류에서 차이가 발생할 수 있다.
중복되는 메소드만 인터페이스로 구현하고 복합 객체에서 추가적으로 메소드를 새롭게 구현하는 방법도 있지만, 추상 클래스를 사용하면 선택적으로 오버라이드 가능하다.
개별 객체에서는 add()
를 구현하지 않을 것이므로, 호출 시 UnsupportedOperationException
가 발생할 것이다.
public class File extends Component {
private String name;
public File(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
}
개별 객체인 File
은 name
의 Setter & Getter만 구현했다.
public class Directory extends Component {
private String name;
private List<Component> components = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void add(Component component) {
components.add(component);
}
@Override
public void remove(Component component) {
components.remove(component);
}
@Override
public Component getChild(int i) {
return components.get(i);
}
}
복합 객체인 Directory
는 자식들을 가질수 있기 때문에 필드에 components
를 가지고 있다.
자식들을 추가, 삭제, 가져오기 위한 add()
, remove()
, getChild()
를 구현했고 File
과 마찬가지로 Directory
도 이름을 가질 수 있기 때문에 name
의 Getter & Setter가 존재한다.
출력 결과를 확인해보자.
public static void main(String[] args) {
Directory directory1 = new Directory("directory1");
Directory directory2 = new Directory("directory2");
File file1 = new File("file1");
File file2 = new File("file2");
directory1.add(file1);
directory2.add(file2);
directory1.add(directory2);
System.out.println(directory1.getName());
System.out.println(directory1.getChild(0).getName());
System.out.println(directory1.getChild(1).getName());
System.out.println(directory1.getChild(1).getChild(0).getName());
}
개별 객체와 복합 객체를 모두 Component
로 추상화했기 때문에 Directory
는 File
과 Directory
를 모두 자식으로 가질 수 있다.
출력 결과는 다음과 같다.
파일과 디렉토리를 예시로 컴포지트 패턴에 대해 알아 보았다.
부분-전체 계층구조를 가진 객체 컬렉션에서 그 객체들을 모두 똑같은 방식으로 다루고 싶을 때, 컴포지트 패턴을 떠올리자.
모든 소스코드는 여기에서 확인할 수 있다.