리스트와 배열 간 빠른 복사 방법

등록일: 2014. 08. 13

자바 코딩, 이럴 땐 이렇게: PMD로 배우는 올바른 자바 코딩 방법

  • 배병선 지음
  • 426쪽
  • 28,000원
  • 2014년 05월 28일

배열에서 ArrayList와 같은 List 컬렉션으로 변경하거나 복사할 때 빈번히 예제 11.2.1과 같은 원시적인 방식을 통해 변환하곤 하는데, 이는 매우 비효율적인 방법이다. 리스트는 데이터의 길이가 가변적이라서 메모리 자원이 허용하는 한도 내에서 지속적으로 데이터의 길이를 늘일 수 있다. 하지만 리스트 또한 내부적으로 모든 데이터는 하나의 배열로 관리되고 있어서 리스트의 길이를 넘는 새로운 데이터가 추가될 때마다 내부 배열의 길이를 재설정하고 값을 복사하고 새로운 데이터를 추가하는 작업을 반복적으로 수행한다. 따라서 예제 11.2.1과 같은 방식은 데이터를 삽입하기 위한 불필요한 작업이 반복적으로 발생하므로 매우 비효율적이다.

예제 11.2.1 반복문을 이용해 배열의 요소를 리스트로 복사한 예

package com.software.optimize.problem;

import java.util.ArrayList;
import java.util.List;

public class UseArraysAsListExample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();

        Integer[] array = new Integer[1000000];

        // 반복문을 이용한 데이터 입력
        for (int i = 0; i < 1000000; i++) {
            array[i] = i;
        }

        long startTime = System.currentTimeMillis();

        // 반복문을 이용한 데이터 입력
        for (int i = 0; i < 1000000; i++) {
            list.add(array[i]);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("##실행시간(초.0f) : " + (endTime - startTime) / 1000.0f + "초");
    }
}

실행 결과

##실행시간(초.0f) : 0.013초

어떤 배열의 요소를 새로운 배열로 복사하는 방법으로 반복문을 이용하거나 clone 메서드를 이용해 복사하는 방식 또한 비효율적인 방식이다. 우선 흔히 사용하는 반복문을 이용해 값을 복사하는 방식은 배열의 값을 하나씩 호출하고 다시 입력하는 과정을 반복문을 통해 수행하므로 소스코드의 길이가 길어지고 효율도 매우 나쁘다. clone을 이용하는 방식은 소스코드는 매우 간결하지만 원본 배열의 값을 모두 복제해 또 하나의 배열을 생성하는 과정에서 자원이 낭비되고 실행 속도 또한 반복문을 이용한 복사보다는 빠르지만 크게 차이가 나지 않는다. 더욱이 복사되는 원본과 복사본은 배열의 길이와 자료형이 모두 같아야 한다는 제약이 있다. 예제 11.2.1은 이런 잘못된 배열 복사 방식의 예다.

예제 11.2.2 clone과 반복문을 이용한 배열 복사의 예

package com.software.optimize.problem;

public class AvoidArrayLoopsExample {
    public static void main(String[] args) {
        int[] source = new int[10000];

        // 원본 배열에 값 입력
        for (int i = 0; i < source.length; i++) {
            source[i] = i;
        }

        AvoidArrayLoopsExample example = new AvoidArrayLoopsExample();

        example.copyByClone(source);
        example.copyByLoop(source);
    }

    public void copyByLoop(int[] source) {
        int[] target = new int[source.length];

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            for (int k = 0; k < source.length; k++) {
                target[k] = source[k];
            }
        }

        // 종료 시간
        long endTime = System.currentTimeMillis();

        // 시간 출력
        System.out.println("##반복문 실행시간(초.0f) : " + (endTime - startTime) / 1000.0f + "초");
    }

    public void copyByClone(int[] source) {
        long startTime = System.currentTimeMillis();

        int[] target;
        for (int i = 0; i < 100; i++) {
            target = source.clone();
        }

        // 종료 시간
        long endTime = System.currentTimeMillis();

        // 시간 출력
        System.out.println("##clone 실행시간(초.0f) : " + (endTime - startTime) / 1000.0f + "초");
    }
}

실행 결과

##clone 실행시간(초.0f) : 0.001초
##반복문 실행시간(초.0f) : 0.007초

문제점 진단

PMD에는 이처럼 비효율적인 배열과 리스트의 복사를 방지하기 위해 UseArraysAsList 룰을 통해 반복문을 이용해 배열의 요소를 리스트로 복사하는 코드를 진단하고 수정하도록 권고한다. 또한 배열 간 비효율적인 복사는 AvoidArrayLoops 룰을 이용해 진단하고 경고한다. 하지만 clone을 이용한 복사에 대해서는 아직 권고하는 바가 없다. 그림 11.2.1은 이처럼 잘못된 복사 방법을 진단한 결과다.

figure11-2-1-1

figure11-2-1-2
그림 11.2.1 비효율적인 배열 복사를 진단한 결과

해결 방안

자바는 자체적인 System.arraycopy 메서드를 JNI(Java Native Interface)에 포함해서 효율적인 배열 복사 기능을 지원한다. 아울러 배열에서 리스트로 변경을 위한 Arrays.asList메서드도 제공한다. 이 두 메서드는 공통적으로 앞에서 나열한 방식에서 발생할 수 있는 불필요한 인스턴스 생성을 방지해 메모리 자원 낭비를 막고, 더 빠른 실행 속도를 보장한다는 장점이 있다. 가독성 측면에서도 단 한 줄의 소스코드로 배열을 복사할 수 있어 더욱 효율적이다. 특히 arraycopy 메서드는 단순히 전체 배열을 복사하는 것이 아닌 원본 배열의 특정 부분만을 선택해서 복사하는 등의 유연한 기능을 제공한다. 예제 11.2.3은 이 두 메서드를 사용하는 예다.

arraycopy 사용법

arraycopy ([원본 배열], [복사 시작 위치], [대상 배열], [대상 배열의 시작 위치], [복사 길이

예제 11.2.3 자바의 기본 메서드를 활용한 배열 복사

AvoidArrayLoopsExample: arraycopy를 활용한 배열 복사

package com.software.optimize.solution;

public class AvoidArrayLoopsExample {
    public static void main(String[] args) {
        int[] source = new int[10000000];

        // 원본 배열에 값 입력
        for (int i = 0; i < source.length; i++) {
            source[i] = i;
        }

        AvoidArrayLoopsExample example = new AvoidArrayLoopsExample();
        example.copyByArrayCopy(source);
    }

    public void copyByArrayCopy(int[] source) {
        int[] target = new int[source.length];

        long startTime = System.currentTimeMillis();

        System.arraycopy(source, 0, target, 0, source.length);

        // 종료 시간
        long endTime = System.currentTimeMillis();

        // 시간 출력
        System.out.println("##arraycopy 실행시간(초.0f) : " + (endTime - startTime) / 1000.0f + "초");
    }
}

UseArraysAsListExample: listAs를 활용한 배열을 리스트로 복사

package com.software.optimize.solution;

import java.util.Arrays;
import java.util.List;

public class UseArraysAsListExample {
    public static void main(String[] args) {
        Integer[] array = new Integer[1000000];

        // 반복문을 이용한 데이터 입력
        for (int i = 0; i < 1000000; i++) {
            array[i] = i;
        }

        // 시작 시간
        long startTime = System.currentTimeMillis();

        List<Integer> list = (List<Integer>) Arrays.asList(array);

        // 종료 시간
        long endTime = System.currentTimeMillis();

        // 시간 출력
        System.out.println("##실행시간(초.0f) : " + (endTime - startTime) / 1000.0f + "초");
    }
}