[개발 일기] 소계 및 합계 기능 구현

Posted by 열정보이
2019. 3. 10. 18:50 개발 일기

오늘은 최근에 개발을 진행한 소계 및 합계를 구하는 기능을 구현했던 것에 대해 작성해보도록 하려고 한다.





1. Requirement


넘어온 데이터들을 '기준에 따라서 소계와 합계 및 기준에 따른 평균'을 구하라.

이때, 기준의 개수는 정해지지 않으며, 보고서마다 다를 수 있으니 동적으로 메서드를 구현해야만 한다.


사진으로 보도록 하자.


사진 1


만약 내가 넘어온 List를 어떠한 처리도 하지 않고, 보고서를 그리는 JS 파일로 넘길 경우, 위와 같이 그려진다.

위의 데이터에서 내가 해야 되는 일은, '(반, 이름) 으로 기준' 을 잡고 '날짜에 대한 대여 횟수를 넣어서 List형식으로 보내줘야 하는 일'이다.

즉, Oracle의 Rollup을 JAVA로 구현하고, 해당 날짜에 대여 횟수를 구하는 작업이라고 하면 되겠다.(실제로 데이터는 책이 아닌, 콜에 대한 데이터다..)


그래서 최종적으로는 다음과 같이 그려질 수 있도록, List에 값을 넣어 보내줘야 한다.


사진 2


2. Input


메서드의 파라미터로 넘어오는 데이터는 'List<Map<String,String>> Type 으로 이뤄져 있는 데이터' 와 해당 '데이터들의 Column 명과 Type 이 담겨있는 JSONObject' 가 넘어온다.

즉 List로 구성된 파라미터로 넘어온 데이터를 data 라고 가정할 때, data.get(0)을 출력해보면 다음과 같다.


1
{반=A, 이름=홍길동, 날짜=2019-03-10, 대여 도서=에프니까 청춘이다, 반납 일=2019-03-24}
cs




3. Output


넘어온 List의 데이터들 사이 사이마다 '각 기준에 맞는 SUB-SUM과 날짜별 책 대여 수량, TOTAL-SUM을 LIST' 에 추가하여 넘겨줘야 한다.




4. How


실제 코드는 훨씬 복잡하나, 간단하게 개발을 하면서 느낀점만 적도록 하겠다.


A) 맨 처음 data.get(0)의 기준값을 key라는 List에 저장하고, data.get(0) 을 SUB-SUM을 구하는 데이터들을 모아놓은 subSumArr List에 저장한다.

이때, subSumArr의 size는 기준의 개수와 같고(반, 이름) 반의 SUB-SUM을 구하기 위한 데이터들은 subSumArr.get(0)에, 이름에 대한 SUB-SUM을 

구하기 위한 데이터들은 subSumArr.get(1)에 모아둔다.


B) for문을 data.size() 만큼 돌면서 각 ROW별로 기준값을 체크해준다. 이때 index 0이 아닌, 1부터 돌면서 key List에 저장된 기준값과 비교하여, 값이 같을 경우, subSumArr.get(0)에 저장한다.


C) 만약 기준값이 다를 경우, 해당 기준의 우선 순위를 따진다. 

사진을 보면 반->이름 으로 기준이며, 가장 낮은 레벨의 기준인 이름이 다를 경우, subSumArr.get(0)에 저장된 값들 중, 더할 수 있는 int, long, double 타입의 데이터는 더하고, 타입이 String일 경우 SUB-SUM으로 값을 바꿔준다. 그리고 방금 구한 이름에 대한 SUB-SUM의 값을 subSumArr.get(1)에 저장하며, subSumArr.get(0)에 담겨있는 값들은 다음 기준에 SUB-SUM을 구하는 값들만 저장할 수 있게 Clear 해준다.


그리고 그 다음 레벨의 기준인 반이 다를 경우, subSumArr.get(1)에 저장된 데이터를 이름의 경우와 같이 구해 SUB-SUM을 구해준다.

기준이 다를 경우에 대한 케이스를 확인 한 후, 다시 기준 값을 새로운 기준에 맞게 변경한다.


실은 이것보다 더 복잡하고 예외 상황이 많으나, 이 정도로만 정리하도록 하겠다.




이러한 개발 과정에서 내가 배운것을 정리해보도록 하겠다.

내가 생각한 '첫 번째 문제' 는 구한 SUB-SUM들을 입력받은 List<Map<String,String>> data 값에 그때그때 추가하는 방법이었다.

그때 그때 SUB-SUM이 생길때마다 data.add(index, SUB-SUM Map) 와 같이 추가하다보니, 'for문이 동작하는 횟수가 증가' 될 수 밖에 없다.

왜냐하면 'for문은 data.size() 만큼 도는데, data의 크기가 자꾸 증가' 하기 때문이다.


그러기 위해, SUB-SUM이 추가될 때는 for문의 변수 i를 ++ 해주는 방법을 선택했으나, 사진 2를 보면 알다시피, 기준이 2개가 모두 다를 경우에는 이름에 대한 SUB-SUM과 반에 대한 SUB-SUM이 추가되므로 i+=2를 해줘했고, 그 결과 나 이외에는 누구도 수정할 수 없는... 유지보수가 불가능한 복잡한 코드가 완성되었다.


그래서 내가 선택한 두 번째 방법은 for문은 data.size() 만큼 도나, data.get(i)를 하여 data를 빼면 새로운 List<Map<String,String>> resultData 라는 변수에 저장을 한다. 즉 List data를 돌면서 값을 하나 하나 새로운 List인 resultData에 옮겨주는 방법이다. 그리고 SUB-SUM이 생길때마다 또 옮겨주는 방법을 선택하였다.


해당 방법을 선택함으로, 코드는 훨씬 간결해질 수 있었고, 다른 사람이 봤을 때, 코드를 분석하는 것도 쉬워졌다.

또한 유사한 작업들은 하나의 Method를 만들어 코드를 재활용 할 수도있게 되었다.


첫 번째 문제를 통해 내가 배운것은, '기능에 맞게 잘 쓰게 코딩하자' 다.

for문은 시작 값과 종료값이 존재하는데, 종료값을 자꾸 바꾼다는 자체가 사실 말도 안되는 코드였던 것이다.

종료값이 자꾸 바뀌니 시작값 변수 i의 값을 종료값이 증가된 값만큼 또 증가시켜줘야 하는 문제들이 발생했었다.

코드는 쉽고 간결하게!! 그리고 메서드는 기능 단위로 나누자!!




'두 번째 문제' 는 LinkedList와 ArrayList의 성능 차이다.

대학교때 이론으로는 귀가 닳도록 들은 개념.... ArrayList와 LinkedList의 차이...!!


List 중간에 값을 삽입 또는 삭제하는 과정이 많다면 LinkedList를 사용하는게 효율적이며, List 안의 값을 순회하는 과정이 많다면 ArrayList를 사용하는게 효과적이다. 누구나 다 아는 것이라고 생각한다. 하지만 실제로 데이터를 가지고 테스트해본적은 없었으나, 이번에 알게되었다...


중간에 데이터를 삽입(삭제) 하는데 ArrayList를 사용해서는 안된다는 것을....


다음 코드를 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
    ArrayList<HashMap<String,String>> map1 = new ArrayList<>();
    LinkedList<HashMap<String,String>> map2 = new LinkedList<>();
        
    long start1 = System.currentTimeMillis();
    for (int i = 0; i < 50000; i++) {
        map1.add(0,new HashMap<String,String>());
    }
    long end1 = System.currentTimeMillis();
    
    long start2 = System.currentTimeMillis();
    for (int i = 0; i < 50000; i++) {
        map2.add(0,new HashMap<String,String>());
    }
    long end2 = System.currentTimeMillis();
    
    System.out.println("ArrayList : " + (end1 - start1));
    System.out.println("LinkedList : " + (end2 - start2));
}
cs


하나는 ArrayList 이고, 다른 하나는 LinkedList이다. 

두 경우 '모두 해당 List의 첫번째 index에 새로운 값' 을 넣는 과정이다.

과연 ArrayList와 LinkedList의 처리과정은 얼마나 차이가 날까?


1
2
ArrayList : 128
LinkedList : 4

cs


for문이 50,000번 돌 경우 위와 같이 '몇 배 이상의 차이' 가 발생한다.

물론 이렇게 극적인 예는 현실세계에서 보기 힘들지 않을까 한다...


그렇다면 '똑같이 값을 추가하나, 첫 번째 index가 아닌 마지막 index에 추가할 경우' 에는 어떻게 될까?


1
2
ArrayList : 4
LinkedList : 4
cs


이때는 동일한 처리속도를 보여주고 있다.

물론 정확한 결과는 아니겠지만...


그렇다면 이번에는 우리가 흔히 알고 있듯이, 'List 안의 값을 순회하는데 LinkedList를 사용하면 안되는 이유' 를 보도록 하자.

이번에는 순회하는 코드다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
    ArrayList<HashMap<String,String>> map1 = new ArrayList<>();
    LinkedList<HashMap<String,String>> map2 = new LinkedList<>();
        
    for (int i = 0; i < 50000; i++) { map1.add(new HashMap<String,String>()); }
    for (int i = 0; i < 50000; i++) { map2.add(new HashMap<String,String>()); }
        
    long start1 = System.currentTimeMillis();
    for (int i = 0; i < map1.size(); i++) {
        map1.get(i);
    }
    long end1 = System.currentTimeMillis();
        
    long start2 = System.currentTimeMillis();
    for (int i = 0; i < map2.size(); i++) {
        map2.get(i);
    }
    long end2 = System.currentTimeMillis();
        
    System.out.println("ArrayList : " + (end1 - start1));
    System.out.println("LinkedList : " + (end2 - start2));
}
cs


과연 결과는 어떨까.


1
2
ArrayList : 0
LinkedList : 1535
cs


차이가 나도 너무 차이가 난다.


왜 그런지 알아보자.





ArrayList는 위 사진과 같이 데이터를 물리 주소에 순차적으로 저장하며, 데이터 외에도 해당 값에 대한 index를 저장하고 있다. 

그렇기에 index를 이용해 해당 값의 물리 주소를 찾을 수 있어 Random Access가 가능하다.

그 결과 순회하는 과정에서 해당 값의 물리적 주소를 알고 있기 때문에, O(1) 의 시간복잡도를 가진다.


그러나 LinkedList 같은 경우 index를 저장하지 않고, 자신의 이전노드의 주소값과 다음 노드의 주소값을 저장하고 있다.

Java Collections Framework 에서 제공하는 LinkedList는 양방향 LinkedList이다.

무튼, 해당 데이터들의 index가 아닌, 앞뒤 노드의 index만 알고 있기 때문에 Random Access가 불가능하다.

즉, Sequential Access만 가능하다. 그렇기에 List를 순회하는데 느린것이다.

또한 시간복잡도는 O(n)을 가진다.



그럼 해당 값을 삽입이나 삭제할때는 어떨까.



만약 index 0 번에 값을 추가한다면, ArrayList의 크기를 증가시킨다. 그 이후에 삽입되는 index 뒤에있는 값들을 모두 뒤로 미루고, 값을 삽입하는 과정을 거치게 된다. 그냥 데이터를 추가만 하는 과정이 아닌, 해당 데이터들을 이동시키는 작업이 추가로 진행되어, ArrayList에 값을 삽입하고 삭제하는 과정은 비효율적인것이다.


그렇다면 LinkedList를 보자


LinkedList 같은 경우, 넣고자 하는 index의 앞뒤 노드가 가르키는 주소값만 변경시켜주면 된다.

ArrayList보다 작업이 적기 때문에 값을 삽입하고 삭제하는 과정에서는 보다 더 효율적이라고 할 수 있는 것이다.


두 번째 문제를 통해 내가 배운것은, 'ArrayList와 LinkedList의 상황에 따른 활용' 이다.

물론 알고는 있었지만, 이렇게 차이가 심한줄을 몰랐기에, 이번에야말로 제대로 배웠다고 할 수 있을 것 같다.




이렇게 소계 및 합계 기능 구현에 대한 개발일기를 마친다.