[JAVA]Composite 패턴과 재귀호출로 파일탐색기 만들기 - 1
요즘 날씨가 너무 덥습니다. 밤에 잠이 오지 않을 정도인데요, 그래서 저는 요즘 밤마다 책을 보고 있습니다.(!) (책을 읽으면 잠이 잘 온다지요ㅎㅎ) 몇 권 돌려가면서 그날그날 기분에 따라 읽고 싶은 책을 읽고 있는데, 그 중 한 권이 바로 'Java 언어로 배우는 디자인 패턴 입문'입니다.
이 책은 입문용이라서 그런지 디자인 패턴 이해를 돕기위해 예제 코드가 아주 단순화되어있습니다. 그래서 꽤나 술술 읽히는 책입니다. 그 중에 'Composite 패턴'이라는 디자인 패턴을 읽다가 문득 예제 코드를 좀 더 응용시켜보고 싶어졌습니다. 예제 코드는 '컴퓨터 파일 시스템' 구조를 패턴에 적용시킨 것인데, 실제로 java.io 패키지의 File 객체를 이용하는 것은 아니고 임의로 파일이 있다고 가정하는 식으로 되어있습니다.
그래서 저는 java.io.File 객체를 활용하여 실제 파일 구조를 가지고 나름대로 부가 기능을 구현해보았습니다. 제목에서도 알 수 있지만 이 포스트는 1편이므로, 앞으로 웹 프로젝트로 확장하여 웹 상에서 사용할 수 있는 파일탐색기로 점점 발전시켜가는 과정을 계속 포스팅할 예정입니다.
Composite 패턴
<상자 안에 상자>
<인형 안에 인형, 마트로시카>
우선 Composite(;혼합물, 복합물) 패턴에 대해 알아보도록 하겠습니다. 위의 두 사진은 Composite 패턴을 이해를 돕기 위해 첨부해보았습니다. 상자 안에 상자가 연속으로 들어있는 선물을 받아보신적이 있으신가요? 혹은 러시아 전통인형 마트로시카를 분해해보신적이 있으신가요? 같은 사물이 안에 또 있고, 또 있어서 뭔가 오묘한 기분이 드는 두 사물의 공통점은 무엇일까요? 바로 '사물 안에 사물'이 있다는 점입니다.
이런 구조는 컴퓨터의 파일시스템에서도 발견할 수 있습니다. 디렉토리 안에 또 다른 디렉토리와 파일들이 존재하고, 그 디렉토리 안에 또 다른 디렉토리가 존재합니다.
디렉토리와 파일은 엄연히 다른 것이지만, 공통점도 있습니다. 바로 디렉토리안에 들어갈 수 있다는 점입니다. 그리고 우리는 디렉토리안에 디렉토리만 있어도, 디렉토리와 파일이 함께 있어도 그저 '디렉토리'로 구분합니다.
Composite 패턴은 '내용물과 그릇을 동일시해서 재귀적인 구조를 만들기위한 디자인 패턴'입니다. 파일시스템으로 설명하자면 디렉토리와 파일을 동일시하는 것입니다. 이 패턴은 그릇과 내용물을 같은 종류로 취급하면 재귀적인 구조를 만들 수 있어 더 편리한 경우가 있다는 점에 착안하고 있습니다.
패키지 구조
unit |
Node |
Directory와 File을 동일시하는 추상클래스 |
unit |
Directory |
디렉토리을 나타내는 클래스 |
unit |
File |
파일을 나타내는 클래스 |
manager |
FileSystemManager |
파일 시스템과 관련된 기능을 수행하는 클래스 |
exception |
FileProcessException |
예외적인 상황에 사용할 예외클래스 |
|
Main |
동작 테스트용 클래스 |
Node는 파일과 디렉토리를 동일시하는 추상클래스입니다. File과 Directory는 Node를 상속받습니다. FileSystemManager는 프로그램 동작시에 직접 인스턴스를 생성해야하는 객체로, 기본적으로 Directory와 File에서 구현한 기능을 응용하여, 재귀적으로 트리 구조를 만든다거나, 파일시스템의 전체 노드를 출력한다거나, 파일을 검색하는 일을 합니다. FileProcessException은 예기치 못한 동작이 발생했을 때 던지는 예외클래스입니다.
Node.java
package unit;
import exception.FileProcessException;
public abstract class Node {
// 노드가 실제로 나타내는 파일 객체
protected java.io.File file;
public abstract long getSize(); // 사이즈 반환
public abstract boolean isDir(); // 디렉토리 여부 반환
public abstract void printList(); // 노드 리스트 출력
public java.io.File getFile() {
return file;
}
public String getPath(){
return file.getPath();
}
public String toString(){
return getPath() + "(" + getSize() + ")";
}
protected Node add(Node node) throws FileProcessException{
throw new FileProcessException();
}
}
Node의 subclass 인 File과 Directory 클래스에서 구현 내용이 다른 getSize(), isDir(), printList() 메소드만 추상클래스로 지정하였습니다.
Directory.java
package unit;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Directory extends Node{
// 디렉토리에 속한 하위 노드 리스트
private List nodeList = new ArrayList();
public Directory(File file){
this.file = file;
iterate();
}
@Override
protected Node add(Node node){
nodeList.add(node);
return this;
}
@Override
public boolean isDir() {
return true;
}
@Override
public long getSize(){
long size = 0;
Iterator it = nodeList.iterator();
while(it.hasNext()){
Node node = (Node) it.next();
size += node.getSize();
}
return size;
}
@Override
public void printList(){
System.out.println(getPath() + "(크기: " + getSize() + " byte)"); // 디렉터리
Iterator it = nodeList.iterator(); // 하위 파일들
while(it.hasNext()){
Node node = (Node) it.next();
node.printList(); // 재귀호출
}
}
public List getNodeList() {
return nodeList;
}
private void iterate(){
File[] children = file.listFiles();
if(children == null){
return;
}
for(File child : children){
if(child.isHidden()){
continue;
}
// 디렉토리와 파일을 구분하여 List에 추가
if(child.isDirectory()){
nodeList.add(new Directory(child));
} else {
nodeList.add(new unit.File(child));
}
}
}
public void search(String searchKeyword, String ext, List childList, boolean matchCase){
if(childList == null){
childList = getNodeList();
}
for(Object obj :childList){
Node child = ((Node) obj);
if(child.isDir()){
// 디렉터리인 경우 다시 하위 리스트 탐색
search(searchKeyword, ext, ((Directory) child).getNodeList(), matchCase);
} else {
String name = child.getPath();
String compareName = name;
if(!matchCase){
compareName = name.toLowerCase();
}
// 검색어가 null, 확장자가 일치하는 경우
if(searchKeyword==null && compareName.endsWith(ext)){
System.out.println(name);
}
// 확장자가 null, 파일명이 포함된 경우
if(ext == null && compareName.contains(searchKeyword)){
System.out.println(name);
}
// NullPointerException을 거르기 위한 조건문
if(searchKeyword == null || ext == null){
continue;
}
// 파일명 포함, 확장자 일치
if(compareName.contains(searchKeyword) && compareName.endsWith(ext)){
System.out.println(name);
}
}
}
}
}
File 클래스와 다르게 Directory 클래스는 하위 노드들을 보관할 수 있는 리스트를 전역 변수로 갖고 있습니다. 이 전역변수는 iterate()메소드에서 add() 메소드를 통해 리스트의 요소가 만들어지며, iterate()는 Directory 클래스의 생성자에서 호출하도록 되어있습니다. 즉, 'Root Directory 노드 생성 -> iterate()를 통한 하위 노드 요소 추가 -> iterate()에서 하위노드가 디렉토리인 경우 다시 Directory 노드 생성 -> iterate()를 통한 하위 노드 요소 추가..' 라는 일련의 과정이 모든 노드를 다 돌때까지 반복됩니다. 재귀적으로 호출하므로써 전체 파일 노드를 다 돌게되는 것이죠. private 으로 지정하여 외부에서는 호출할 수 없도록 하였습니다.
package manager;
import exception.FileProcessException;
import unit.Directory;
import unit.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class FileSystemManager {
private List rootDirList = new ArrayList();
private File[] roots;
public FileSystemManager(){
this.roots = File.listRoots();
}
public int rootSize(){
return roots.length;
}
public List getRootDirList() {
return rootDirList;
}
public void setNodeTree(){
setNodeTree(0, roots.length);
}
public void setNodeTree(int index){
if(index > roots.length-1){
throw new FileProcessException("인덱스가 너무 큽니다.");
}
setNodeTree(index, index+1);
}
public void setNodeTree(int stratIndex, int lastIndex){
for(int i = stratIndex; i < lastIndex; i++){
Node dirNode = new Directory(roots[i]);
rootDirList.add(dirNode);
}
}
public void printTotalList(List list){
if(list == null){
list = rootDirList;
}
Iterator it = list.iterator();
while(it.hasNext()){
Node node = (Node) it.next();
node.printList();
}
}
public void searchFile(String searchKeyword, String ext, boolean matchCase){
if(searchKeyword == null && ext == null){
throw new FileProcessException("둘 중 하나는 null 이 아니어야 합니다.");
}
if(!matchCase){ // 대소문자 구분 여부
if(searchKeyword != null){
searchKeyword = searchKeyword.toLowerCase();
}
if(ext != null){
ext = ext.toLowerCase();
}
}
Iterator it = rootDirList.iterator();
while(it.hasNext()){
Node node = (Node) it.next();
if(node.isDir()){
((Directory) node).search(searchKeyword, ext, null, matchCase);
}
}
}
}
import manager.FileSystemManager;
public class Main {
public static void main(String[] args){
FileSystemManager manager = new FileSystemManager();
manager.setNodeTree(1);
manager.printTotalList(null); // 전체리스트 출력
System.out.println("=======================================");
manager.searchFile(null, "png", false); // 대소문자 구분 X, 확장자 png 검색
}
}
실행한 결과입니다. 디렉토리 노드는 Directory클래스의 printList() 메소드에서 구현했던 바와 같이 크기와 함께 출력되었으며 파일 노드는 앞에 tab이 붙어 출력되었습니다. 그리고 구분선(=======) 아래는 확장자가 png인 파일을 탐색하여 출력한 결과입니다. D 드라이브에 있는 모든 png 파일이 확장자의 대소문자 구분에 상관없이 출력된것을 볼 수 있습니다. (searchFile의 세번째 인자를 false로 지정했기 때문에)
D드라이브 바로 아래에 있는 파일이 한 번에 출력되지 않고 이리저리 흩어져 있는데 이는 정렬이 되지 않아서 그런 것 같습니다. 이 다음번 포스팅에서는 이 문제의 해결과 함께 파일 추가 삭제 등의 기능과 만든 메소드를 활용하여 웹에서 표현하는 것까지 해보도록 하겠습니다.
'백엔드' 카테고리의 다른 글
java Generics && Netty Bootstrap (0) | 2017.03.20 |
---|---|
딥러닝에 대하여 1 - 딥러닝과 머신러닝, 그리고 신경망 기초 개념 (0) | 2017.03.06 |
Git merge, rebase 이해하기 (0) | 2016.07.29 |
JAVA exception 처리하기 (0) | 2016.07.01 |
Git의 내부구조 (0) | 2016.05.20 |