백엔드

[JAVA]Composite 패턴과 재귀호출로 파일탐색기 만들기 - 1

CyberI 2016. 8. 1. 20:57


[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() 메소드만 추상클래스로 지정하였습니다.


File.java

package unit;

public class File extends Node {
public File(java.io.File file){
this.file = file;
}
@Override
public boolean isDir() {
return false;
}
@Override
public long getSize(){
return file.length();
}
@Override
public void printList(){
System.out.println("\t"+getPath());
}
}

파일은 추상클래스 Node를 상속받아 추상 메소드를 실제로 구현하고있습니다.


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 으로 지정하여 외부에서는 호출할 수 없도록 하였습니다. 

 search() 클래스는 디렉토리내의 특정 파일을 검색하는 메소드입니다. iterate()와 마찬가지로 재귀적으로 호출되어 전체 노드를 돌며 검색 조건에 맞는 파일을 찾아냅니다.
FileSystemManager.java

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);
}

}
}

}
FileSystemManager는 파일 구조의 루트 디렉토리를 전역 변수로 가지고 있습니다. 이 루트 디렉토리로부터 시작하여 하위 노드를 돌며 tree구조의 디렉토리를 세팅하여, 전체 파일 리스트를 출력한다거나 검색하는 기능을 구현하고 있습니다.

 setNodeTree()가 오버로드되어 파라미터만 다르게 3개의 메소드를 구현한 이유는 파일시스템의 루트디렉토리를 지정하게 하기 위함입니다. 예를 들어, C드라이브, D드라이브가 있는 경우에 C드라이브의 파일만 탐색하고 싶은 경우setNodeTree(0)을 호출하면 루트디렉토리에서 인덱스가 0인 C드라이브만을 탐색하게 됩니다. 아무것도 없는 경우는 전체 루트를 다 탐색하며, C,D,E 드라이브가 있는 경우를 대비하여 끝 인덱스를 지정할 수 있게 하였습니다. 이와 같은 경우, setNodeTree(0,1)로 하면 C,D 드라이브만 탐색하게 됩니다.

Main.java
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 검색

}

}
실행 클래스인 Main.java 입니다. FileSystemManager 인스턴스를 생성하여 루트 디렉터리 중에서 index가 1인 D드라이브의 디렉터리만 탐색합니다. 그 후 D드라이브의 전체리스트를 출력하고, 확장자가 png인 파일을 찾아서 출력해보도록 하겠습니다.

실행한 결과입니다. 디렉토리 노드는 Directory클래스의 printList() 메소드에서 구현했던 바와 같이 크기와 함께 출력되었으며 파일 노드는 앞에 tab이 붙어 출력되었습니다. 그리고 구분선(=======) 아래는 확장자가 png인 파일을 탐색하여 출력한 결과입니다. D 드라이브에 있는 모든 png 파일이 확장자의 대소문자 구분에 상관없이 출력된것을 볼 수 있습니다. (searchFile의 세번째 인자를 false로 지정했기 때문에) 

 D드라이브 바로 아래에 있는 파일이 한 번에 출력되지 않고 이리저리 흩어져 있는데 이는 정렬이 되지 않아서 그런 것 같습니다. 이 다음번 포스팅에서는 이 문제의 해결과 함께 파일 추가 삭제 등의 기능과 만든 메소드를 활용하여 웹에서 표현하는 것까지 해보도록 하겠습니다.

긴 글 읽어주셔서 감사합니다. :)