본문 바로가기
■Development■/《Java》

[Java] System.arraycopy 완벽 가이드

by 은스타 2019. 9. 4.
반응형

Java System.arraycopy 완벽 가이드

안녕하세요.
이번 포스팅은 Java에서 배열을 효율적으로 복사하는 핵심 메서드인 System.arraycopy에 대해 상세히 알아보겠습니다. 대용량 데이터를 다루거나 성능이 중요한 애플리케이션을 개발할 때 배열 복사 방법은 프로그램의 속도와 메모리 사용에 큰 영향을 미칩니다. 이 글에서는 System.arraycopy의 기본 개념부터 고급 활용법, 성능 분석까지 모든 것을 다루어 여러분의 코딩 실력을 한 단계 업그레이드하는 데 도움을 드리겠습니다.


목차

  1. System.arraycopy란?
  2. 기본 문법과 매개변수
  3. System.arraycopy 사용 예제
  4. 다양한 데이터 타입 복사하기
  5. 성능 분석: 다른 복사 방법과 비교
  6. System.arraycopy의 내부 동작 원리
  7. 주의사항과 제한 사항
  8. 실전 활용 사례
  9. 자주 묻는 질문(FAQ)
  10. 마무리

 

#1. System.arraycopy란?

System.arraycopy는 Java의 표준 라이브러리에 포함된 네이티브 메서드로, 한 배열에서 다른 배열로 요소를 빠르게 복사하는 기능을 제공합니다. 이 메서드는 JDK 1.0부터 제공되어 왔으며, Java의 가장 기본적이면서도 강력한 배열 조작 도구 중 하나입니다.

주요 특징

  • 네이티브 메서드: C/C++로 구현되어 JVM의 최적화된 성능을 제공
  • 빠른 속도: 자바의 다른 배열 복사 방법보다 일반적으로 더 빠름
  • 메모리 효율성: 새 배열을 생성하지 않고 기존 배열로 직접 복사 가능
  • 부분 복사: 배열의 특정 부분만 선택적으로 복사 가능
  • 다양한 타입 지원: 기본 데이터 타입과 객체 배열 모두 지원

 

#2. 기본 문법과 매개변수

System.arraycopy 메서드의 기본 문법은 다음과 같습니다:

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

각 매개변수의 의미는 다음과 같습니다:

No 매개변수 설명
1 src 원본 배열 (복사할 데이터가 있는 배열)
2 srcPos 원본 배열에서 복사를 시작할 인덱스
3 dest 대상 배열 (데이터가 복사될 배열)
4 destPos 대상 배열에서 데이터를 저장할 시작 인덱스
5 length 복사할 요소의 개수

간단한 예시

int[] source = {1, 2, 3, 4, 5};
int[] destination = new int[5];

// source 배열의 내용을 destination 배열로 복사
System.arraycopy(source, 0, destination, 0, source.length);

// 결과: destination = {1, 2, 3, 4, 5}

 

#3. System.arraycopy 사용 예제

실제 코드를 통해 System.arraycopy의 다양한 사용법을 알아보겠습니다.

예제 1: 전체 배열 복사

public class ArrayCopyExample1 {
    public static void main(String[] args) {
        int[] sourceArray = {1, 2, 3, 4, 5};
        int[] destArray = new int[sourceArray.length];

        // 전체 배열 복사
        System.arraycopy(sourceArray, 0, destArray, 0, sourceArray.length);

        // 결과 출력
        System.out.print("복사된 배열: ");
        for (int value : destArray) {
            System.out.print(value + " ");
        }
        // 출력 결과: 복사된 배열: 1 2 3 4 5
    }
}

예제 2: 배열의 일부분만 복사

public class ArrayCopyExample2 {
    public static void main(String[] args) {
        int[] sourceArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int[] destArray = new int[5];

        // 원본 배열의 인덱스 3부터 5개 요소를 대상 배열의 인덱스 0부터 복사
        System.arraycopy(sourceArray, 3, destArray, 0, 5);

        // 결과 출력
        System.out.print("부분 복사된 배열: ");
        for (int value : destArray) {
            System.out.print(value + " ");
        }
        // 출력 결과: 부분 복사된 배열: 4 5 6 7 8
    }
}

예제 3: 동일한 배열 내에서 요소 이동

public class ArrayCopyExample3 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};

        // 인덱스 0부터 3개 요소를 같은 배열의 인덱스 1로 이동
        // 주의: 이렇게 하면 array[0]의 값은 유지됨
        System.arraycopy(array, 0, array, 1, 3);

        // 결과 출력
        System.out.print("요소 이동 후 배열: ");
        for (int value : array) {
            System.out.print(value + " ");
        }
        // 출력 결과: 요소 이동 후 배열: 1 1 2 3 5
    }
}

예제 4: 배열의 특정 요소 삭제

public class ArrayCopyExample4 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        int removeIndex = 2; // 인덱스 2(값 3)를 삭제

        // 삭제할 요소 다음부터 끝까지의 요소들을 한 칸씩 앞으로 이동
        System.arraycopy(array, removeIndex + 1, array, removeIndex, array.length - removeIndex - 1);

        // 마지막 요소는 0으로 설정 (또는 적절한 기본값)
        array[array.length - 1] = 0;

        // 결과 출력
        System.out.print("요소 삭제 후 배열: ");
        for (int value : array) {
            System.out.print(value + " ");
        }
        // 출력 결과: 요소 삭제 후 배열: 1 2 4 5 0
    }
}

 

#4. 다양한 데이터 타입 복사하기

System.arraycopy는 기본 데이터 타입과 객체 배열 모두에 사용할 수 있습니다.

기본 데이터 타입 배열 복사

// byte 배열 복사
byte[] srcBytes = {1, 2, 3, 4, 5};
byte[] destBytes = new byte[srcBytes.length];
System.arraycopy(srcBytes, 0, destBytes, 0, srcBytes.length);

// char 배열 복사
char[] srcChars = {'a', 'b', 'c', 'd', 'e'};
char[] destChars = new char[srcChars.length];
System.arraycopy(srcChars, 0, destChars, 0, srcChars.length);

// boolean 배열 복사
boolean[] srcBooleans = {true, false, true, false, true};
boolean[] destBooleans = new boolean[srcBooleans.length];
System.arraycopy(srcBooleans, 0, destBooleans, 0, srcBooleans.length);

// double 배열 복사
double[] srcDoubles = {1.1, 2.2, 3.3, 4.4, 5.5};
double[] destDoubles = new double[srcDoubles.length];
System.arraycopy(srcDoubles, 0, destDoubles, 0, srcDoubles.length);

객체 배열 복사 (얕은 복사)

public class ObjectArrayCopyExample {
    static class Person {
        String name;
        int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{name='" + name + "', age=" + age + "}";
        }
    }

    public static void main(String[] args) {
        // 객체 배열 생성
        Person[] sourcePeople = {
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Charlie", 35)
        };

        // 동일한 크기의 대상 배열 생성
        Person[] destPeople = new Person[sourcePeople.length];

        // 객체 배열 복사 (얕은 복사)
        System.arraycopy(sourcePeople, 0, destPeople, 0, sourcePeople.length);

        // 결과 출력
        System.out.println("원본 배열과 복사된 배열의 참조 비교:");
        for (int i = 0; i < sourcePeople.length; i++) {
            System.out.println("sourcePeople[" + i + "] == destPeople[" + i + "]: " 
                              + (sourcePeople[i] == destPeople[i]));
        }

        // 원본 배열의 객체 수정
        sourcePeople[0].name = "Alice Modified";

        // 복사된 배열에서도 변경 확인
        System.out.println("\n원본 배열 객체 수정 후 복사된 배열:");
        for (Person person : destPeople) {
            System.out.println(person);
        }
    }
}

위 예제에서 객체 배열을 복사할 때는 참조만 복사되는 얕은 복사(shallow copy)가 발생합니다. 즉, 원본 배열과 대상 배열의 각 요소는 동일한 객체를 참조합니다.

 

#5. 성능 분석: 다른 복사 방법과 비교

Java에서 배열을 복사하는 방법은 여러 가지가 있습니다. 각 방법의 성능을 비교해보겠습니다.

배열 복사 방법들

  1. System.arraycopy(): 네이티브 메서드로 가장 빠른 성능 제공
  2. Arrays.copyOf(): 내부적으로 System.arraycopy 호출, 새 배열 생성
  3. Arrays.copyOfRange(): 부분 배열 복사, 내부적으로 System.arraycopy 사용
  4. clone(): 배열의 복제본 생성, 새 배열 반환
  5. 반복문을 사용한 수동 복사: 요소별로 직접 복사

성능 비교 코드

public class ArrayCopyPerformanceTest {
    private static final int ARRAY_SIZE = 10_000_000;
    private static final int ITERATIONS = 100;

    public static void main(String[] args) {
        int[] sourceArray = new int[ARRAY_SIZE];
        for (int i = 0; i < ARRAY_SIZE; i++) {
            sourceArray[i] = i;
        }

        // 1. System.arraycopy
        long startTime = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            int[] destArray = new int[ARRAY_SIZE];
            System.arraycopy(sourceArray, 0, destArray, 0, ARRAY_SIZE);
        }
        long systemArrayCopyTime = System.nanoTime() - startTime;

        // 2. Arrays.copyOf
        startTime = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            int[] destArray = Arrays.copyOf(sourceArray, ARRAY_SIZE);
        }
        long arraysCopyOfTime = System.nanoTime() - startTime;

        // 3. Arrays.copyOfRange
        startTime = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            int[] destArray = Arrays.copyOfRange(sourceArray, 0, ARRAY_SIZE);
        }
        long arraysCopyOfRangeTime = System.nanoTime() - startTime;

        // 4. clone
        startTime = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            int[] destArray = sourceArray.clone();
        }
        long cloneTime = System.nanoTime() - startTime;

        // 5. Manual copy with loop
        startTime = System.nanoTime();
        for (int i = 0; i < ITERATIONS; i++) {
            int[] destArray = new int[ARRAY_SIZE];
            for (int j = 0; j < ARRAY_SIZE; j++) {
                destArray[j] = sourceArray[j];
            }
        }
        long manualLoopTime = System.nanoTime() - startTime;

        // 결과 출력 (밀리초 단위로 변환)
        System.out.println("배열 크기: " + ARRAY_SIZE);
        System.out.println("반복 횟수: " + ITERATIONS);
        System.out.println("System.arraycopy 시간: " + systemArrayCopyTime / 1_000_000 + " ms");
        System.out.println("Arrays.copyOf 시간: " + arraysCopyOfTime / 1_000_000 + " ms");
        System.out.println("Arrays.copyOfRange 시간: " + arraysCopyOfRangeTime / 1_000_000 + " ms");
        System.out.println("clone 시간: " + cloneTime / 1_000_000 + " ms");
        System.out.println("수동 반복문 시간: " + manualLoopTime / 1_000_000 + " ms");
    }
}

성능 비교 결과

실행 환경에 따라 결과가 다를 수 있지만, 일반적으로 다음과 같은 성능 순서를 보입니다:

  1. System.arraycopy(): 가장 빠름 (네이티브 구현)
  2. clone(): System.arraycopy보다 약간 느림
  3. Arrays.copyOf(): System.arraycopy를 내부적으로 사용하지만 새 배열 생성 오버헤드 있음
  4. Arrays.copyOfRange(): copyOf와 유사하지만 범위 계산 오버헤드 추가
  5. 수동 반복문: 가장 느림 (JVM 최적화 부족)

배열 크기가 클수록 System.arraycopy의 성능 이점이 더 두드러집니다.

 

#6. System.arraycopy의 내부 동작 원리

System.arraycopy는 Java Native Interface(JNI)를 통해 구현된 네이티브 메서드입니다. 내부적으로는 C/C++로 작성되어 있어 JVM이 직접 최적화된 메모리 복사 작업을 수행합니다.

주요 동작 원리

  1. 메모리 블록 복사: 배열은 연속된 메모리 공간에 저장되므로, 시스템 수준에서 효율적인 메모리 블록 복사 연산이 수행됩니다.
  2. 타입 검사: JVM은 복사 과정에서 배열 타입의 호환성을 검사합니다. 호환되지 않는 타입의 배열 간 복사를 시도하면 ArrayStoreException이 발생합니다.
  3. 경계 검사: 원본 및 대상 배열의 인덱스와 길이가 유효한지 검사합니다. 배열 범위를 벗어나면 IndexOutOfBoundsException이 발생합니다.

OpenJDK 구현

OpenJDK의 System.arraycopy 구현은 다음과 같이 동작합니다:

  1. HotSpot JVM에서는 Unsafe.copyMemory() 또는 최적화된 메모리 복사 함수를 사용
  2. 원시 타입 배열은 직접 메모리 복사로 처리
  3. 객체 배열은 각 요소의 참조를 복사하고 필요한 경우 타입 검사를 수행

 

#7. 주의사항과 제한 사항

System.arraycopy를 사용할 때 알아두어야 할 주의사항들입니다.

1. 타입 호환성

원본 배열과 대상 배열은 타입이 호환되어야 합니다. 예를 들어, int[] 배열에서 double[] 배열로 직접 복사할 수 없습니다.

int[] sourceInts = {1, 2, 3};
double[] destDoubles = new double[3];

// 오류 발생: ArrayStoreException
System.arraycopy(sourceInts, 0, destDoubles, 0, 3);

객체 배열의 경우, 하위 타입에서 상위 타입으로는 복사가 가능하지만, 역방향은 불가능합니다.

// 가능: StringObject의 하위 타입
String[] sourceStrings = {"A", "B", "C"};
Object[] destObjects = new Object[3];
System.arraycopy(sourceStrings, 0, destObjects, 0, 3);

// 불가능: Object에서 String으로 직접 복사 시도
Object[] sourceObjects = {"A", "B", "C"};
String[] destStrings = new String[3];
// 다음 코드는 런타임 오류 발생 가능성 있음
System.arraycopy(sourceObjects, 0, destStrings, 0, 3);

2. 배열 범위 문제

배열 범위를 벗어나는 복사 시도는 IndexOutOfBoundsException을 발생시킵니다.

int[] source = {1, 2, 3, 4, 5};
int[] dest = new int[3];

// 오류 발생: 원본 배열 크기(5)가 대상 배열 크기(3)보다 큼
System.arraycopy(source, 0, dest, 0, 5);

// 오류 발생: srcPos(3) + length(5) > source.length(5)
System.arraycopy(source, 3, dest, 0, 5);

3. null 배열 처리

원본 또는 대상 배열이 null인 경우 NullPointerException이 발생합니다.

int[] source = {1, 2, 3};
int[] dest = null;

// 오류 발생: NullPointerException
System.arraycopy(source, 0, dest, 0, 3);

4. 얕은 복사 주의

객체 배열 복사 시 얕은 복사(shallow copy)가 수행됩니다. 즉, 객체 자체가 아닌 객체에 대한 참조만 복사됩니다.

// Person 객체 배열
Person[] people1 = {new Person("Alice"), new Person("Bob")};
Person[] people2 = new Person[2];

// 얕은 복사 수행
System.arraycopy(people1, 0, people2, 0, 2);

// 원본 배열의 객체 수정이 복사본에도 영향을 미침
people1[0].name = "Charlie";
System.out.println(people2[0].name); // 출력: Charlie

 

#8. 실전 활용 사례

1. 동적 배열 구현 (ArrayList와 유사)

public class DynamicArray {
    private int[] array;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public DynamicArray() {
        array = new int[DEFAULT_CAPACITY];
        size = 0;
    }

    public void add(int element) {
        if (size == array.length) {
            // 배열 크기 조정 필요
            resize();
        }
        array[size++] = element;
    }

    private void resize() {
        // 기존 배열 크기의 두 배로 새 배열 생성
        int[] newArray = new int[array.length * 2];

        // System.arraycopy로 기존 요소 복사
        System.arraycopy(array, 0, newArray, 0, size);

        // 새 배열로 참조 업데이트
        array = newArray;
    }

    public void remove(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }

        // 삭제할 요소 이후의 모든 요소를 한 칸씩 앞으로 이동
        System.arraycopy(array, index + 1, array, index, size - index - 1);

        // 마지막 요소 정리 및 크기 감소
        array[--size] = 0;
    }

    public int get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
        return array[index];
    }

    public int size() {
        return size;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < size; i++) {
            sb.append(array[i]);
            if (i < size - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) {
        DynamicArray dynamicArray = new DynamicArray();

        // 요소 추가
        for (int i = 0; i < 15; i++) {
            dynamicArray.add(i * 10);
        }

        System.out.println("초기 배열: " + dynamicArray);

        // 요소 삭제
        dynamicArray.remove(5);
        System.out.println("인덱스 5 삭제 후: " + dynamicArray);

        // 첫 번째 요소 삭제
        dynamicArray.remove(0);
        System.out.println("첫 번째 요소 삭제 후: " + dynamicArray);
    }
}

2. 큐(Queue) 구현

public class ArrayQueue {
    private int[] elements;
    private int front;
    private int rear;
    private int size;
    private int capacity;

    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        elements = new int[capacity];
        front = 0;
        rear = -1;
        size = 0;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public boolean isFull() {
        return size == capacity;
    }

    public void enqueue(int item) {
        if (isFull()) {
            throw new IllegalStateException("Queue is full");
        }

        rear = (rear + 1) % capacity;
        elements[rear] = item;
        size++;
    }

    public int dequeue() {
        if (isEmpty()) {
            throw new IllegalStateException("Queue is empty");
        }

        int item = elements[front];
        front = (front + 1) % capacity;
        size--;
        return item;
    }

    public int peek() {
        if (isEmpty()) {
            throw new IllegalStateException("Queue is empty");
        }
        return elements[front];
    }

    public int size() {
        return size;
    }

    // 큐 내용을 새 배열로 복사하여 반환
    public int[] toArray() {
        int[] result = new int[size];

        if (front <= rear) {
            // 단순한 경우: front에서 rear까지 연속된 영역
            System.arraycopy(elements, front, result, 0, size);
        } else {
            // 복잡한 경우: front부터 배열 끝까지, 그리고 0부터 rear까지
            int firstPart = capacity - front;
            System.arraycopy(elements, front, result, 0, firstPart);
            System.arraycopy(elements, 0, result, firstPart, rear + 1);
        }

        return result;
    }

    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(5);

        // 큐에 요소 추가
        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);

        // 앞쪽 요소 확인
        System.out.println("큐의 첫 번째 요소: " + queue.peek());

        // 요소 제거
        int removed = queue.dequeue();
        System.out.println("큐에서 제거된 요소: " + removed);

        // 현재 큐 상태 확인
        System.out.print("현재 큐 내용: ");
        for (int item : queue.toArray()) {
            System.out.print(item + " ");
        }
    }
}

3. 배열 합치기

public class ArrayMergeExample {
    public static int[] mergeArrays(int[] arr1, int[] arr2) {
        int[] result = new int[arr1.length + arr2.length];

        // 첫 번째 배열 복사
        System.arraycopy(arr1, 0, result, 0, arr1.length);

        // 두 번째 배열 복사
        System.arraycopy(arr2, 0, result, arr1.length, arr2.length);

        return result;
    }

    public static void main(String[] args) {
        int[] first = {1, 2, 3, 4, 5};
        int[] second = {6, 7, 8, 9, 10};

        int[] merged = mergeArrays(first, second);

        System.out.print("합쳐진 배열: ");
        for (int value : merged) {
            System.out.print(value + " ");
        }
    }
}

#9. 자주 묻는 질문(FAQ)

Q1: System.arraycopy와 Arrays.copyOf의 차이점은 무엇인가요?

A: 주요 차이점은 다음과 같습니다:

  1. 반환 값: System.arraycopy는 void를 반환하며 기존 배열에 복사하고, Arrays.copyOf는 새 배열을 생성하여 반환합니다.
  2. 기능: System.arraycopy는 더 세밀한 제어(시작 위치, 길이 등)가 가능하고, Arrays.copyOf는 보다 간단한 인터페이스를 제공합니다.
  3. 내부 구현: Arrays.copyOf는 내부적으로 System.arraycopy를 호출합니다.
  4. 성능: Arrays.copyOf는 새 배열 생성 오버헤드 때문에 System.arraycopy보다 약간 느립니다.

Q2: 배열을 복사할 때 발생할 수 있는 예외는 무엇인가요?

A: System.arraycopy 사용 시 발생할 수 있는 주요 예외는 다음과 같습니다:

  1. ArrayStoreException: 호환되지 않는 타입의 배열 간 복사 시도
  2. IndexOutOfBoundsException: 배열 범위를 벗어나는 인덱스 사용
  3. NullPointerException: 원본 또는 대상 배열이 null인 경우

Q3: 객체 배열의 깊은 복사(deep copy)를 하려면 어떻게 해야 하나요?

A: System.arraycopy는 얕은 복사만 수행합니다. 깊은 복사를 구현하려면 각 객체를 개별적으로 복제해야 합니다:

// 깊은 복사 예제
Person[] original = {new Person("Alice"), new Person("Bob")};
Person[] deepCopy = new Person[original.length];

// 각 객체를 복제하여 새 배열에 저장
for (int i = 0; i < original.length; i++) {
    deepCopy[i] = original[i].clone(); // Person 클래스에 clone 메서드 구현 필요
}

또는 직렬화/역직렬화를 사용할 수도 있습니다.

Q4: 다차원 배열도 System.arraycopy로 복사할 수 있나요?

A: System.arraycopy는 1차원 배열 복사에 최적화되어 있습니다. 다차원 배열의 경우 첫 차원만 얕은 복사가 됩니다. 완전한 복사를 위해서는 중첩 반복문이나 재귀를 사용해야 합니다:

// 2차원 배열 복사 예제
int[][] original = {{1, 2}, {3, 4}, {5, 6}};
int[][] copy = new int[original.length][];

// 각 하위 배열 복사
for (int i = 0; i < original.length; i++) {
    copy[i] = new int[original[i].length];
    System.arraycopy(original[i], 0, copy[i], 0, original[i].length);
}

Q5: System.arraycopy가 clone() 메서드보다 항상 더 빠른가요?

A: 대부분의 경우 System.arraycopy가 clone()보다 빠르지만, 매우 작은 배열에서는 그 차이가 미미할 수 있습니다. 또한 최신 JVM은 clone() 메서드 호출도 많이 최적화했기 때문에 성능 차이가 줄어들었습니다. 그러나 대용량 배열이나 성능이 중요한 애플리케이션에서는 여전히 System.arraycopy가 선호됩니다.


#10. 마무리

이 글에서는 Java의 System.arraycopy 메서드에 대해 상세히 알아보았습니다. 기본 사용법부터 성능 분석, 실전 활용 사례까지 다양한 측면에서 이 강력한 배열 복사 메서드를 살펴보았습니다.

System.arraycopy는 Java의 가장 기본적인 배열 조작 도구 중 하나이면서도, 최적화된 성능을 제공하는 강력한 메서드입니다. 특히 대용량 데이터를 다루거나 성능에 민감한 애플리케이션에서는 이 메서드의 특성을 잘 이해하고 활용하는 것이 중요합니다.

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

끝.

 

참고 자료

반응형