본문 바로가기

SW 개발 이야기

CSV 파일 기반 List 활용 (Code Analyst 코드 이야기 #3)

Code Analyst는 여러 점검을 수행하고 그 결과를 제공한다. 일부 점검은 결과가 크기도 한데, 몇 천 정도가 나오기도 한다. 예를 들면 전체 소스에 중복 코드 목록이 그렇게 되는 경우가 많다. 

문제는 이런 여러 결과를 메모리에 보관해야 하는 부담이다. 그래서 생각한 방식이 List 인터페이스를 갖으면서 리스트 내용이 파일로 보관하는 것이다. 오픈소스SW를 여기 저기 찾아봤지만, 그런 기능을 제공하는 것을 찾지 못했다. 

그래서 직접 구현하기로 했고, 이를 Code Analyst에서 잘 사용하고 있다.

우선, 해당 코드는 github.com/RedCA-Family/code-analyst/blob/development/src/main/java/com/samsungsds/analyst/code/util/CSVFileCollectionList.java 이다.

클래스 정의는 다음과 같이 제네릭(generics)를 활용했고, try-with-resources를 지원하기 위해 Closeable 인터페이스도 구현한다. 보관되는 파일 형식은 CSV(Comma Separated Values)로 결정했다.

public class CSVFileCollectionList<E extends CSVFileResult> implements List<E>, Closeable {
    // ...
}

내부적인 구현은 처리를 단순화하기 위해 읽기 상태 여부(isReadingStatus)를 통해 쓰기 모드와 읽기 모드로 구분하여 처리한다. 일반적인 리스트와는 달리 CSV 파일을 통해 내용이 관리되기 때문에 List에 데이터를 추가, 변경, 삭제 등을 자유롭게 하지 못하도록 한 것이다. 그래서 처음에는 쓰기 모드로 데이터를 추가(add)만 하다가 읽기 시작하면 읽기 모드로 전환하여 데이터를 가져오는 것만을 지원한다. 혹시 읽기 모드에서 데이터를 추가하려고 하면 다음과 같이 Exception을 발생시킨다.

@Override
public boolean add(E e) {
	if (isReadingStatus) {
		throw new IllegalStateException("Currently reading status!!!");
	}

	StringBuilder builder = new StringBuilder();

	for (int i = 0; i < e.getColumnSize(); i++) {
		if (i != 0) {
			builder.append(",");
		}
		builder.append(CSVUtil.getCSVStyleString(e.getDataIn(i)));
	}

	writer.println(builder.toString());

	size++;

	return true;
}

 쓰기 모드라면 데이터를 CSV 파일에 연결된 PrintWriter를 통해 CSV 형식으로 저장한다.

읽는 처리는 어떻게 될까? List에서 데이터를 가져오는 방식은 다음과 같이 Iterator나 ListIterator를 통해 처리된다.

@Override
public Iterator<E> iterator() {
	LOGGER.debug("iterator : {}", clazz.getSimpleName());
	changeToReadingStatus();

	return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;	// index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such

    public boolean hasNext() {
    	if (cursor == size) {
    		changeToReadingEndedStatus();

    		return false;
    	} else {
    		return true;
    	}
     }

     public E next() {
        int i = cursor;
        if (i >= size) {
            throw new NoSuchElementException();
        }

        E e = getElementInstance();

        cursor = i + 1;

		lastRet = i;

        return e;
   }

	private E getElementInstance() {
	    E e;
		try {
			e = clazz.newInstance();
		} catch (InstantiationException | IllegalAccessException ex) {
			throw new RuntimeException(ex);
		}

		try (Reader in = new StringReader(reader.readLine())) {
			Iterable<CSVRecord> records = CSVFormat.RFC4180.parse(in);
			for (CSVRecord record : records) {
				for (int columnIndex = 0; columnIndex < e.getColumnSize(); columnIndex++) {
					e.setDataIn(columnIndex, record.get(columnIndex));
				}
			}
		} catch (IOException ex) {
			throw new RuntimeException(ex);
		}
		return e;
	}
}
@Override
public ListIterator<E> listIterator() {
	LOGGER.debug("listIterator : {}", clazz.getSimpleName());

	if (elementData == null) {
		toArray();
	}

	return new ListItr(0);
}

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

	@SuppressWarnings("unchecked")
	public E previous() {
        int i = cursor - 1;
        if (i < 0) {
            throw new NoSuchElementException();
        }

        cursor = i;

		return (E) elementData[lastRet = i];
    }

    public void set(E e) {
    	if (lastRet < 0) {
            throw new IllegalStateException();
        }

        elementData[lastRet] = e;
    }

    public void add(E e) {
    	throw new UnsupportedOperationException("ListIterator's add");
    }

	@Override
	public void remove() {
		throw new UnsupportedOperationException("ListIterator's remove");
	}

	@SuppressWarnings("unchecked")
	@Override
	public E next() {
		int i = cursor;
        if (i >= size) {
            throw new NoSuchElementException();
        }

        cursor = i + 1;

        return (E) elementData[lastRet = i];
	}
}

그럼, 이제 사용 부분을 확인해 보자. 아, 먼저 제네릭의 element 타입에 해당하는 CSVFileResult 인터페이스 부분을 먼저 확인해야 한다. 이 CSVFileResult 인터페이스는 역할은 CSV 파일로부터 가져온 개별 데이터가 VO(Value Object)에 어떻게 매핑되는지를 처리하기 위해 만들어졌다. 즉, CSV의 몇 번째 컬럼의 VO의 어떤 property와 연결되는지에 대한 정보를 제공한다.

public interface CSVFileResult {
	int getColumnSize();
	String getDataIn(int columnIndex);
	void setDataIn(int columnIndex, String data);
}

구현 예를 보면 다음과 같은 방식으로 컬럼 대 필드를 매핑한다.

public class CheckStyleResult implements Serializable, CSVFileResult {
    private static final Logger LOGGER = LogManager.getLogger(CheckStyleResult.class);

    @Expose
    private String path;
    @Expose
    private int line;
    // 기타 멤버 변수 생략

    public CheckStyleResult() {
        // default constructor (CSV)
        // column : path, line
        path = "";
        line = 0;
    }

    @Override
    public int getColumnSize() {
        return 2;
    }

    @Override
    public String getDataIn(int columnIndex) {
        switch (columnIndex) {
            case 0 : return path;
            case 1 : return String.valueOf(line);
            default : throw new IndexOutOfBoundsException("Index: " + columnIndex);
        }
    }

    @Override
    public void setDataIn(int columnIndex, String data) {
        switch (columnIndex) {
            case 0 : path = data; break;
            case 1 : line = Integer.parseInt(data); break;
            default : throw new IndexOutOfBoundsException("Index: " + columnIndex);
        }
    }

    public CheckStyleResult(String path, String line /*, ... */) {
        this.path = path;
        this.line = Integer.parseInt(line);
    }

    // getters 생략
}

이제 해당 List 객체를 생성하는 부분을 확인해 보자.

List<CheckStyleResult> list = new CSVFileCollectionList<>(CheckStyleResult.class);

// 일반 list와 같이 add()나 for each 등을 통해 처리

이제 메모리에 자유로운 List 객체를 사용할 수 있다.

Written with by Vincent Han